tests/Test-AwsCiemConnector.ps1
|
param( [string]$ConnectorPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'modules/ISC.AwsConnector.psm1'), [string]$CiemPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'modules/ISC.AwsCiemConnector.psm1'), [string]$ConsolePath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'modules/ISC.OperatorConsole.psm1') ) $ErrorActionPreference = 'Stop' $script:AssertionCount = 0 function Assert-True { param([bool]$Condition, [string]$Message) $script:AssertionCount++ if (-not $Condition) { throw "Assertion failed: $Message" } } function Assert-Equal { param($Expected, $Actual, [string]$Message) $script:AssertionCount++ if ($Expected -ne $Actual) { throw "Assertion failed: $Message. Expected '$Expected', got '$Actual'." } } # AWS.Tools cmdlets are absent here, so these globals stand in for the CloudTrail scan. function Mock-CiemTrailScan { param([object[]]$Trails) $script:MockCiemTrails = @($Trails) Set-Item -Path function:global:Get-EC2Region -Value { [CmdletBinding()] param([string]$Region) throw 'EC2 region enumeration is unavailable in tests' } Set-Item -Path function:global:Get-CTTrail -Value { [CmdletBinding()] param([string]$Region) $script:MockCiemTrails } } function Clear-CiemTrailScanMock { Remove-Item -Path function:global:Get-CTTrail -ErrorAction SilentlyContinue Remove-Item -Path function:global:Get-EC2Region -ErrorAction SilentlyContinue $script:MockCiemTrails = @() } function Mock-S3BucketList { param([string[]]$BucketNames) $script:MockS3Buckets = @($BucketNames | ForEach-Object { [PSCustomObject]@{ BucketName = $_ } }) Set-Item -Path function:global:Get-S3Bucket -Value { [CmdletBinding()] param([string]$BucketName, [string]$Region) $script:MockS3Buckets } } function Clear-S3BucketListMock { Remove-Item -Path function:global:Get-S3Bucket -ErrorAction SilentlyContinue $script:MockS3Buckets = @() } Import-Module $ConsolePath -Force -WarningAction SilentlyContinue Import-Module $ConnectorPath -Force -WarningAction SilentlyContinue Import-Module $CiemPath -Force -WarningAction SilentlyContinue Initialize-AwsConnectorData Initialize-AwsCiemConnectorData Assert-True (-not (Test-EmbeddedCiemSelected -FeatureNames @('ActivityInsights'))) 'Ciem not selected without Ciem pack' Assert-True (Test-EmbeddedCiemSelected -FeatureNames @('Ciem', 'ActivityInsights')) 'Ciem selected with Ciem pack' Assert-True (Test-AwsSaasCiemFeature -FeatureNames @('Ciem')) 'Test-AwsSaasCiemFeature alias' $pack = Get-EmbeddedCiemFeaturePack Assert-True ($pack.Label -like '*CIEM*') 'embedded CIEM feature pack label' $empty = Build-EmbeddedCiemConnectionSettings -Context @{ FeatureNames = @() } Assert-True ($empty.Count -eq 0) 'no CIEM settings when Ciem not selected' $arns = @(Get-NormalizedCloudTrailArnList -Value @(' arn:a , arn:b ')) Assert-True ($arns.Count -eq 2) 'normalized CloudTrail ARN list' # A trail this run created is authoritative: no scan, so unrelated trails cannot leak in. $createdArn = 'arn:aws:cloudtrail:us-east-1:123456789012:trail/sailpoint-ciem-cloud-trail' $kept = @(Get-SaasCiemCloudTrailArns -HomeRegion 'us-east-1' -BucketName 'bucket' -ExtraArn @($createdArn) -SkipDiscovery) Assert-Equal 1 $kept.Count 'SkipDiscovery keeps only the ARNs passed in' Assert-Equal $createdArn $kept[0] 'SkipDiscovery preserves the created trail ARN' foreach ($ctx in @( @{ FeatureNames = @('Ciem'); Region = 'us-east-1'; CloudTrailBucket = 'bucket' CloudTrailBucketAccountId = '123456789012'; CloudTrailArn = @($createdArn); SkipCloudTrailDiscovery = $true } [PSCustomObject]@{ FeatureNames = @('Ciem'); Region = 'us-east-1'; CloudTrailBucket = 'bucket' CloudTrailBucketAccountId = '123456789012'; CloudTrailArn = @($createdArn); SkipCloudTrailDiscovery = $true } )) { $settings = Build-EmbeddedCiemConnectionSettings -Context $ctx Assert-Equal $createdArn $settings['CloudTrail ARN(s)'] 'created trail is the only ARN in CIEM settings' } try { Get-CiemCloudTrailArns -HomeRegion 'us-east-1' -BucketOnly | Out-Null throw 'BucketOnly without a bucket name should fail' } catch { Assert-True ($_.Exception.Message -like '*requires -BucketName*') 'BucketOnly requires a bucket name' } # Discovery for CIEM settings stays scoped to the chosen bucket: ISC can only read that bucket. $bucketTrail = 'arn:aws:cloudtrail:us-east-1:123456789012:trail/picked' $otherTrail = 'arn:aws:cloudtrail:eu-west-1:123456789012:trail/unrelated' Mock-CiemTrailScan -Trails @( [PSCustomObject]@{ TrailARN = $bucketTrail; S3BucketName = 'picked-bucket' } [PSCustomObject]@{ TrailARN = $otherTrail; S3BucketName = 'some-other-bucket' } ) try { $scoped = @(Get-SaasCiemCloudTrailArns -HomeRegion 'us-east-1' -BucketName 'picked-bucket') Assert-Equal 1 $scoped.Count 'discovery keeps only trails for the chosen bucket' Assert-Equal $bucketTrail $scoped[0] 'unrelated trails stay out of CIEM settings' $options = @(Get-CloudTrailLogBucketOptions -HomeRegion 'us-east-1' -ScanScope HomeRegion) Assert-Equal 2 $options.Count 'discovery lists each CloudTrail log bucket' $picked = @($options | Where-Object { $_.BucketName -eq 'picked-bucket' })[0] Assert-Equal $bucketTrail (@($picked.TrailArns)[0]) 'discovered bucket keeps its trail ARN' $auto = Select-CloudTrailLogBucketOption -Options @($picked) Assert-Equal 'picked-bucket' $auto.BucketName 'a single discovered bucket is selected without a prompt' $many = Select-CloudTrailLogBucketOption -Options $options Assert-True ($null -eq $many) 'multiple buckets stay unselected so the wizard can prompt' $preferred = Select-CloudTrailLogBucketOption -Options $options -PreferredBucket 'picked-bucket' Assert-Equal 'picked-bucket' $preferred.BucketName 'an explicit bucket name matches among discovered trails' } finally { Clear-CiemTrailScanMock } # NewCloudTrailExistingBucket needs a bucket that exists, with or without a trail, so that mode picks # from S3 rather than from CloudTrail. Mock-S3BucketList -BucketNames @('zeta-logs', 'alpha-logs') try { $buckets = @(Get-AwsS3BucketNames -RegionName 'us-east-1') Assert-Equal 2 $buckets.Count 'S3 discovery lists buckets in the account' Assert-Equal 'alpha-logs' $buckets[0] 'S3 bucket names are sorted for the picker' } finally { Clear-S3BucketListMock } $bucketPolicy = New-CloudTrailBucketPolicyDocument -BucketName 'my-bucket' -Partition 'aws' Assert-True ($bucketPolicy -like '*my-bucket*') 'bucket policy references bucket name' # SailPoint's existing-bucket template creates the trail but does not attach an S3 resource policy. # CloudTrail then fails with "Incorrect S3 bucket policy is detected". $delivery = New-CloudTrailDeliveryBucketPolicyDocument -BucketName 'sailpoint-ciem-063085096017' ` -AccountId '063085096017' -Partition 'aws' $deliveryDoc = $delivery | ConvertFrom-Json $deliverySids = @($deliveryDoc.Statement | ForEach-Object { [string]$_.Sid }) Assert-True ($deliverySids -contains 'AWSCloudTrailAclCheck') 'delivery policy allows CloudTrail ACL check' Assert-True ($deliverySids -contains 'AWSCloudTrailWrite') 'delivery policy allows CloudTrail PutObject' Assert-True (($delivery | ConvertFrom-Json).Statement[1].Resource -eq 'arn:aws:s3:::sailpoint-ciem-063085096017/AWSLogs/063085096017/*') ` 'delivery PutObject is scoped to this account prefix' # Get-S3BucketPolicy (AWS.Tools v5 / S3 rest-xml) can return the XML envelope instead of JSON. # Feeding that to ConvertFrom-Json is: Unexpected character encountered while parsing value: <. $xmlWrapped = @' <GetBucketPolicyOutput> <Policy>{"Version":"2012-10-17","Statement":[{"Sid":"KeepMe","Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::other/*"}]}</Policy> </GetBucketPolicyOutput> '@ $fromXml = ConvertTo-NormalizedS3BucketPolicyDocument -Document $xmlWrapped Assert-True ($fromXml.TrimStart().StartsWith('{')) 'XML-wrapped bucket policy extracts JSON' $xmlMerged = Merge-CloudTrailDeliveryBucketPolicy -ExistingDocument $fromXml -DeliveryDocument $delivery | ConvertFrom-Json Assert-True (@($xmlMerged.Statement | ForEach-Object { [string]$_.Sid }) -contains 'KeepMe') 'XML-wrapped existing policy still merges' $noPolicyXml = @' <?xml version="1.0" encoding="UTF-8"?> <Error><Code>NoSuchBucketPolicy</Code><Message>The bucket policy does not exist</Message></Error> '@ Assert-Equal '' (ConvertTo-NormalizedS3BucketPolicyDocument -Document $noPolicyXml) 'NoSuchBucketPolicy XML is treated as no policy' try { ConvertTo-NormalizedS3BucketPolicyDocument -Document '<html><body>Access Denied</body></html>' | Out-Null throw 'should have rejected the document' } catch { Assert-True ($_.Exception.Message -like '*HTML*not JSON*') 'HTML GetBucketPolicy body is rejected with a clear error' } try { ConvertTo-NormalizedS3BucketPolicyDocument -Document @' <Error><Code>PermanentRedirect</Code><Message>The bucket you are attempting to access must be addressed using the specified endpoint.</Message><Endpoint>s3.eu-west-1.amazonaws.com</Endpoint></Error> '@ | Out-Null throw 'should have rejected the document' } catch { Assert-True ($_.Exception.Message -like '*PermanentRedirect*') 'S3 error XML surfaces the error code' } Assert-Equal 'us-east-1' (ConvertTo-AwsRegionNameFromS3Location -Location $null -FallbackRegion 'eu-west-1') 'empty LocationConstraint is us-east-1' Assert-Equal 'eu-west-1' (ConvertTo-AwsRegionNameFromS3Location -Location 'EU' -FallbackRegion 'us-east-1') 'legacy EU constraint is eu-west-1' Assert-Equal 'eu-west-1' (ConvertTo-AwsRegionNameFromS3Location -Location 'eu-west-1' -FallbackRegion 'us-east-1') 'region-name constraint is kept' Assert-Equal 'us-gov-west-1' (ConvertTo-AwsRegionNameFromS3Location -Location '' -FallbackRegion 'us-gov-west-1') 'empty GovCloud location keeps a gov fallback' $existingWithOther = ConvertTo-IamJson @{ Version = '2012-10-17' Statement = @( @{ Sid = 'KeepMe'; Effect = 'Allow'; Principal = '*'; Action = 's3:GetObject'; Resource = 'arn:aws:s3:::other/*' } @{ Sid = 'AWSCloudTrailWrite'; Effect = 'Deny'; Principal = '*'; Action = 's3:PutObject'; Resource = '*' } ) } $merged = Merge-CloudTrailDeliveryBucketPolicy -ExistingDocument $existingWithOther -DeliveryDocument $delivery | ConvertFrom-Json $mergedSids = @($merged.Statement | ForEach-Object { [string]$_.Sid }) Assert-True ($mergedSids -contains 'KeepMe') 'merge keeps unrelated bucket-policy statements' Assert-Equal 1 @($mergedSids | Where-Object { $_ -eq 'AWSCloudTrailWrite' }).Count 'merge replaces stale CloudTrail write statement' Assert-True (@($merged.Statement | Where-Object { $_.Sid -eq 'AWSCloudTrailWrite' })[0].Effect -eq 'Allow') ` 'replaced CloudTrail write statement is Allow' Assert-True (Test-CiemCfnStackNeedsDeleteBeforeRedeploy -StackStatus 'ROLLBACK_COMPLETE') 'ROLLBACK_COMPLETE needs delete' Assert-True (-not (Test-CiemCfnStackNeedsDeleteBeforeRedeploy -StackStatus 'CREATE_COMPLETE')) 'CREATE_COMPLETE does not need delete' $cfnLines = @(Select-CiemCfnStackFailureLines -Events @( [PSCustomObject]@{ LogicalResourceId = 'SailPointSaasCiemCloudTrail'; ResourceStatus = 'ROLLBACK_COMPLETE'; ResourceStatusReason = 'Validation failed with 1 error(s). Call DescribeEvents to retrieve the full list of issues with resource and property details, resolve each error, then retry the operation.' } [PSCustomObject]@{ LogicalResourceId = 'CloudTrailBucket'; ResourceStatus = 'CREATE_FAILED'; ResourceStatusReason = 'example-trail-bucket already exists' } )) Assert-Equal 1 $cfnLines.Count 'generic DescribeEvents stack reason is dropped when a resource failure exists' Assert-True ($cfnLines[0] -like '*already exists*') 'resource CREATE_FAILED reason is kept' $deployResult = [PSCustomObject]@{ RoleArn = 'arn:aws:iam::123456789012:role/SailPointCIEMAuditRole' ExternalId = 'ext-id' CloudTrailArns = @('arn:aws:cloudtrail:us-east-1:123456789012:trail/t1') BucketName = 'bucket' SingleAccount = $false ProvisionIdentityCenter = $true ReadIdentityCenter = $true } $dedicated = Build-DedicatedCiemConnectionSettings -DeploymentResult $deployResult -SessionAccountId '123456789012' Assert-Equal $deployResult.RoleArn $dedicated.Fields['Role ARN'] 'dedicated settings role ARN' Write-Host "PASS ($script:AssertionCount assertions)" |