modules/ISC.AwsSaasConnector.psm1
|
#Requires -Version 5.1 Set-StrictMode -Version Latest function Initialize-AwsSaasConnectorData { $script:AggregationActions = @( 'iam:GetPolicyVersion' 'iam:ListServiceSpecificCredentials' 'iam:ListMFADevices' 'iam:ListSigningCertificates' 'iam:GetGroup' 'iam:ListSSHPublicKeys' 'iam:ListAttachedRolePolicies' 'iam:ListAttachedUserPolicies' 'iam:ListAttachedGroupPolicies' 'iam:ListRolePolicies' 'iam:ListAccessKeys' 'iam:ListPolicies' 'iam:GetRole' 'iam:GetPolicy' 'iam:ListGroupPolicies' 'iam:ListRoles' 'iam:ListUserPolicies' 'iam:GetUserPolicy' 'iam:ListGroupsForUser' 'iam:ListAccountAliases' 'iam:ListUsers' 'iam:ListGroups' 'iam:GetGroupPolicy' 'iam:GetUser' 'iam:GetRolePolicy' 'iam:GetLoginProfile' 'iam:ListEntitiesForPolicy' 'iam:GetAccessKeyLastUsed' 'iam:ListUserTags' 'iam:ListRoleTags' 'iam:ListPolicyTags' ) $script:OrganizationActionsNonMgo = @( 'organizations:ListAccounts' ) $script:OrganizationActionsMgo = @( 'organizations:ListPoliciesForTarget' 'organizations:ListAccountsForParent' 'organizations:ListRoots' 'organizations:ListAccounts' 'organizations:ListTargetsForPolicy' 'organizations:DescribeOrganization' 'organizations:DescribeOrganizationalUnit' 'organizations:DescribeAccount' 'organizations:ListParents' 'organizations:ListOrganizationalUnitsForParent' 'organizations:DescribePolicy' 'organizations:ListPolicies' 'organizations:ListTagsForResource' ) $script:ProvisioningActionsNonMgo = @( 'iam:UpdateLoginProfile' 'iam:UpdateAccessKey' 'iam:CreateUser' 'iam:CreateAccessKey' 'iam:CreateLoginProfile' 'iam:RemoveUserFromGroup' 'iam:AddUserToGroup' 'iam:DeleteLoginProfile' 'iam:AttachUserPolicy' ) $script:ProvisioningActionsMgo = @( 'iam:UpdateLoginProfile' 'iam:CreateGroup' 'iam:DeleteAccessKey' 'iam:DeleteGroup' 'iam:AttachUserPolicy' 'iam:DeleteUserPolicy' 'iam:UpdateAccessKey' 'iam:AttachRolePolicy' 'iam:DeleteUser' 'iam:CreateUser' 'iam:CreateAccessKey' 'iam:CreatePolicy' 'iam:CreateLoginProfile' 'iam:RemoveUserFromGroup' 'iam:AddUserToGroup' 'iam:DetachRolePolicy' 'iam:DeleteSigningCertificate' 'iam:AttachGroupPolicy' 'iam:DeleteRolePolicy' 'iam:DetachGroupPolicy' 'iam:DetachUserPolicy' 'iam:DeleteGroupPolicy' 'iam:DeleteLoginProfile' ) $script:ActivityInsightsActions = @( 'cloudtrail:Get*' 'cloudtrail:Describe*' 'cloudtrail:List*' 'cloudtrail:LookupEvents' ) $script:BedrockAgentDiscoveryActions = @( 'bedrock:ListAgents' 'bedrock:GetAgent' 'bedrock:ListAgentActionGroups' 'bedrock:GetAgentActionGroup' 'bedrock:ListAgentKnowledgeBases' 'bedrock:GetAgentKnowledgeBase' 'bedrock:ListKnowledgeBases' 'bedrock:GetKnowledgeBase' 'bedrock:ListAgentAliases' 'bedrock:GetAgentAlias' 'bedrock:ListAgentVersions' 'bedrock:GetAgentVersion' 'bedrock:ListDataSources' 'bedrock:GetDataSource' 'bedrock:ListAgentCollaborators' 'bedrock:GetAgentCollaborator' 'kms:Decrypt' 'kms:GenerateDataKey' ) $script:AgentCoreDiscoveryActions = @( 'bedrock-agentcore:ListAgentRuntimes' 'bedrock-agentcore:GetAgentRuntime' 'bedrock-agentcore:ListAgentRuntimeEndpoints' 'bedrock-agentcore:GetAgentRuntimeEndpoint' 'bedrock-agentcore:ListAgentRuntimeVersions' 'bedrock-agentcore:ListGateways' 'bedrock-agentcore:GetGateway' 'bedrock-agentcore:ListGatewayTargets' 'bedrock-agentcore:GetGatewayTarget' 'bedrock-agentcore:ListWorkloadIdentities' 'bedrock-agentcore:GetWorkloadIdentity' 'bedrock-agentcore:ListOauth2CredentialProviders' 'bedrock-agentcore:GetApiKeyCredentialProvider' 'bedrock-agentcore:ListApiKeyCredentialProviders' 'bedrock-agentcore:GetOauth2CredentialProvider' ) $script:ConnectCustomerAgentDiscoveryActions = @( 'connect:ListInstances' 'connect:DescribeInstance' 'connect:ListBots' 'connect:ListLexBots' 'lex:ListBots' 'lex:GetBot' 'lex:DescribeBotAlias' 'lex:ListBotAliases' ) $script:AgentDiscoveryActions = @( $script:BedrockAgentDiscoveryActions + $script:AgentCoreDiscoveryActions + $script:ConnectCustomerAgentDiscoveryActions ) $script:FeaturePacks = [ordered]@{ ActivityInsights = @{ Label = 'Activity Insights - CloudTrail event lookup' PolicyName = 'SPActivityInsightsPolicy' Actions = $script:ActivityInsightsActions } Ciem = @{ Label = 'CIEM - Enable Cloud Infrastructure Entitlement Management, inventory and effective access' PolicyName = 'SPCiemPolicy' } AgentDiscovery = @{ Label = 'Machine identity - Bedrock, AgentCore, and Connect Customer agents' PolicyName = 'SPAgentDiscoveryPolicy' Actions = $script:AgentDiscoveryActions } } # SPIdentityCenter* are no longer produced: Identity Center belongs to the CIEM AWS source, not the # SaaS source. They stay listed so runs detach what earlier versions attached to the SaaS role. $script:ManagedPolicyNames = @( 'SPAggregationPolicy' 'SPOrganizationPolicy' 'SPProvisioningPolicy' 'SPActivityInsightsPolicy' 'SPCiemPolicy' 'SPAgentDiscoveryPolicy' 'SPIdentityCenterPolicy' 'SPIdentityCenterProvisioningPolicy' 'SPCloudTrailBucketPolicy' ) } function Get-SelectedPolicyDocuments { param( [Parameter(Mandatory)][string]$Set, [string[]]$FeatureNames, [switch]$SkipProvisioning, [string]$BucketName, [string]$Partition ) $docs = [ordered]@{} $docs['SPAggregationPolicy'] = New-StarPolicyDocument -Actions $script:AggregationActions $orgActions = if ($Set -eq 'NonMgo') { $script:OrganizationActionsNonMgo } else { $script:OrganizationActionsMgo } $docs['SPOrganizationPolicy'] = New-StarPolicyDocument -Actions $orgActions if (-not $SkipProvisioning) { $provActions = if ($Set -eq 'NonMgo') { $script:ProvisioningActionsNonMgo } else { $script:ProvisioningActionsMgo } $docs['SPProvisioningPolicy'] = New-StarPolicyDocument -Actions $provActions } foreach ($name in @($FeatureNames)) { if ([string]::IsNullOrWhiteSpace($name)) { continue } if ($name -in @('IdentityCenter', 'IdentityCenterProvisioning')) { Write-Warning "$name is not an AWS SaaS feature. Identity Center is governed by a CIEM AWS source (-SourceType CiemAws); ignoring it here." continue } if (-not $script:FeaturePacks.Contains($name)) { throw "Unknown AWS SaaS feature pack '$name'. Valid packs: $($script:FeaturePacks.Keys -join ', ')." } $pack = $script:FeaturePacks[$name] if ($name -eq 'Ciem') { $docs[$pack.PolicyName] = New-StarPolicyDocument -Actions (Get-CiemPolicyActions) } else { $actions = @($pack.Actions) if ($SkipProvisioning -and $name -eq 'AgentDiscovery') { $actions = @($actions | Where-Object { $_ -ne 'kms:GenerateDataKey' }) } $docs[$pack.PolicyName] = New-StarPolicyDocument -Actions $actions } } if ($BucketName) { $docs['SPCloudTrailBucketPolicy'] = New-CloudTrailBucketPolicyDocument -BucketName $BucketName -Partition $Partition } return $docs } function Get-LocalPolicyByName { param( [Parameter(Mandatory)][string]$Name, $AwsParams ) $policies = @(Get-IAMPolicyList @AwsParams -Scope Local | Where-Object { $_.PolicyName -eq $Name }) if ($policies.Count -gt 0) { return $policies[0] } return $null } function Set-CustomerManagedPolicy { param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Document, [Parameter(Mandatory)][string]$Description, $AwsParams ) $existing = Get-LocalPolicyByName -Name $Name -AwsParams $AwsParams if (-not $existing) { $created = New-IAMPolicy @AwsParams -PolicyName $Name -PolicyDocument $Document -Description $Description Write-Ok "Created policy $Name" return $created.Arn } $versions = @(Get-IAMPolicyVersionList @AwsParams -PolicyArn $existing.Arn) if ($versions.Count -ge 5) { $oldest = $versions | Where-Object { -not $_.IsDefaultVersion } | Sort-Object CreateDate | Select-Object -First 1 if ($oldest) { Remove-IAMPolicyVersion @AwsParams -PolicyArn $existing.Arn -VersionId $oldest.VersionId -Force } } New-IAMPolicyVersion @AwsParams -PolicyArn $existing.Arn -PolicyDocument $Document -SetAsDefault $true | Out-Null Write-Ok "Updated policy $Name" return $existing.Arn } function Set-SailPointIamRole { param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$TrustDocument, [Parameter(Mandatory)]$PolicyDocuments, [Parameter(Mandatory)][string]$AccountId, [Parameter(Mandatory)][string]$Partition, $AwsParams ) $role = $null try { $role = Get-IAMRole @AwsParams -RoleName $Name } catch { if ($_.Exception.Message -notmatch 'NoSuchEntity|cannot be found') { throw } } if ($role) { Update-IAMAssumeRolePolicy @AwsParams -RoleName $Name -PolicyDocument $TrustDocument Write-Ok "Updated trust policy on $Name" } else { $role = New-IAMRole @AwsParams -RoleName $Name -AssumeRolePolicyDocument $TrustDocument ` -Description 'SailPoint Identity Security Cloud - Amazon Web Services SaaS connector' ` -MaxSessionDuration 3600 Set-AwsCreatedRoleArn -Arn $role.Arn Write-Ok "Created role $($role.Arn)" } $desiredArns = [System.Collections.Generic.List[string]]::new() foreach ($policyName in $PolicyDocuments.Keys) { $arn = Set-CustomerManagedPolicy -Name $policyName -Document $PolicyDocuments[$policyName] ` -Description "SailPoint ISC AWS SaaS - $policyName" -AwsParams $AwsParams try { Register-IAMRolePolicy @AwsParams -RoleName $Name -PolicyArn $arn Write-Ok "Attached $policyName" } catch { if ($_.Exception.Message -match 'already attached|Duplicate') { Write-Info "Already attached $policyName" } else { throw } } $desiredArns.Add($arn) } $attached = @(Get-IAMAttachedRolePolicyList @AwsParams -RoleName $Name) foreach ($item in $attached) { $shortName = $item.PolicyName if ($script:ManagedPolicyNames -contains $shortName -and $desiredArns -notcontains $item.PolicyArn) { Unregister-IAMRolePolicy @AwsParams -RoleName $Name -PolicyArn $item.PolicyArn Write-Info "Detached unused $shortName" } } return "arn:${Partition}:iam::${AccountId}:role/${Name}" } function Get-AwsSaasCatalog { $packs = [ordered]@{} foreach ($key in @($script:FeaturePacks.Keys | Sort-Object { [string]$script:FeaturePacks[$_].Label })) { $packs[$key] = $script:FeaturePacks[$key] } return [PSCustomObject]@{ FeaturePacks = $packs ManagedPolicyNames = $script:ManagedPolicyNames } } function Test-SailPointRoleConfiguration { param( [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string[]]$ExpectedPrincipal, [Parameter(Mandatory)][string]$ExpectedExternalId, $AwsParams ) $role = Get-IAMRole @AwsParams -RoleName $Name $trust = ConvertFrom-IamPolicyDocument -Document $role.AssumeRolePolicyDocument $statement = @($trust.Statement | Where-Object { $_.Effect -eq 'Allow' } | Select-Object -First 1) if (-not $statement) { throw "Role $Name has no Allow statement in its trust policy." } $principal = $statement.Principal.AWS $actualPrincipals = @() if ($principal -is [System.Collections.IEnumerable] -and $principal -isnot [string]) { $actualPrincipals = @($principal) } elseif ($principal) { $actualPrincipals = @([string]$principal) } foreach ($expected in $ExpectedPrincipal) { if ($actualPrincipals -notcontains $expected) { Write-Warning "Trust policy on $Name does not include principal $expected." } } $externalId = $statement.Condition.StringEquals.'sts:ExternalId' if ($externalId -ne $ExpectedExternalId) { throw "Role $Name trust policy ExternalId mismatch. Expected '$ExpectedExternalId', got '$externalId'." } Write-Ok "Verified trust policy on $Name (External ID and principals)." } Export-ModuleMember -Function @( 'Initialize-AwsSaasConnectorData', 'Get-SelectedPolicyDocuments', 'Get-LocalPolicyByName', 'Set-CustomerManagedPolicy', 'Set-SailPointIamRole', 'Test-SailPointRoleConfiguration', 'Get-AwsSaasCatalog' ) |