modules/Legacy/AWS.Implementation.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Configures AWS for the SailPoint ISC Amazon Web Services SaaS connector or the CIEM AWS source.

.DESCRIPTION
    AwsSaas (default): creates or updates a cross-account IAM role trusted by SailPoint's ciem_universal
    principal, attaches documented aggregation / organization / provisioning policies, and optional feature packs.
    With -Feature Ciem it also prepares CloudTrail ARN(s) and the bucket account ID so you can enable
    Cloud Infrastructure Entitlement Management on the AWS SaaS source.

    CiemAws: downloads SailPoint's CloudFormation templates and creates or updates the documented stack or
    StackSet for CIEM inventory and optional activity collection (including new CloudTrail / S3 when selected).

    Run one source type per invocation. Each type writes its own connection-settings file under a separate
    default directory so a SaaS run cannot overwrite CIEM output.

    References:
    https://documentation.sailpoint.com/connectors/saas/aws/help/saas_connectivity/aws/introduction.html
    https://documentation.sailpoint.com/saas/help/ciem/index.html
    https://documentation.sailpoint.com/saas/help/ciem/aws/config/index.html
    https://documentation.sailpoint.com/saas/help/ciem/aws/config/config_aws_auto.html
    https://documentation.sailpoint.com/saas/help/ciem/aws/config/aws_permission_sets.html
    https://documentation.sailpoint.com/connectors/aws/help/integrating_aws/mgo_source_policies.html
    https://documentation.sailpoint.com/connectors/aws/help/integrating_aws/non_mgo_policies.html
    https://documentation.sailpoint.com/connectors/saas/aws/help/saas_connectivity/aws/activity_insights.html

.PARAMETER SourceType
    AwsSaas (default) - ISC Amazon Web Services SaaS connector (native IAM policies).
    CiemAws - SailPoint CIEM AWS source (SailPoint CloudFormation templates).

.PARAMETER ProfileName
    AWS credential profile (SSO or access keys). Uses the default credential chain when omitted.

.PARAMETER Region
    AWS region for STS and Organizations API calls. IAM itself is global. Default: us-east-1.

.PARAMETER Cloud
    Commercial (default) or GovCloud. Selects the default SailPoint trust principal(s).

.PARAMETER TrustPrincipal
    Replaces the default trust principal(s) written to the role. Accepts full role ARNs or bare
    12-digit account IDs (expanded to arn:<partition>:iam::<id>:role/ciem_universal).

    Commercial defaults trust both the documented CIEM account (874540850173) and the ISC SaaS
    runtime account (706944607044). GovCloud trusts 229634586956. If AssumeRole still fails,
    take the account ID from the assumed-role ARN in that error and pass it here (include the
    defaults you still need).

.PARAMETER RoleName
    IAM role name. Default: SailPointAWSRole (AwsSaas) or SailPointCIEMAuditRole (CiemAws).

.PARAMETER ExternalId
    External ID from the ISC source Connection Settings (AWS SaaS or CIEM AWS). Required.

.PARAMETER InventoryStackName
    CiemAws: CloudFormation stack or StackSet name for inventory collection. Default: SailPointCiemInventory.

.PARAMETER ActivityStackName
    CiemAws: CloudFormation stack name for activity collection. Default: SailPointCiemActivity.

.PARAMETER CiemActivity
    CiemAws activity template selection: None, OrganizationManagement (org management-account stack),
    ExistingCloudTrail, NewCloudTrailExistingBucket, or NewCloudTrailAndBucket (single account).

.PARAMETER EnableIdentityStoreReadOnly
    Identity Center read permissions (true/false). Organization scope only: org activity stack uses the
    template parameter; inventory-only org deploys attach SailPointCIEMAuditICReadOnlyPolicy. Ignored
    (and rejected if true) with Single Account / Account instance, which cannot enable Identity Center.

.PARAMETER EnableIdentityStoreProvision
    Identity Center provisioning permissions (true/false). Organization scope only. Leave unset unless
    the CIEM source has Provision Identity Center enabled. Mutually exclusive with Single Account.

.PARAMETER TrailName
    CiemAws: CloudTrail name when creating a new trail. Default: sailpoint-ciem-cloud-trail.

.PARAMETER CloudTrailArn
    Optional CloudTrail ARN(s) to include in connection settings (comma-separated). Used by CiemAws
    and by AwsSaas when -Feature Ciem (ISC Additional Settings: Enable Cloud Infrastructure
    Entitlement Management). Trails in this account are also discovered automatically.

.PARAMETER PolicySet
    Mgo - multiple group object policies (default; recommended).
    NonMgo - single entitlement type (IAM groups only).

.PARAMETER Feature
    Optional documented feature packs: ActivityInsights, Ciem, AgentDiscovery. ActivityInsights also
    adds sts:TagSession to the role trust policy, because SailPoint passes session tags when assuming
    the role for activity collection. Identity Center is not a SaaS feature: SailPoint governs
    Identity Center users and permission sets through a CIEM AWS source (-SourceType CiemAws).

.PARAMETER AggregationOnly
    Skip SPProvisioningPolicy.

.PARAMETER CloudTrailBucket
    CloudTrail log bucket. AwsSaas: optional unless -Feature Ciem, which requires it for
    SPCloudTrailBucketPolicy and CIEM settings. CiemAws: required by SailPoint templates.
    Default for CiemAws: sailpoint-ciem-<account-id> (globally unique, valid S3 DNS name).

.PARAMETER CloudTrailBucketAccountId
    12-digit AWS account that hosts the CloudTrail S3 bucket. AwsSaas -Feature Ciem: ISC
    "AWS CloudTrail Bucket Account ID". Defaults to the signed-in account.

.PARAMETER Scope
    CurrentAccount (default) or Organization. Organization lists member accounts and creates the same
    role in each when -MemberAssumeRole is set.

.PARAMETER MemberAssumeRole
    Existing role in member accounts that this identity can assume (for example OrganizationAccountAccessRole).
    Required for -Scope Organization beyond the management account.

.PARAMETER OutputDirectory
    Directory for the connection-settings file. Default: ./wizard-output/aws-isc (AwsSaas) or ./wizard-output/aws-ciem (CiemAws).

.PARAMETER NonInteractive
    Fail instead of prompting when required values are missing.

.EXAMPLE
    .\AWS.ps1

.EXAMPLE
    .\AWS.ps1 -ExternalId '11111111-2222-3333-4444-555555555555' -Feature ActivityInsights,Ciem `
        -CloudTrailBucket 'my-org-cloudtrail-logs' -NonInteractive

.EXAMPLE
    .\AWS.ps1 -SourceType CiemAws -ExternalId '11111111-2222-3333-4444-555555555555' -Scope Organization `
        -CloudTrailBucket 'my-org-cloudtrail-logs' -CiemActivity OrganizationManagement -NonInteractive

.NOTES
    AwsSaas: sign in with permission to create IAM roles and policies; org-wide SaaS also needs member assume role.
    CiemAws: sign in with CloudFormation and (for organizations) StackSet permissions in the management account.
    Published CIEM templates trust only the documented CIEM account (874540850173 commercial). After deployment the
    script rewrites the role trust to the same defaults as AwsSaas (CIEM plus the ISC SaaS runtime account
    706944607044), because some tenants assume the role from the latter. Use -TrustPrincipal to set a different list.
#>

[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param(
    [Parameter()]
    [ValidateSet('AwsSaas', 'CiemAws')]
    [string]$SourceType = 'AwsSaas',

    [Parameter()]
    [string]$ProfileName,

    [Parameter()]
    [string]$Region = 'us-east-1',

    [Parameter()]
    [ValidateSet('Commercial', 'GovCloud')]
    [string]$Cloud = 'Commercial',

    [Parameter()]
    [string[]]$TrustPrincipal,

    [Parameter()]
    [string]$RoleName = 'SailPointAWSRole',

    [Parameter()]
    [string]$ExternalId,

    [Parameter()]
    [ValidateSet('Mgo', 'NonMgo')]
    [string]$PolicySet = 'Mgo',

    [Parameter()]
    [ValidateSet('ActivityInsights', 'AgentDiscovery', 'Ciem')]
    [string[]]$Feature,

    [Parameter()]
    [switch]$AggregationOnly,

    [Parameter()]
    [string]$CloudTrailBucket,

    [Parameter()]
    [string]$CloudTrailBucketAccountId,

    [Parameter()]
    [ValidateSet('CurrentAccount', 'Organization')]
    [string]$Scope = 'CurrentAccount',

    [Parameter()]
    [string]$MemberAssumeRole,

    [Parameter()]
    [string]$InventoryStackName = 'SailPointCiemInventory',

    [Parameter()]
    [string]$ActivityStackName = 'SailPointCiemActivity',

    [Parameter()]
    [ValidateSet('None', 'OrganizationManagement', 'ExistingCloudTrail', 'NewCloudTrailExistingBucket', 'NewCloudTrailAndBucket')]
    [string]$CiemActivity = 'None',

    [Parameter()]
    [ValidateSet('true', 'false')]
    [string]$EnableIdentityStoreReadOnly = 'true',

    [Parameter()]
    [ValidateSet('true', 'false')]
    [string]$EnableIdentityStoreProvision = 'true',

    [Parameter()]
    [string]$TrailName = 'sailpoint-ciem-cloud-trail',

    [Parameter()]
    [ValidateSet('Discover', 'CreateNew', 'EnterName')]
    [string]$SaasCiemCloudTrailSetup,

    [Parameter()]
    [string]$SaasCiemCloudTrailStackName = 'SailPointSaasCiemCloudTrail',

    [Parameter()]
    [string[]]$CloudTrailArn,

    [Parameter()]
    [string]$OutputDirectory,

    [Parameter()]
    [switch]$NonInteractive
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$script:ModuleRoot = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'modules'
Import-Module (Join-Path $script:ModuleRoot 'ISC.OperatorConsole.psm1') -Force -WarningAction SilentlyContinue
Import-IscModule -Path (Join-Path $script:ModuleRoot 'ISC.OperatorToolchain.psm1') -Force
Initialize-OperatorConsole -NonInteractive:$NonInteractive
Import-IscModule -Path (Join-Path $script:ModuleRoot 'ISC.AwsConnector.psm1') -Force
Import-IscModule -Path (Join-Path $script:ModuleRoot 'ISC.AwsCiemConnector.psm1') -Force
Import-IscModule -Path (Join-Path $script:ModuleRoot 'ISC.AwsSaasConnector.psm1') -Force
Import-IscModule -Path (Join-Path $script:ModuleRoot 'ISC.AwsSourceSetup.psm1') -Force
Initialize-AwsSourceSetup -ModuleRoot $script:ModuleRoot -NonInteractive:$NonInteractive


function Write-Banner {
    param(
        [ValidateSet('AwsSaas', 'CiemAws')]
        [string]$SourceType = 'AwsSaas'
    )

    Write-Host ''
    if ($SourceType -eq 'CiemAws') {
        Write-Host ' SailPoint ISC - CIEM AWS source connection setup' -ForegroundColor Cyan
        Write-Host ' Deploys SailPoint CloudFormation templates for CIEM inventory and activity.' -ForegroundColor DarkCyan
    }
    else {
        Write-Host ' SailPoint ISC - Amazon Web Services SaaS source connection setup' -ForegroundColor Cyan
        Write-Host ' Creates or updates the IAM role used by the AWS SaaS connector.' -ForegroundColor DarkCyan
    }
    Write-Host ''
}

# SailPoint templates that read an existing trail (ExistingCloudTrail, OrganizationManagement) create
# no trail and output no ARN, so the bucket and the ARNs both have to come from what already exists.
function Read-DiscoveredCloudTrailBucket {
    param(
        [Parameter(Mandatory)][string]$HomeRegion,
        [string]$PreferredBucket,
        [string]$Prompt = 'Select CloudTrail log bucket:'
    )

    Write-Step 'CloudTrail discovery'
    Write-Info 'Looking up existing CloudTrail trails and log buckets in this account...'
    $bucketOptions = @(Get-CloudTrailLogBucketOptions -HomeRegion $HomeRegion -ScanScope HomeRegion)
    if ($bucketOptions.Count -eq 0) {
        Write-Info 'No trails in the home region. Scanning all AWS regions...'
        $bucketOptions = @(Get-CloudTrailLogBucketOptions -HomeRegion $HomeRegion -ScanScope AllRegions)
    }

    $selected = Select-CloudTrailLogBucketOption -Options $bucketOptions -PreferredBucket $PreferredBucket
    if (-not $selected -and $bucketOptions.Count -gt 0) {
        $bucketNames = @($bucketOptions | ForEach-Object { $_.BucketName })
        $bucketLabels = @($bucketOptions | ForEach-Object {
            $trailCount = @($_.TrailArns).Count
            $trailNames = @($_.TrailArns | ForEach-Object { ($_ -split '/')[-1] }) -join ', '
            "$($_.BucketName) ($trailCount trail$(if ($trailCount -eq 1) { '' } else { 's' }): $trailNames)"
        })
        $pickedBucket = Read-Choice -Prompt $Prompt -Options $bucketNames -Labels $bucketLabels
        $selected = Select-CloudTrailLogBucketOption -Options $bucketOptions -PreferredBucket $pickedBucket
    }
    return $selected
}

# NewCloudTrailExistingBucket creates the trail, so the bucket only has to exist - it usually has no
# trail yet and CloudTrail discovery would never surface it.
function Read-ExistingS3Bucket {
    param(
        [Parameter(Mandatory)][string]$HomeRegion,
        [string]$PreferredBucket
    )

    Write-Step 'S3 bucket discovery'
    Write-Info 'Listing S3 buckets in this account for the new CloudTrail...'
    $bucketNames = @(Get-AwsS3BucketNames -RegionName $HomeRegion)
    if ($bucketNames.Count -eq 0) { return $null }

    $default = if ($PreferredBucket -and $bucketNames -contains $PreferredBucket) { $PreferredBucket } else { $bucketNames[0] }
    return Read-Choice -Prompt 'Existing S3 bucket for the new CloudTrail:' -Options $bucketNames -Default $default
}

# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------

try {
    Ensure-AwsToolsModules -ExtraModules @(
        'AWS.Tools.CloudFormation'
        'AWS.Tools.CloudTrail'
        'AWS.Tools.EC2'
        'AWS.Tools.S3'
    )

    $askSourceType = -not $PSBoundParameters.ContainsKey('SourceType') -and -not $NonInteractive
    if ($askSourceType -and -not $NonInteractive) {
        $SourceType = Read-Choice -Prompt 'ISC source type:' -Options @('AwsSaas', 'CiemAws') -Labels @(
            'Amazon Web Services SaaS connector (native IAM role and SP* policies)'
            'CIEM AWS source (SailPoint CloudFormation templates)'
        ) -Default 'AwsSaas'
    }

    Write-Banner -SourceType $SourceType
    if ($SourceType -eq 'CiemAws') {
        Ensure-AwsToolsModules -ExtraModules @(
            'AWS.Tools.CloudFormation'
            'AWS.Tools.CloudTrail'
            'AWS.Tools.EC2'
        )
    }

    if ($SourceType -eq 'CiemAws' -and $RoleName -eq (Get-DefaultSaasRoleName)) {
        $RoleName = Get-DefaultCiemRoleName
    }
    elseif ($SourceType -eq 'AwsSaas' -and $RoleName -eq (Get-DefaultCiemRoleName)) {
        $RoleName = Get-DefaultSaasRoleName
    }

    # The two source types share this script but not their parameters, so say which ones this run drops
    # rather than letting them look applied.
    $otherTypeParameters = if ($SourceType -eq 'CiemAws') {
        @('PolicySet', 'Feature', 'AggregationOnly', 'MemberAssumeRole', 'SaasCiemCloudTrailSetup', 'SaasCiemCloudTrailStackName')
    }
    else {
        @('CiemActivity', 'InventoryStackName', 'ActivityStackName', 'EnableIdentityStoreReadOnly', 'EnableIdentityStoreProvision')
    }
    $ignoredParameters = @($otherTypeParameters | Where-Object { $PSBoundParameters.ContainsKey($_) })
    if ($ignoredParameters.Count -gt 0) {
        $otherType = if ($SourceType -eq 'CiemAws') { 'AwsSaas' } else { 'CiemAws' }
        Write-Warning "Ignoring $($ignoredParameters -join ', '): those parameters belong to -SourceType $otherType."
    }

    $session = $null
    $connectKey = $null
    $existingRole = $null
    $isUpdate = $false

    # Whether each value still has to be asked for is decided once, from what the caller passed.
    # Testing the variables instead would stop a step from asking again when Esc returns to it.
    $askProfileName = -not $ProfileName -and -not $NonInteractive
    $askCloud = -not $PSBoundParameters.ContainsKey('Cloud') -and -not $NonInteractive
    $askRegion = -not $PSBoundParameters.ContainsKey('Region') -and -not $NonInteractive
    $askTrustPrincipal = -not $TrustPrincipal -and -not $NonInteractive
    $askRoleName = -not $PSBoundParameters.ContainsKey('RoleName')
    $askExternalId = -not $ExternalId
    $askPolicySet = $SourceType -eq 'AwsSaas' -and -not $PSBoundParameters.ContainsKey('PolicySet') -and -not $NonInteractive
    $askFeature = $SourceType -eq 'AwsSaas' -and -not $PSBoundParameters.ContainsKey('Feature')
    $askAggregationOnly = $SourceType -eq 'AwsSaas' -and -not $PSBoundParameters.ContainsKey('AggregationOnly') -and -not $NonInteractive
    $askCloudTrailBucket = -not $PSBoundParameters.ContainsKey('CloudTrailBucket') -and -not $NonInteractive
    $askSaasCiemCloudTrailSetup = $SourceType -eq 'AwsSaas' -and -not $PSBoundParameters.ContainsKey('SaasCiemCloudTrailSetup') -and -not $NonInteractive
    $askSaasCiemCloudTrailStack = -not $PSBoundParameters.ContainsKey('SaasCiemCloudTrailStackName') -and -not $NonInteractive
    $askSaasCiemTrailName = $SourceType -eq 'AwsSaas' -and -not $PSBoundParameters.ContainsKey('TrailName') -and -not $NonInteractive
    $askCloudTrailBucketAccountId = -not $PSBoundParameters.ContainsKey('CloudTrailBucketAccountId') -and -not $NonInteractive
    $askCloudTrailArn = -not $PSBoundParameters.ContainsKey('CloudTrailArn') -and -not $NonInteractive
    $askScope = -not $PSBoundParameters.ContainsKey('Scope') -and -not $NonInteractive
    $askMemberAssumeRole = $SourceType -eq 'AwsSaas' -and -not $MemberAssumeRole -and -not $NonInteractive
    $askCiemActivity = $SourceType -eq 'CiemAws' -and -not $PSBoundParameters.ContainsKey('CiemActivity') -and -not $NonInteractive
    $askEnableIcRead = $SourceType -eq 'CiemAws' -and -not $PSBoundParameters.ContainsKey('EnableIdentityStoreReadOnly') -and -not $NonInteractive
    $askEnableIcProv = $SourceType -eq 'CiemAws' -and -not $PSBoundParameters.ContainsKey('EnableIdentityStoreProvision') -and -not $NonInteractive
    $askTrailName = $SourceType -eq 'CiemAws' -and -not $PSBoundParameters.ContainsKey('TrailName') -and -not $NonInteractive
    $askInventoryStack = $SourceType -eq 'CiemAws' -and -not $PSBoundParameters.ContainsKey('InventoryStackName') -and -not $NonInteractive
    $askActivityStack = $SourceType -eq 'CiemAws' -and -not $PSBoundParameters.ContainsKey('ActivityStackName') -and -not $NonInteractive

    if (-not $askRegion -and $Cloud -eq 'GovCloud' -and $Region -eq 'us-east-1') {
        $Region = 'us-gov-west-1'
    }

    # The Identity Center answers must survive Esc navigation, so adjust their defaults only once.
    $icDefaultsInitialized = $false
    # Organization activity-stack prompt defaults to Yes once; later Esc passes keep the last answer.
    $orgActivityDefaultInitialized = $false
    # Attach Identity Center policies only when the operator stated an intent, by parameter or by prompt.
    $identityCenterRequested = $askEnableIcRead -or $askEnableIcProv -or
        $PSBoundParameters.ContainsKey('EnableIdentityStoreReadOnly') -or
        $PSBoundParameters.ContainsKey('EnableIdentityStoreProvision')

    $saasCiem = $false
    $wizardComplete = $false
    while (-not $wizardComplete) {
        Start-WizardPass
        try {
            if (Enter-WizardPrompt) {
                if ($askExternalId) {
                    $externalIdPrompt = if ($SourceType -eq 'CiemAws') {
                        'External ID from the CIEM AWS source Connection Settings'
                    }
                    else {
                        'External ID from the ISC AWS SaaS source Connection Settings'
                    }
                    $ExternalId = Read-InputString -Prompt $externalIdPrompt -Default $ExternalId -Required
                }
                Complete-WizardPrompt
            }

            if (Enter-WizardPrompt) {
                if ($askProfileName) {
                    $ProfileName = Read-InputString -Prompt 'AWS profile name (blank = default credential chain)' -Default $ProfileName
                }
                Complete-WizardPrompt
            }

            if (Enter-WizardPrompt) {
                if ($askCloud) {
                    $Cloud = Read-Choice -Prompt 'AWS cloud:' -Options @('Commercial', 'GovCloud') -Labels @(
                        'Commercial - trust CIEM (874540850173) and ISC SaaS (706944607044)'
                        'GovCloud - trust arn:aws-us-gov:iam::229634586956:role/ciem_universal'
                    ) -Default $(if ($Cloud) { $Cloud } else { 'Commercial' })
                }
                Complete-WizardPrompt
            }

            if (Enter-WizardPrompt) {
                if ($askRegion) {
                    $defaultRegion = if ($Cloud -eq 'GovCloud') { 'us-gov-west-1' } else { 'us-east-1' }
                    $Region = Read-InputString -Prompt 'AWS region (for STS / Organizations)' -Default $(if ($Region) { $Region } else { $defaultRegion }) -Required
                }
                Complete-WizardPrompt
            }

            $nextConnectKey = "$ProfileName|$Region|$Cloud"
            if (-not $session -or $nextConnectKey -ne $connectKey) {
                $session = Connect-AwsSession -RequestedProfile $ProfileName -RequestedRegion $Region
                $connectKey = $nextConnectKey
            }
            $partition = (Get-SailPointTrustConfig -Cloud $Cloud).Partition
            $defaultPrincipals = @((Get-SailPointTrustConfig -Cloud $Cloud).Principals)

            if (Enter-WizardPrompt) {
                if ($askTrustPrincipal) {
                    if ($SourceType -eq 'CiemAws') {
                        Write-Info "CIEM templates trust only arn:aws:iam::874540850173:role/ciem_universal. After deployment the role is set to trust: $($defaultPrincipals -join ', ')"
                        Write-Info 'If Test Connection still reports "Failed to get account authorization details", enter the account ID from the AssumeRole AccessDenied entry in CloudTrail.'
                    }
                    else {
                        Write-Info "Default $Cloud trust: $($defaultPrincipals -join ', ')"
                        Write-Info 'CIEM is the documented commercial principal; ISC SaaS is the connector runtime some tenants assume from. If AssumeRole still fails, enter the account ID from that error.'
                    }
                    $extraPrincipal = Read-InputString -Prompt 'Additional SailPoint account ID or role ARN to trust (blank = template / defaults only)'
                    if ($extraPrincipal) {
                        $TrustPrincipal = @($defaultPrincipals + $extraPrincipal)
                    }
                    else {
                        $TrustPrincipal = $null
                    }
                }
                Complete-WizardPrompt
            }

            $principals = Resolve-TrustPrincipal -Value $(if ($TrustPrincipal) { $TrustPrincipal } else { $defaultPrincipals }) -Partition $partition

            $awsParams = Get-IamCmdletParams -RegionName $Region

            Write-Step 'Checking AWS Organizations'
            $orgContext = Get-AwsOrganizationContext -CurrentAccountId $session.Account -AwsParams $awsParams
            $managementAccountId = $session.Account
            if ($orgContext.Available -and $orgContext.MasterAccountId) {
                $managementAccountId = $orgContext.MasterAccountId
                if ($orgContext.IsManagement) {
                    Write-Ok "This is the organization management account ($managementAccountId)"
                    if ($orgContext.AccountIds.Count -gt 0) {
                        Write-Ok "organizations:ListAccounts returned $($orgContext.AccountIds.Count) ACTIVE account(s)"
                    }
                }
                else {
                    if ($SourceType -eq 'AwsSaas') {
                        Write-Warning "This account $($session.Account) is a member. ISC Management Account ID and Cloud Scope require the management account $managementAccountId."
                        Write-Warning 'Create SailPointAWSRole in the management account. organizations:ListAccounts fails in member accounts, so the AWS Accounts dropdown stays empty.'
                    }
                    else {
                        Write-Warning "CIEM organization setup should run from the management account ($managementAccountId). You are signed in to member account $($session.Account)."
                    }
                }
                if ($orgContext.Error) {
                    Write-Warning "Could not list organization accounts: $($orgContext.Error)"
                }
            }
            elseif ($orgContext.NotInUse) {
                if ($SourceType -eq 'AwsSaas') {
                    Write-Warning "Account $($session.Account) is not part of an AWS Organization (AWSOrganizationsNotInUseException)."
                    Write-Warning 'ISC fills AWS Account Settings by calling organizations:ListAccounts, so that list stays empty and aggregation fails with "AWS Account IDs must be configured". SailPoint does not support single-account AWS SaaS sources.'
                    Write-Info 'Fix: enable an organization in this account (aws organizations create-organization --feature-set ALL), which makes it the management account, then reopen AWS Account Settings.'
                    Write-Info 'The role and policies below are still created correctly; only Cloud Scope is blocked.'
                }
                else {
                    Write-Info 'No AWS Organization detected. CIEM single-account mode is supported; enable Single Account in the CIEM AWS source.'
                }
            }
            else {
                if ($SourceType -eq 'AwsSaas') {
                    Write-Warning "AWS Organizations is not readable from this account: $($orgContext.Error)"
                    Write-Warning 'The ISC AWS Accounts (Cloud Scope) dropdown calls organizations:ListAccounts after assuming this role. Without that permission on the management-account role, the dropdown fails with Failed to get config options for key: cloudScope.'
                }
                else {
                    Write-Warning "AWS Organizations is not readable from this account: $($orgContext.Error)"
                }
            }

            if (Enter-WizardPrompt) {
                if ($askRoleName) {
                    $defaultRole = if ($SourceType -eq 'CiemAws') { Get-DefaultCiemRoleName } else { Get-DefaultSaasRoleName }
                    $RoleName = Read-InputString -Prompt 'IAM role name' -Default $(if ($RoleName) { $RoleName } else { $defaultRole }) -Required
                }
                Complete-WizardPrompt
            }

            $isUpdate = $false
            if ($SourceType -eq 'AwsSaas') {
                $existingRole = $null
                try { $existingRole = Get-IAMRole @awsParams -RoleName $RoleName } catch { }
                $isUpdate = [bool]$existingRole
                if (Enter-WizardPrompt) {
                    if ($isUpdate) {
                        Write-Step "A role named '$RoleName' already exists in account $($session.Account)"
                        Write-Info $existingRole.Arn
                        if (-not (Read-YesNo -Prompt 'Update it instead of stopping?' -Default $true)) {
                            throw 'Cancelled. Choose a different -RoleName to create a new role.'
                        }
                    }
                    Complete-WizardPrompt
                }

                if (Enter-WizardPrompt) {
                    if ($askPolicySet) {
                        $PolicySet = Read-Choice -Prompt 'Policy set:' -Options @('Mgo', 'NonMgo') -Labels @(
                            'Multiple group objects - groups, managed/inline policies, roles, OUs, SCPs, accounts (recommended)'
                            'Non-MGO - IAM groups as the only entitlement type'
                        ) -Default $(if ($PolicySet) { $PolicySet } else { 'Mgo' })
                    }
                    Complete-WizardPrompt
                }

                if (Enter-WizardPrompt) {
                    if ($askFeature) {
                        $packNames = @((Get-AwsSaasCatalog).FeaturePacks.Keys)
                        # Suffix the -Feature value so the prompt and the plan screen name packs the same way.
                        $packLabels = @($packNames | ForEach-Object { '{0} ({1})' -f (Get-AwsSaasCatalog).FeaturePacks[$_].Label, $_ })
                        Write-Step 'Permissions'
                        Write-Info 'SPAggregationPolicy and SPOrganizationPolicy are always attached. Provisioning is included unless you choose aggregation only.'
                        $Feature = @(Read-MultiChoice -Prompt 'Optional feature permissions to add:' -Options $packNames -Labels $packLabels)
                    }
                    Complete-WizardPrompt
                }

                if (Enter-WizardPrompt) {
                    if ($askAggregationOnly) {
                        $AggregationOnly = -not (Read-YesNo -Prompt 'Include provisioning permissions (create/update/delete IAM users and entitlements)?' -Default $(-not $AggregationOnly))
                    }
                    Complete-WizardPrompt
                }
            }

            if (Enter-WizardPrompt) {
                if ($askScope) {
                    $scopeLabels = if ($SourceType -eq 'CiemAws') {
                        @(
                            "Single AWS account ($($session.Account)) - enable Single Account in CIEM source"
                            'All AWS Organization accounts (recommended) - inventory StackSet plus optional management activity stack'
                        )
                    }
                    else {
                        @(
                            "This account only ($($session.Account))"
                            'This account plus every ACTIVE account in the AWS Organization'
                        )
                    }
                    $Scope = Read-Choice -Prompt 'Deployment scope:' -Options @('CurrentAccount', 'Organization') -Labels $scopeLabels `
                        -Default $(if ($Scope) { $Scope } else { 'CurrentAccount' })
                }
                Complete-WizardPrompt
            }

            if ($SourceType -eq 'CiemAws') {
                if (Enter-WizardPrompt) {
                    if ($askCiemActivity) {
                        if ($Scope -eq 'Organization') {
                            if (-not $orgActivityDefaultInitialized) {
                                $orgActivityDefaultInitialized = $true
                                if (-not $PSBoundParameters.ContainsKey('CiemActivity')) {
                                    $CiemActivity = 'OrganizationManagement'
                                }
                            }
                            $includeActivity = Read-YesNo -Prompt 'Deploy management-account activity stack (CloudTrail bucket read, Identity Center)?' -Default ($CiemActivity -ne 'None')
                            $CiemActivity = if ($includeActivity) { 'OrganizationManagement' } else { 'None' }
                        }
                        else {
                            $CiemActivity = Read-Choice -Prompt 'CIEM collection mode (single account):' -Options @(
                                'None', 'ExistingCloudTrail', 'NewCloudTrailExistingBucket', 'NewCloudTrailAndBucket'
                            ) -Labels @(
                                'Inventory only (SailPoint inventory template)'
                                'Activity - existing CloudTrail and S3 bucket'
                                'Activity - new CloudTrail with existing S3 bucket'
                                'Activity - new CloudTrail and new S3 bucket'
                            ) -Default $(if ($CiemActivity) { $CiemActivity } else { 'None' })
                        }
                    }
                    Complete-WizardPrompt
                }

                # Identity Center is Organization-only. Account instance (Single Account) cannot enable it.
                $icViaTemplate = ($Scope -eq 'Organization' -and $CiemActivity -eq 'OrganizationManagement')
                if ($Scope -eq 'CurrentAccount') {
                    $boundIcOn = (
                        ($PSBoundParameters.ContainsKey('EnableIdentityStoreReadOnly') -and $EnableIdentityStoreReadOnly -eq 'true') -or
                        ($PSBoundParameters.ContainsKey('EnableIdentityStoreProvision') -and $EnableIdentityStoreProvision -eq 'true')
                    )
                    if ($boundIcOn) {
                        throw 'CIEM Single Account (Account instance) cannot enable Identity Center. Use -Scope Organization, or omit -EnableIdentityStoreReadOnly / -EnableIdentityStoreProvision.'
                    }
                    $EnableIdentityStoreReadOnly = 'false'
                    $EnableIdentityStoreProvision = 'false'
                    if (-not $icDefaultsInitialized) { $icDefaultsInitialized = $true }
                }
                elseif (-not $icDefaultsInitialized) {
                    $icDefaultsInitialized = $true
                    if (-not $icViaTemplate) {
                        if ($askEnableIcRead) { $EnableIdentityStoreReadOnly = 'false' }
                        if ($askEnableIcProv) { $EnableIdentityStoreProvision = 'false' }
                    }
                }

                if ($Scope -eq 'Organization') {
                    # The source has one Provision Identity Center toggle, so ask once and set both
                    # template parameters. Provisioning calls the read APIs, so they always pair.
                    if (Enter-WizardPrompt) {
                        if ($askEnableIcRead -and $askEnableIcProv) {
                            $icPrompt = if ($icViaTemplate) {
                                'Enable Identity Center in the activity stack (source toggle: Provision Identity Center)?'
                            }
                            else {
                                'Grant Identity Center permissions (sso / identitystore) to the role (source toggle: Provision Identity Center)?'
                            }
                            $icAnswer = Read-Choice -Prompt $icPrompt -Options @('true', 'false') -Labels @('Yes', 'No') `
                                -Default $(if ($EnableIdentityStoreProvision -eq 'true') { 'true' } else { 'false' })
                            $EnableIdentityStoreReadOnly = $icAnswer
                            $EnableIdentityStoreProvision = $icAnswer
                        }
                        Complete-WizardPrompt
                    }

                    if ($EnableIdentityStoreProvision -eq 'true' -and $EnableIdentityStoreReadOnly -ne 'true') {
                        $EnableIdentityStoreReadOnly = 'true'
                        Write-Info 'Identity Center read permissions enabled as well; provisioning depends on them.'
                    }
                }

                if (Enter-WizardPrompt) {
                    if ($askTrailName -and $CiemActivity -in @('NewCloudTrailExistingBucket', 'NewCloudTrailAndBucket')) {
                        $TrailName = Read-InputString -Prompt 'New CloudTrail name' -Default $TrailName -Required
                    }
                    Complete-WizardPrompt
                }

                if (Enter-WizardPrompt) {
                    if ($askInventoryStack) {
                        $InventoryStackName = Read-InputString -Prompt 'Inventory stack or StackSet name' -Default $InventoryStackName -Required
                    }
                    Complete-WizardPrompt
                }

                if (Enter-WizardPrompt) {
                    if ($askActivityStack -and $CiemActivity -ne 'None') {
                        $ActivityStackName = Read-InputString -Prompt 'Activity stack name' -Default $ActivityStackName -Required
                    }
                    Complete-WizardPrompt
                }
            }

            $saasCiem = $SourceType -eq 'AwsSaas' -and (Test-EmbeddedCiemSelected -FeatureNames $Feature)

            if (Enter-WizardPrompt) {
                if ($askCloudTrailBucket -or ($askSaasCiemCloudTrailSetup -and $saasCiem)) {
                    $bucketValidate = { param($v) Test-AwsS3BucketName $v }
                    if ($SourceType -eq 'CiemAws') {
                        # Each template needs a different bucket: one that an existing trail already
                        # logs into, one that merely exists, or one the stack is about to create.
                        $readsExistingTrail = $CiemActivity -in @('ExistingCloudTrail', 'OrganizationManagement')
                        $needsExistingBucket = $CiemActivity -eq 'NewCloudTrailExistingBucket'

                        if ($askCloudTrailBucket -and $readsExistingTrail) {
                            $discovered = Read-DiscoveredCloudTrailBucket -HomeRegion $Region -PreferredBucket $CloudTrailBucket
                            if ($discovered) {
                                $CloudTrailBucket = $discovered.BucketName
                                $CloudTrailArn = @($discovered.TrailArns)
                                Write-Ok "Using bucket $CloudTrailBucket with $(@($discovered.TrailArns).Count) trail ARN(s)"
                            }
                            else {
                                Write-Info 'No CloudTrail trails were found in this account. Enter the bucket the existing trail logs into.'
                                $CloudTrailBucket = Read-InputString -Prompt 'Existing CloudTrail S3 bucket name' `
                                    -Default $CloudTrailBucket -Required -Validate $bucketValidate
                            }
                        }
                        elseif ($askCloudTrailBucket -and $needsExistingBucket) {
                            $pickedBucket = Read-ExistingS3Bucket -HomeRegion $Region -PreferredBucket $CloudTrailBucket
                            if ($pickedBucket) {
                                $CloudTrailBucket = $pickedBucket
                                Write-Ok "Trail $TrailName will be created in existing bucket $CloudTrailBucket"
                            }
                            else {
                                Write-Info 'No S3 buckets were found in this account. Enter the bucket name.'
                                $CloudTrailBucket = Read-InputString -Prompt 'Existing S3 bucket name for the new CloudTrail' `
                                    -Default $CloudTrailBucket -Required -Validate $bucketValidate
                            }
                        }
                        elseif ($askCloudTrailBucket) {
                            if ([string]::IsNullOrWhiteSpace($CloudTrailBucket) -and $session.Account) {
                                $CloudTrailBucket = Get-DefaultCiemCloudTrailBucketName -AccountId $session.Account
                            }
                            $bucketPrompt = 'CloudTrail S3 bucket name (required by SailPoint CIEM templates)'
                            $CloudTrailBucket = Read-InputString -Prompt $bucketPrompt -Default $CloudTrailBucket -Required -Validate $bucketValidate
                        }
                    }
                    elseif ($saasCiem) {
                        Write-Step 'CIEM CloudTrail'
                        Write-Info 'CIEM on the AWS SaaS source needs CloudTrail logs in an S3 bucket and the trail ARN(s).'

                        if ($askSaasCiemCloudTrailSetup) {
                            $SaasCiemCloudTrailSetup = Read-Choice -Prompt 'CloudTrail log bucket for CIEM:' -Options @(
                                'CreateNew', 'Discover', 'EnterName'
                            ) -Labels @(
                                'Create a new CloudTrail and S3 bucket (SailPoint CloudFormation) (recommended)'
                                'Use an existing bucket from CloudTrail in this account'
                                'Enter a bucket name manually'
                            ) -Default 'CreateNew'
                        }
                        elseif ([string]::IsNullOrWhiteSpace($SaasCiemCloudTrailSetup)) {
                            $SaasCiemCloudTrailSetup = 'EnterName'
                        }

                        switch ($SaasCiemCloudTrailSetup) {
                            'Discover' {
                                Ensure-AwsToolsModules -ExtraModules @('AWS.Tools.CloudTrail', 'AWS.Tools.EC2')
                                $bucketOptions = @(Get-CloudTrailLogBucketOptions -HomeRegion $Region -ScanScope HomeRegion)
                                if ($bucketOptions.Count -eq 0) {
                                    $scanAll = Read-YesNo -Prompt 'No trails in the home region. Scan all AWS regions? (can take a few minutes)' -Default $false
                                    if ($scanAll) {
                                        $bucketOptions = @(Get-CloudTrailLogBucketOptions -HomeRegion $Region -ScanScope AllRegions)
                                    }
                                }
                                if ($bucketOptions.Count -eq 0) {
                                    Write-Info 'No CloudTrail trails were found. Switching to create a new CloudTrail and bucket.'
                                    $SaasCiemCloudTrailSetup = 'CreateNew'
                                }
                                else {
                                    $bucketNames = @($bucketOptions | ForEach-Object { $_.BucketName })
                                    $bucketLabels = @($bucketOptions | ForEach-Object {
                                        $trailCount = @($_.TrailArns).Count
                                        "$($_.BucketName) ($trailCount trail$(if ($trailCount -eq 1) { '' } else { 's' }))"
                                    })
                                    $pickedBucket = Read-Choice -Prompt 'Select CloudTrail log bucket:' -Options $bucketNames -Labels $bucketLabels
                                    $selected = @($bucketOptions | Where-Object { $_.BucketName -eq $pickedBucket } | Select-Object -First 1)
                                    $CloudTrailBucket = $pickedBucket
                                    $CloudTrailArn = @($selected.TrailArns)
                                    Write-Ok "Using bucket $CloudTrailBucket with $($CloudTrailArn.Count) trail ARN(s)"
                                }
                            }
                        }

                        if ($SaasCiemCloudTrailSetup -eq 'CreateNew') {
                            if ([string]::IsNullOrWhiteSpace($CloudTrailBucket) -and $session.Account) {
                                $CloudTrailBucket = Get-DefaultCiemCloudTrailBucketName -AccountId $session.Account
                            }
                            if ($askCloudTrailBucket) {
                                $CloudTrailBucket = Read-InputString -Prompt 'New S3 bucket name for CloudTrail logs' `
                                    -Default $CloudTrailBucket -Required -Validate $bucketValidate
                            }
                            elseif ([string]::IsNullOrWhiteSpace($CloudTrailBucket)) {
                                throw 'AwsSaas -SaasCiemCloudTrailSetup CreateNew requires -CloudTrailBucket (or run interactively).'
                            }
                            if ($askSaasCiemTrailName) {
                                $TrailName = Read-InputString -Prompt 'New CloudTrail name' -Default $TrailName -Required
                            }
                            if ($askSaasCiemCloudTrailStack) {
                                $SaasCiemCloudTrailStackName = Read-InputString -Prompt 'CloudFormation stack name for CloudTrail resources' `
                                    -Default $SaasCiemCloudTrailStackName -Required
                            }
                            Write-Info "At apply, stack $SaasCiemCloudTrailStackName will create bucket $CloudTrailBucket and trail $TrailName."
                        }
                        elseif ($SaasCiemCloudTrailSetup -eq 'EnterName') {
                            if ($askCloudTrailBucket) {
                                $CloudTrailBucket = Read-InputString -Prompt 'CloudTrail S3 bucket name (required to enable CIEM on the AWS SaaS source)' `
                                    -Default $CloudTrailBucket -Required -Validate $bucketValidate
                            }
                        }
                    }
                    else {
                        $CloudTrailBucket = Read-InputString -Prompt 'CloudTrail S3 bucket name (blank = skip)' -Default $CloudTrailBucket -Validate $bucketValidate
                    }
                }
                Complete-WizardPrompt
            }

            if ($saasCiem) {
                if (Enter-WizardPrompt) {
                    if ($askCloudTrailBucketAccountId) {
                        $defaultBucketAccount = if ($CloudTrailBucketAccountId) { $CloudTrailBucketAccountId } else { $session.Account }
                        $CloudTrailBucketAccountId = Read-InputString -Prompt 'AWS CloudTrail Bucket Account ID (account that hosts the S3 bucket)' `
                            -Default $defaultBucketAccount -Required -Validate { param($v) Test-AwsAccountId $v }
                    }
                    Complete-WizardPrompt
                }

                if (Enter-WizardPrompt) {
                    # CreateNew has no ARN to ask about: the trail does not exist yet, and the stack
                    # reports the ARN it creates at apply.
                    if ($askCloudTrailArn -and $SaasCiemCloudTrailSetup -ne 'CreateNew' `
                            -and (-not $CloudTrailArn -or $CloudTrailArn.Count -eq 0)) {
                        $extraArns = Read-InputString -Prompt 'CloudTrail ARN(s) to include (comma-separated; blank = discover in this account)'
                        if ($extraArns) {
                            $CloudTrailArn = @(Get-NormalizedCloudTrailArnList -Value $extraArns)
                        }
                    }
                    Complete-WizardPrompt
                }
            }

            if ($SourceType -eq 'AwsSaas') {
                if (Enter-WizardPrompt) {
                    if ($Scope -eq 'Organization' -and $askMemberAssumeRole) {
                        $MemberAssumeRole = Read-InputString -Prompt 'Member-account role to assume (e.g. OrganizationAccountAccessRole; blank = management account only)' -Default $MemberAssumeRole
                    }
                    Complete-WizardPrompt
                }
            }

            if (Enter-WizardPrompt) {
                if (-not $OutputDirectory) {
                    $OutputDirectory = (Get-ConnectionSettingsOutput -Type $SourceType).Directory
                }
                $OutputDirectory = Read-InputString -Prompt 'Output directory for connection settings' -Default $OutputDirectory -Required
                Complete-WizardPrompt
            }

            Write-Step 'Plan'
            Write-Host " Source type : $SourceType"
            Write-Host " Role : $RoleName"
            Write-Host " Account : $($session.Account)"
            Write-Host " Org management : $managementAccountId"
            Write-Host " Cloud : $Cloud"
            Write-Host " Scope : $Scope"
            if ($SourceType -eq 'AwsSaas') {
                $policyDocs = Get-SelectedPolicyDocuments -Set $PolicySet -FeatureNames $Feature `
                    -SkipProvisioning:$AggregationOnly -BucketName $CloudTrailBucket -Partition $partition
                $tagSession = @($Feature) -contains 'ActivityInsights'
                $trustDoc = New-TrustPolicyDocument -PrincipalArn $principals -ExternalIdValue $ExternalId `
                    -AllowTagSession:$tagSession
                $planAction = if ($isUpdate) { 'Update existing role' } else { 'Create role' }
                $planFeatures = if ($Feature) { $Feature -join ', ' } else { 'none' }
                $planProv = if ($AggregationOnly) { 'aggregation only' } else { 'included' }
                $planTrustActions = if ($tagSession) { 'sts:AssumeRole, sts:TagSession' } else { 'sts:AssumeRole' }
                Write-Host " Action : $planAction"
                Write-Host " Trust principal : $($principals -join ', ')"
                Write-Host " Trust actions : $planTrustActions"
                Write-Host " Policy set : $PolicySet"
                Write-Host " Provisioning : $planProv"
                Write-Host " Feature packs : $planFeatures"
                Write-Host " CloudTrail bucket : $(if ($CloudTrailBucket) { $CloudTrailBucket } else { 'none' })"
                if ($saasCiem) {
                    $planBucketAccount = if ($CloudTrailBucketAccountId) { $CloudTrailBucketAccountId } else { $session.Account }
                    $planTrailArns = if ($CloudTrailArn) { (Get-NormalizedCloudTrailArnList -Value $CloudTrailArn) -join ', ' } else { 'discover after apply' }
                    $planCloudTrailSetup = if ($SaasCiemCloudTrailSetup -eq 'CreateNew') {
                        "create via stack $SaasCiemCloudTrailStackName"
                    }
                    elseif ($SaasCiemCloudTrailSetup) { $SaasCiemCloudTrailSetup }
                    else { 'EnterName' }
                    Write-Host " CIEM on SaaS source : enable (Additional Settings)"
                    Write-Host " CloudTrail setup : $planCloudTrailSetup"
                    Write-Host " CloudTrail bucket acct: $planBucketAccount"
                    Write-Host " CloudTrail ARN(s) : $planTrailArns"
                }
                Write-Host " Policies : $($policyDocs.Keys -join ', ')"
                Write-Host ''
                Write-Info 'ISC Connection Settings need the role name (not the ARN), region, External ID, and management account ID.'
                if ($saasCiem) {
                    Write-Info 'Then enable Cloud Infrastructure Entitlement Management and paste CloudTrail ARN(s) plus the bucket account ID.'
                }
            }
            else {
                if ($Scope -eq 'Organization') {
                    $invTemplate = Get-CiemTemplateFileName -Purpose 'Inventory' -Cloud $Cloud
                    Write-Host " Inventory StackSet : $InventoryStackName ($invTemplate)"
                    if ($CiemActivity -eq 'OrganizationManagement') {
                        $actTemplate = Get-CiemTemplateFileName -Purpose 'OrganizationManagement' -Cloud $Cloud
                        Write-Host " Activity stack : $ActivityStackName ($actTemplate)"
                    }
                }
                else {
                    if ($CiemActivity -eq 'None') {
                        $invTemplate = Get-CiemTemplateFileName -Purpose 'Inventory' -Cloud $Cloud
                        Write-Host " Inventory stack : $InventoryStackName ($invTemplate)"
                    }
                    else {
                        $actTemplate = Get-CiemTemplateFileName -Purpose $CiemActivity -Cloud $Cloud
                        Write-Host " Activity stack : $ActivityStackName ($actTemplate)"
                    }
                }
                Write-Host " CloudTrail bucket : $CloudTrailBucket"
                if ($CloudTrailArn -and @($CloudTrailArn).Count -gt 0) {
                    Write-Host " CloudTrail ARN(s) : $((Get-NormalizedCloudTrailArnList -Value $CloudTrailArn) -join ', ')"
                }
                if ($Scope -eq 'CurrentAccount') {
                    Write-Host " Identity Center : off (unsupported with Single Account / Account instance)"
                }
                else {
                    $icSource = if ($CiemActivity -eq 'OrganizationManagement') { 'activity template' } else { 'inline role policies' }
                    $icState = if ($EnableIdentityStoreProvision -eq 'true') { 'on' } elseif ($EnableIdentityStoreReadOnly -eq 'true') { 'read only' } else { 'off' }
                    Write-Host " Identity Center : $icState ($icSource)"
                }
                $trustLabel = if ($TrustPrincipal) { 'Trust override ' } else { 'Role trust ' }
                Write-Host " $trustLabel : $($principals -join ', ')"
                Write-Host ''
                Write-Info 'CIEM Connection Settings need the Role ARN (management account when using Organizations), External ID, and optional CloudTrail ARNs.'
            }

            if (Enter-WizardPrompt) {
                if (-not (Read-YesNo -Prompt 'Proceed?' -Default $true)) {
                    Write-Host 'Cancelled.' -ForegroundColor Yellow
                    return
                }
                Complete-WizardPrompt
            }

            $wizardComplete = $true
        }
        catch {
            if (-not (Test-PromptBack $_)) { throw }
            if (-not (Move-WizardBack)) {
                Write-Host ''
                Write-Host 'Cancelled.' -ForegroundColor Yellow
                return
            }
        }
    }

    $consoleHost = if ($Cloud -eq 'GovCloud') { 'https://console.amazonaws-us-gov.com' } else { 'https://console.aws.amazon.com' }
    $settingsOutput = Get-ConnectionSettingsOutput -Type $SourceType -Directory $OutputDirectory

    $saasCiem = $SourceType -eq 'AwsSaas' -and (Test-EmbeddedCiemSelected -FeatureNames $Feature)
    $saasCiemTrailCreated = $false
    if ($saasCiem) {
        if ([string]::IsNullOrWhiteSpace($SaasCiemCloudTrailSetup) -and -not [string]::IsNullOrWhiteSpace($CloudTrailBucket)) {
            $SaasCiemCloudTrailSetup = 'EnterName'
        }
        if ($SaasCiemCloudTrailSetup -eq 'CreateNew') {
            if ([string]::IsNullOrWhiteSpace($ExternalId)) {
                throw 'AwsSaas CIEM CloudTrail creation requires -ExternalId from ISC Connection Settings.'
            }
            Ensure-AwsToolsModules -ExtraModules @('AWS.Tools.CloudFormation', 'AWS.Tools.CloudTrail', 'AWS.Tools.EC2', 'AWS.Tools.S3')
            $trailDeployment = Deploy-SaasCiemCloudTrailResources -Cloud $Cloud -RegionName $Region -ExternalId $ExternalId `
                -BucketName $CloudTrailBucket -TrailName $TrailName -StackName $SaasCiemCloudTrailStackName -AwsParams $awsParams
            $CloudTrailBucket = $trailDeployment.BucketName
            if ($trailDeployment.CloudTrailArns.Count -gt 0) {
                # Use the trail this run set up, not whatever else the account happens to log.
                $CloudTrailArn = @($trailDeployment.CloudTrailArns)
                $saasCiemTrailCreated = $true
            }
        }

        $ciemCtx = [PSCustomObject]@{
            FeatureNames               = $Feature
            CloudTrailBucket           = $CloudTrailBucket
            CloudTrailBucketAccountId  = $CloudTrailBucketAccountId
            SessionAccountId           = $session.Account
            Region                     = $Region
            CloudTrailArn              = $CloudTrailArn
            SkipCloudTrailDiscovery    = $saasCiemTrailCreated
        }
        Apply-EmbeddedCiemPrerequisites -Context $ciemCtx
        $CloudTrailBucketAccountId = $ciemCtx.CloudTrailBucketAccountId
    }

    if ($SourceType -eq 'CiemAws') {
        # Every SailPoint CIEM template takes BucketName. Non-interactive runs never reach the bucket
        # prompt, so they fall back to the template-style default instead of failing.
        if ([string]::IsNullOrWhiteSpace($CloudTrailBucket) -and $session.Account) {
            $CloudTrailBucket = Get-DefaultCiemCloudTrailBucketName -AccountId $session.Account
            Write-Info "No -CloudTrailBucket given; using $CloudTrailBucket."
        }
        if ([string]::IsNullOrWhiteSpace($CloudTrailBucket)) {
            throw 'CiemAws requires -CloudTrailBucket (SailPoint templates need BucketName).'
        }
        $bucketFailure = Test-AwsS3BucketName $CloudTrailBucket
        if ($bucketFailure) { throw $bucketFailure }
        if (-not $PSCmdlet.ShouldProcess($InventoryStackName, 'Deploy SailPoint CIEM CloudFormation')) { return }

        $identityCenterRequested = $Scope -eq 'Organization' -and (
            $askEnableIcRead -or $askEnableIcProv -or
            $PSBoundParameters.ContainsKey('EnableIdentityStoreReadOnly') -or
            $PSBoundParameters.ContainsKey('EnableIdentityStoreProvision')
        )

        $ciemResult = Apply-DedicatedCiemSourceDeployment -Cloud $Cloud -RegionName $Region -Scope $Scope -RoleName $RoleName `
            -ExternalId $ExternalId -BucketName $CloudTrailBucket -ActivityMode $CiemActivity `
            -InventoryStackName $InventoryStackName -ActivityStackName $ActivityStackName `
            -EnableIdentityStoreReadOnlyFlag $EnableIdentityStoreReadOnly `
            -EnableIdentityStoreProvisionFlag $EnableIdentityStoreProvision `
            -TrailNameValue $TrailName -ExtraCloudTrailArn $CloudTrailArn -TrustPrincipalList $TrustPrincipal `
            -AwsParams $awsParams -CurrentAccountId $session.Account -ManagementAccountId $managementAccountId `
            -IdentityCenterRequested:$identityCenterRequested

        $ciemSettings = Build-DedicatedCiemConnectionSettings -DeploymentResult $ciemResult -SessionAccountId $session.Account
        $roleUrl = "$consoleHost/iam/home#/roles/$($ciemResult.RoleName)"
        $cfnUrl = "$consoleHost/cloudformation/home?region=$Region#/stacks"
        $ciemDocUrl = 'https://documentation.sailpoint.com/saas/help/ciem/aws/connect_aws.html'
        $cloudTrailList = $ciemSettings.CloudTrailList

        $situation = [System.Collections.Generic.List[string]]::new()
        $situation.Add('CIEM CloudFormation deployment is complete. Paste the values below into the CIEM AWS source Connection Settings.')
        $situation.Add('Do not enable CIEM on the AWS SaaS source for the same management account when using a dedicated CIEM AWS source.')
        if ($ciemResult.SingleAccount) {
            $situation.Add('In ISC: enable the Single Account toggle on the CIEM AWS source.')
            $situation.Add('Leave Provision Identity Center off. Account instance and Identity Center are mutually exclusive on the CIEM AWS source.')
        }
        elseif ($ciemResult.ProvisionIdentityCenter) {
            $situation.Add('The role carries Identity Center provisioning permissions. Confirm the CIEM source has the PROVISIONING feature before enabling Provision Identity Center.')
        }
        else {
            $situation.Add('In ISC: turn Provision Identity Center OFF. The role has no sso / identitystore permissions, and Test Connection fails while that toggle is on.')
            $situation.Add('To use Identity Center, re-run with Organization scope and answer Yes to the Identity Center permission questions.')
        }
        $situation.Add('After a successful test connection, mark Groups and AccountPermissionSet entitlements as cloud-enabled.')
        $situation.Add('Some CloudTrail events omit the Resource attribute; last-activity detail may be incomplete in certifications.')

        Write-ConnectionSettings -Fields $ciemSettings.Fields -Title 'CIEM AWS source Connection Settings' -Path $settingsOutput.FilePath

        $completionItems = @(
            [PSCustomObject]@{ Label = 'Role ARN'; Value = $ciemResult.RoleArn; Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'External ID'; Value = $ciemResult.ExternalId; Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'CloudTrail ARN(s)'; Value = $cloudTrailList; Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'CloudTrail bucket account ID'; Value = $session.Account; Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'Single Account'; Value = $(if ($ciemResult.SingleAccount) { 'Yes' } else { 'No' }); Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'Provision Identity Center'; Value = $(if ($ciemResult.ProvisionIdentityCenter) { 'Yes' } else { 'No' }); Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'IAM role in AWS console'; Value = $roleUrl; Kind = 'Open'; Mask = $false }
            [PSCustomObject]@{ Label = 'CloudFormation console'; Value = $cfnUrl; Kind = 'Open'; Mask = $false }
            [PSCustomObject]@{ Label = 'Connecting AWS and SailPoint CIEM'; Value = $ciemDocUrl; Kind = 'Open'; Mask = $false }
        )

        Invoke-CompletionActionMenu -Title 'Next: complete CIEM AWS Connection Settings' `
            -Situation $situation.ToArray() `
            -Items $completionItems `
            -AllowSaveToDisk `
            -SavePath $settingsOutput.FilePath
        return
    }

    $action = if ($isUpdate) { "Update IAM role '$RoleName'" } else { "Create IAM role '$RoleName'" }
    if (-not $PSCmdlet.ShouldProcess($RoleName, $action)) { return }

    $targets = [System.Collections.Generic.List[pscustomobject]]::new()
    $targets.Add([PSCustomObject]@{ AccountId = $session.Account; Credential = $null; Label = 'management / current' })
    $skippedMemberAccounts = [System.Collections.Generic.List[string]]::new()

    if ($Scope -eq 'Organization') {
        Write-Step 'Listing organization accounts'
        $orgIds = @(Get-OrganizationAccountIds -AwsParams $awsParams)
        Write-Ok "$($orgIds.Count) ACTIVE account(s)"
        foreach ($id in $orgIds) {
            if ($id -eq $session.Account) { continue }
            if (-not $MemberAssumeRole) {
                $skippedMemberAccounts.Add($id)
                Write-Info "Skipping $id (no member assume role). Re-run in that account or pass -MemberAssumeRole."
                continue
            }
            try {
                $creds = Get-MemberCredentials -AccountId $id -RoleNameToAssume $MemberAssumeRole -Partition $partition -RegionName $Region
                $targets.Add([PSCustomObject]@{ AccountId = $id; Credential = $creds; Label = 'member' })
            }
            catch {
                Write-Warning "Could not assume $MemberAssumeRole in $id : $($_.Exception.Message)"
            }
        }
    }

    $roleArns = [System.Collections.Generic.List[string]]::new()
    foreach ($target in $targets) {
        Write-Step "Configuring $($target.Label) account $($target.AccountId)"
        $targetParams = Get-IamCmdletParams -Credential $target.Credential -RegionName $Region
        $arn = Set-SailPointIamRole -Name $RoleName -TrustDocument $trustDoc -PolicyDocuments $policyDocs `
            -AccountId $target.AccountId -Partition $partition -AwsParams $targetParams
        $roleArns.Add($arn)
        Write-Ok $arn

        Test-SailPointRoleConfiguration -Name $RoleName -ExpectedPrincipal $principals `
            -ExpectedExternalId $ExternalId -AwsParams $targetParams
    }

    $consoleHost = if ($Cloud -eq 'GovCloud') { 'https://console.amazonaws-us-gov.com' } else { 'https://console.aws.amazon.com' }
    $roleUrl = "$consoleHost/iam/home#/roles/$RoleName"
    $orgUrl = "$consoleHost/organizations/v2/home/accounts"

    $cloudScopeIds = if ($orgContext.AccountIds.Count -gt 0) { $orgContext.AccountIds -join ', ' } else { '' }

    $situation = [System.Collections.Generic.List[string]]::new()
    $situation.Add('AWS IAM setup on this account is complete. Finish any pending steps below, then paste the Connection Settings values into ISC.')
    if ($skippedMemberAccounts.Count -gt 0) {
        $situation.Add("Pending: deploy the same role name ($RoleName) and External ID in member account(s): $($skippedMemberAccounts -join ', ').")
    }
    if ($orgContext.NotInUse) {
        $situation.Add('Pending: this account is not in an AWS Organization, so ISC Cloud Scope (AWS Accounts) will stay empty until you create or join an organization.')
    }
    elseif ($orgContext.Available -and -not $orgContext.IsManagement) {
        $situation.Add("Pending: create $RoleName in the organization management account $managementAccountId (you ran this in member account $($session.Account)).")
    }
    elseif ($orgContext.Available -and $orgContext.IsManagement -and $orgContext.AccountIds.Count -eq 0) {
        $situation.Add('Pending: organizations:ListAccounts did not return accounts. Confirm SPOrganizationPolicy on the management-account role before opening AWS Account Settings in ISC.')
    }
    else {
        $situation.Add('In ISC: save Connection Settings, then open AWS Account Settings and select AWS Accounts (Cloud Scope).')
    }
    if ($PolicySet -eq 'NonMgo') {
        $situation.Add('Pending (Non-MGO): remove organization schema objects via the ISC source schema API if the source is not in an Organization.')
    }
    $situation.Add('Use the role name only in ISC (not the ARN). Authentication is IAM Role.')

    $embeddedCiemFields = [ordered]@{}
    $saasCloudTrailList = ''
    if ($saasCiem) {
        $ciemCtx = [PSCustomObject]@{
            FeatureNames              = $Feature
            CloudTrailBucket          = $CloudTrailBucket
            CloudTrailBucketAccountId = $CloudTrailBucketAccountId
            SessionAccountId          = $session.Account
            Region                    = $Region
            CloudTrailArn             = $CloudTrailArn
            SkipCloudTrailDiscovery   = $saasCiemTrailCreated
        }
        $embeddedCiemFields = Build-EmbeddedCiemConnectionSettings -Context $ciemCtx
        $saasCloudTrailList = [string]$embeddedCiemFields['CloudTrail ARN(s)']
        $situation.Add('In ISC Additional Settings: enable Cloud Infrastructure Entitlement Management (CIEM), then paste CloudTrail ARN(s) and the CloudTrail Bucket Account ID.')
        $situation.Add('Do not enable CIEM on this AWS SaaS source if you also use a dedicated CIEM AWS source for the same management account.')
        $situation.Add('After a successful test connection, mark Groups, AWSManagedPolicy, CustomerManagedPolicy, and InlinePolicy entitlements as cloud-enabled.')
    }
    $situation.Add('This source governs IAM users only. To govern Identity Center users and permission sets, run this script again with -SourceType CiemAws and connect a CIEM AWS source.')

    $fields = [ordered]@{
        'Role Name' = $RoleName
        'Region' = $Region
        'External ID' = $ExternalId
        'Management Account ID' = $managementAccountId
    }
    if ($cloudScopeIds) {
        $fields['AWS Accounts (Cloud Scope)'] = $cloudScopeIds
    }
    if ($saasCiem) {
        foreach ($entry in $embeddedCiemFields.GetEnumerator()) {
            $fields[$entry.Key] = $entry.Value
        }
    }
    $settingsPath = $settingsOutput.FilePath
    Write-ConnectionSettings -Fields $fields -Title 'ISC source Connection Settings' -Path $settingsPath

    $completionItems = @(
        [PSCustomObject]@{ Label = 'Role Name'; Value = $RoleName; Kind = 'Copy'; Mask = $false }
        [PSCustomObject]@{ Label = 'Region'; Value = $Region; Kind = 'Copy'; Mask = $false }
        [PSCustomObject]@{ Label = 'External ID'; Value = $ExternalId; Kind = 'Copy'; Mask = $false }
        [PSCustomObject]@{ Label = 'Management Account ID'; Value = $managementAccountId; Kind = 'Copy'; Mask = $false }
    )
    if ($cloudScopeIds) {
        $completionItems += [PSCustomObject]@{ Label = 'AWS Accounts (Cloud Scope)'; Value = $cloudScopeIds; Kind = 'Copy'; Mask = $false }
    }
    if ($saasCiem) {
        $ciemSettingsUrl = 'https://documentation.sailpoint.com/connectors/saas/aws/help/saas_connectivity/aws/ciem_settings.html'
        $completionItems += @(
            [PSCustomObject]@{ Label = 'Enable CIEM'; Value = 'Yes'; Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'CloudTrail ARN(s)'; Value = $saasCloudTrailList; Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'CloudTrail bucket account ID'; Value = $CloudTrailBucketAccountId; Kind = 'Copy'; Mask = $false }
            [PSCustomObject]@{ Label = 'CIEM Settings (AWS SaaS)'; Value = $ciemSettingsUrl; Kind = 'Open'; Mask = $false }
        )
    }
    $completionItems += @(
        [PSCustomObject]@{ Label = 'IAM role in AWS console'; Value = $roleUrl; Kind = 'Open'; Mask = $false }
        [PSCustomObject]@{ Label = 'AWS Organizations console'; Value = $orgUrl; Kind = 'Open'; Mask = $false }
    )

    Invoke-CompletionActionMenu -Title 'Next: complete ISC Connection Settings' `
        -Situation $situation.ToArray() `
        -Items $completionItems `
        -AllowSaveToDisk `
        -SavePath $settingsPath
}
catch {
    if (Test-CancelledNavigation $_) {
        Write-Host ''
        Write-Host 'Cancelled.' -ForegroundColor Yellow
        return
    }
    Write-Host ''
    if (Get-AwsCreatedRoleArn) {
        Write-Warning "Role $(Get-AwsCreatedRoleArn) was created before this failure. Re-run the script with the same role name to finish configuring it."
    }
    Write-Error $_
    exit 1
}