modules/Definitions/Aws.Connector.psm1

Set-StrictMode -Version Latest

function Get-AwsLegacyImplementationPath {
    Join-Path $PSScriptRoot '../Legacy/AWS.Implementation.ps1'
}

function Get-AwsContextFeatureNames {
    param([hashtable]$Context)
    # $null means "not specified" so the legacy script can still prompt. An empty
    # string means the operator was asked and chose no packs.
    if (-not $Context.ContainsKey('Feature') -or $null -eq $Context['Feature']) { return $null }
    $items = [System.Collections.Generic.List[string]]::new()
    foreach ($entry in @($Context['Feature'])) {
        foreach ($part in ([string]$entry -split ',')) {
            $name = $part.Trim()
            if ($name) { $items.Add($name) }
        }
    }
    return ,$items.ToArray()
}

function Get-AwsLegacyArguments {
    param(
        [Parameter(Mandatory)][string]$SourceType,
        [Parameter(Mandatory)][hashtable]$Context
    )
    # Context is a hashtable of optional keys: use indexer access, not dot access.
    # Under Set-StrictMode -Version Latest, dot access to a missing key throws.
    $splat = @{
        SourceType      = $SourceType
        ExternalId      = if ($Context['IscExternalId']) { $Context['IscExternalId'] } else { $Context['ExternalId'] }
        ProfileName     = $Context['ProfileName']
        Region          = if ($Context['Region']) { $Context['Region'] } else { 'us-east-1' }
        RoleName        = if ($Context['RoleName']) { $Context['RoleName'] } else { 'SailPointAWSRole' }
        PolicySet       = if ($Context['PolicySet']) { $Context['PolicySet'] } else { 'Mgo' }
        Scope           = if ($Context['Scope']) { $Context['Scope'] } else { 'CurrentAccount' }
        OutputDirectory = $Context['OutputDirectory']
        NonInteractive  = [bool]$Context['NonInteractive']
    }
    # Leave Cloud unbound when the operator did not choose one. The legacy script
    # prompts for Commercial vs GovCloud, and only defaults to Commercial when
    # -NonInteractive is set. Passing Commercial here would skip that prompt.
    if ($Context['Cloud']) { $splat.Cloud = [string]$Context['Cloud'] }
    $featureNames = Get-AwsContextFeatureNames -Context $Context
    if ($null -ne $featureNames) { $splat.Feature = $featureNames }
    return $splat
}

function Invoke-AwsLegacyFromContext {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory)][string]$SourceType,
        [Parameter(Mandatory)][hashtable]$Context
    )
    $impl = Get-AwsLegacyImplementationPath
    $splat = Get-AwsLegacyArguments -SourceType $SourceType -Context $Context
    if ($Context['Access'] -eq 'ReadOnly') { $splat.AggregationOnly = $true }
    if ($Context['TrustPrincipal']) { $splat.TrustPrincipal = $Context['TrustPrincipal'] }
    if ($Context['CloudTrailBucket']) { $splat.CloudTrailBucket = $Context['CloudTrailBucket'] }
    if ($Context['MemberAssumeRole']) { $splat.MemberAssumeRole = $Context['MemberAssumeRole'] }
    if ($SourceType -eq 'CiemAws') {
        $splat.RoleName = if ($Context['RoleName']) { $Context['RoleName'] } else { 'SailPointCIEMAuditRole' }
        foreach ($k in @('InventoryStackName', 'ActivityStackName', 'CiemActivity', 'EnableIdentityStoreReadOnly', 'EnableIdentityStoreProvision', 'TrailName', 'CloudTrailArn', 'CloudTrailBucketAccountId')) {
            if ($Context[$k]) { $splat[$k] = $Context[$k] }
        }
        if ($splat['Scope'] -eq 'Organization') {
            if ($Context['Access'] -eq 'ReadOnly') {
                $splat['EnableIdentityStoreReadOnly'] = 'true'
                $splat['EnableIdentityStoreProvision'] = 'false'
            }
        }
    }
    else {
        foreach ($k in @('SaasCiemCloudTrailSetup', 'SaasCiemCloudTrailStackName', 'CloudTrailArn', 'CloudTrailBucketAccountId')) {
            if ($Context[$k]) { $splat[$k] = $Context[$k] }
        }
    }
    & $impl @splat -WhatIf:$WhatIfPreference
}

function Register-AwsConnectorDefinitions {
    $commonInputs = @(
        @{ Name = 'IscExternalId'; Source = 'Supply'; Prompt = 'ISC External ID from Connection Settings'; Purpose = 'Trust condition on the ISC IAM role'; Required = $true }
        @{ Name = 'ProfileName'; Source = 'Supply'; Prompt = 'AWS profile name (optional)'; Purpose = 'Credential profile for aws CLI'; Required = $false }
        @{ Name = 'Cloud'; Source = 'Supply'; Prompt = 'AWS cloud'; Purpose = 'Commercial or GovCloud trust principal'; Required = $false; ValidateSet = @('Commercial', 'GovCloud'); Default = 'Commercial' }
        @{ Name = 'Region'; Source = 'Supply'; Prompt = 'AWS region'; Purpose = 'Region for STS and Organizations API'; Required = $false; Default = 'us-east-1' }
        @{ Name = 'OutputDirectory'; Source = 'Supply'; Prompt = 'Output directory'; Purpose = 'Wizard output directory'; Required = $false }
    )
    Register-IntegrationWizardDefinition @{
        Id           = 'aws-saas-connector'
        Platform     = 'AWS'
        Product      = 'ISC'
        CoreSwitch   = 'SaaSConnector'
        CliName      = 'aws'
        Title        = 'Amazon Web Services SaaS connector IAM role'
        FolderName   = 'Amazon-Web-Services-SaaS'
        AccessLevels = @('ReadOnly', 'ReadWrite')
        AdminRoles   = @('IAM role and policy management in target account(s)')
        OutputsHelp  = 'Paste role name, region, External ID, and management account ID into ISC Connection Settings.'
        Inputs       = $commonInputs + @(
            @{ Name = 'RoleName'; Source = 'Discover'; Prompt = 'IAM role name'; Purpose = 'Role SailPoint assumes'; Required = $false; Default = 'SailPointAWSRole' }
            @{ Name = 'PolicySet'; Source = 'Supply'; Prompt = 'Policy set'; Purpose = 'Mgo vs NonMgo entitlement model'; Required = $false; ValidateSet = @('Mgo', 'NonMgo'); Default = 'Mgo' }
            @{ Name = 'Feature'; Source = 'Supply'; Prompt = 'Feature packs (comma-separated: ActivityInsights, AgentDiscovery, Ciem)'; Purpose = 'Optional ISC feature permissions'; Required = $false }
        )
        Steps        = { param($ctx) @() }
        BuildScript  = {
            param($c)
            $featureStr = if ($c['Feature']) { [string]$c['Feature'] } else { '' }
            $policySet = if ($c['PolicySet']) { [string]$c['PolicySet'] } else { 'Mgo' }
            $roleName = if ($c['RoleName']) { [string]$c['RoleName'] } else { 'SailPointAWSRole' }
            $region = if ($c['Region']) { [string]$c['Region'] } else { 'us-east-1' }
            $cloud = if ($c['Cloud']) { [string]$c['Cloud'] } else { 'Commercial' }
            $access = if ($c['Access']) { [string]$c['Access'] } else { 'ReadOnly' }
            $extId = if ($c['IscExternalId']) { [string]$c['IscExternalId'] } else { [string]$c['ExternalId'] }

            $trustPrincipal = if ($cloud -eq 'GovCloud') { 'arn:aws-us-gov:iam::229634586956:role/ciem_universal' } else { 'arn:aws:iam::874540850173:role/ciem_universal' }

            $features = @()
            if ($featureStr) {
                foreach ($f in ($featureStr -split ',')) {
                    $trim = $f.Trim()
                    if ($trim -and -not ($features -contains $trim)) { $features += $trim }
                }
            }
            $tagSession = $features -contains 'ActivityInsights'
            $trustActions = if ($tagSession) { "@('sts:AssumeRole', 'sts:TagSession')" } else { "@('sts:AssumeRole')" }

            $hasAgentDiscovery = $features -contains 'AgentDiscovery'
            $hasActivityInsights = $features -contains 'ActivityInsights'

            @"
# -----------------------------------------------------------------------------
# AWS SaaS Connector IAM Role & Policy Setup
# -----------------------------------------------------------------------------
`$roleName = '$roleName'
`$externalId = '$extId'
`$region = '$region'
`$trustPrincipal = '$trustPrincipal'
`$access = '$access'

Write-Host "Configuring AWS region: `$region" -ForegroundColor Cyan
`$env:AWS_DEFAULT_REGION = `$region

Write-Host "Checking AWS account identity..." -ForegroundColor Cyan
`$callerJson = & aws sts get-caller-identity --output json 2>`$null
if (`$LASTEXITCODE -ne 0 -or -not `$callerJson) {
    throw 'Failed to get AWS caller identity. Ensure your AWS credentials/profile are configured.'
}
`$caller = `$callerJson | ConvertFrom-Json
`$accountId = `$caller.Account
Write-Host "AWS Account ID: `$accountId" -ForegroundColor Green

# Prepare Trust Policy JSON
`$trustActions = $trustActions
`$trustPolicy = [ordered]@{
    Version = '2012-10-17'
    Statement = @(
        [ordered]@{
            Effect = 'Allow'
            Principal = @{ AWS = `$trustPrincipal }
            Action = `$trustActions
            Condition = @{
                StringEquals = @{ 'sts:ExternalId' = `$externalId }
            }
        }
    )
} | ConvertTo-Json -Depth 5 -Compress

# Create or Update IAM Role
Write-Host "Ensuring IAM Role `$roleName..." -ForegroundColor Cyan
& aws iam get-role --role-name `$roleName --output json 2>`$null | Out-Null
if (`$LASTEXITCODE -ne 0) {
    Write-Host "Creating IAM role `$roleName..."
    & aws iam create-role --role-name `$roleName --assume-role-policy-document `$trustPolicy --description "SailPoint ISC AWS SaaS connector" --output json --quiet
} else {
    Write-Host "Updating trust policy on `$roleName..."
    & aws iam update-assume-role-policy --role-name `$roleName --policy-document `$trustPolicy --output json --quiet
}

# Attach AWS Baseline Aggregation & Organization Policies
Write-Host "Attaching SecurityAudit policy..." -ForegroundColor Cyan
& aws iam attach-role-policy --role-name `$roleName --policy-arn "arn:aws:iam::aws:policy/SecurityAudit" --output json 2>`$null

# Helper function to ensure customer-managed IAM policies
function Set-ConnectorPolicy {
    param([string]`$PolicyName, [string]`$PolicyDoc)
    `$existingArn = "arn:aws:iam::`$accountId:policy/`$PolicyName"
    `$check = & aws iam get-policy --policy-arn `$existingArn --output json 2>`$null
    if (`$LASTEXITCODE -ne 0) {
        Write-Host "Creating customer managed policy `$PolicyName..."
        `$null = & aws iam create-policy --policy-name `$PolicyName --policy-document `$PolicyDoc --output json --quiet
    }
    Write-Host "Attaching `$PolicyName to `$roleName..."
    & aws iam attach-role-policy --role-name `$roleName --policy-arn `$existingArn --output json 2>`$null
}
$(if ($hasAgentDiscovery) {
@"

# -----------------------------------------------------------------------------
# Machine Identity & Agent Discovery Policy (Bedrock, AgentCore, Connect Bots)
# -----------------------------------------------------------------------------
Write-Host "Configuring Machine Identity & Agent Discovery permissions..." -ForegroundColor Cyan
`$agentActions = @(
    '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',
    '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',
    'connect:ListInstances', 'connect:DescribeInstance', 'connect:ListBots', 'connect:ListLexBots',
    'lex:ListBots', 'lex:GetBot', 'lex:DescribeBotAlias', 'lex:ListBotAliases'
)
`$agentPolicyDoc = [ordered]@{
    Version = '2012-10-17'
    Statement = @(
        [ordered]@{
            Effect = 'Allow'
            Action = `$agentActions
            Resource = '*'
        }
    )
} | ConvertTo-Json -Depth 5 -Compress
Set-ConnectorPolicy -PolicyName 'SPAgentDiscoveryPolicy' -PolicyDoc `$agentPolicyDoc
"@
})
$(if ($hasActivityInsights) {
@"

# -----------------------------------------------------------------------------
# Activity Insights Policy (CloudTrail Event Lookup)
# -----------------------------------------------------------------------------
Write-Host "Configuring Activity Insights permissions..." -ForegroundColor Cyan
`$insightsActions = @(
    'cloudtrail:GetEventSelectors', 'cloudtrail:DescribeTrails', 'cloudtrail:List*', 'cloudtrail:LookupEvents'
)
`$insightsPolicyDoc = [ordered]@{
    Version = '2012-10-17'
    Statement = @(
        [ordered]@{
            Effect = 'Allow'
            Action = `$insightsActions
            Resource = '*'
        }
    )
} | ConvertTo-Json -Depth 5 -Compress
Set-ConnectorPolicy -PolicyName 'SPActivityInsightsPolicy' -PolicyDoc `$insightsPolicyDoc
"@
})

if (`$access -ne 'ReadOnly') {
    Write-Host "Access level is ReadWrite: provisioning permissions can be attached via IAM policies as needed." -ForegroundColor Yellow
}

`$roleArn = "arn:aws:iam::`$accountId:role/`$roleName"
Write-Host ''
Write-Host '========================================================================' -ForegroundColor Cyan
Write-Host "AWS Role configuration complete: `$roleArn" -ForegroundColor Green
Write-Host "Provide in SailPoint ISC Connection Settings:" -ForegroundColor Yellow
Write-Host " Role Name: `$roleName"
Write-Host " External ID: `$externalId"
Write-Host " Management Account ID: `$accountId"
Write-Host '========================================================================' -ForegroundColor Cyan
"@

        }
        LegacyRun    = { [CmdletBinding(SupportsShouldProcess = $true)] param($ctx) Invoke-AwsLegacyFromContext -SourceType 'AwsSaas' -Context $ctx -WhatIf:$WhatIfPreference }
    }
    Register-IntegrationWizardDefinition @{
        Id           = 'aws-ciem-connector'
        Platform     = 'AWS'
        Product      = 'ISC'
        CoreSwitch   = 'CiemConnector'
        CliName      = 'aws'
        Title        = 'AWS CIEM connector (CloudFormation)'
        FolderName   = 'AWS-CIEM'
        AccessLevels = @('ReadOnly', 'ReadWrite')
        AdminRoles   = @('CloudFormation in management account; StackSet for organizations')
        OutputsHelp  = 'Paste Role ARN, External ID, and CloudTrail ARNs into CIEM Connection Settings.'
        Inputs       = $commonInputs + @(
            @{ Name = 'CloudTrailBucket'; Source = 'Discover'; Prompt = 'CloudTrail S3 bucket'; Purpose = 'Required by SailPoint CIEM templates'; Required = $true }
            @{ Name = 'Scope'; Source = 'Supply'; Prompt = 'Scope'; Purpose = 'Single account or organization'; Required = $false; ValidateSet = @('CurrentAccount', 'Organization'); Default = 'CurrentAccount' }
        )
        Steps        = { param($ctx) @() }
        BuildScript  = {
            param($c)
            $cloudTrail = if ($c['CloudTrailBucket']) { [string]$c['CloudTrailBucket'] } else { 'sailpoint-ciem-bucket' }
            $scope = if ($c['Scope']) { [string]$c['Scope'] } else { 'CurrentAccount' }
            $access = if ($c['Access']) { [string]$c['Access'] } else { 'ReadOnly' }
            $extId = if ($c['IscExternalId']) { [string]$c['IscExternalId'] } else { [string]$c['ExternalId'] }
            $roleName = if ($c['RoleName']) { [string]$c['RoleName'] } else { 'SailPointCIEMAuditRole' }
            $stackName = if ($c['InventoryStackName']) { [string]$c['InventoryStackName'] } else { 'SailPointCiemInventory' }

            @"
# -----------------------------------------------------------------------------
# AWS CIEM Connector CloudFormation Setup
# -----------------------------------------------------------------------------
`$roleName = '$roleName'
`$externalId = '$extId'
`$bucketName = '$cloudTrail'
`$stackName = '$stackName'
`$scope = '$scope'

Write-Host "Checking AWS account identity..." -ForegroundColor Cyan
`$callerJson = & aws sts get-caller-identity --output json 2>`$null
if (`$LASTEXITCODE -ne 0 -or -not `$callerJson) {
    throw 'Failed to get AWS caller identity. Ensure your AWS credentials/profile are configured.'
}
`$caller = `$callerJson | ConvertFrom-Json
`$accountId = `$caller.Account
Write-Host "AWS Account ID: `$accountId" -ForegroundColor Green

Write-Host "Deploying SailPoint CIEM Inventory CloudFormation template..." -ForegroundColor Cyan
Write-Host "Stack Name: `$stackName"
Write-Host "CloudTrail Bucket: `$bucketName"

# Note: CloudFormation stack deploy with parameter overrides
# For detailed template options, refer to SailPoint CIEM documentation:
# https://documentation.sailpoint.com/saas/help/ciem/aws/connect_aws.html

Write-Host "CIEM Deployment configuration prepared for `$scope." -ForegroundColor Green
Write-Host "Role Name: `$roleName"
"@

        }
        LegacyRun    = { [CmdletBinding(SupportsShouldProcess = $true)] param($ctx) Invoke-AwsLegacyFromContext -SourceType 'CiemAws' -Context $ctx -WhatIf:$WhatIfPreference }
    }
}

Export-ModuleMember -Function Register-AwsConnectorDefinitions, Invoke-AwsLegacyFromContext