modules/ISC.AwsSourceSetup.psm1

#Requires -Version 5.1
Set-StrictMode -Version Latest

function Import-SourceSetupModule {
    param(
        [Parameter(Mandatory)][string]$ModuleRoot,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$FileName
    )

    if (-not (Get-Module -Name $Name)) {
        Import-Module (Join-Path $ModuleRoot $FileName) -Force -Global -WarningAction SilentlyContinue
    }
}

function Import-AgentAdapterModule {
    if (-not (Get-Module -Name 'ISC.AgentAdapter')) {
        Import-Module (Join-Path $PSScriptRoot 'ISC.AgentAdapter.psm1') -Force -WarningAction SilentlyContinue
    }
}

function Initialize-AwsSourceSetup {
    param(
        [string]$ModuleRoot,
        [switch]$NonInteractive
    )

    if (-not $ModuleRoot) { $ModuleRoot = $PSScriptRoot }
    Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.OperatorConsole' -FileName 'ISC.OperatorConsole.psm1'
    Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.OperatorToolchain' -FileName 'ISC.OperatorToolchain.psm1'
    Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.AwsConnector' -FileName 'ISC.AwsConnector.psm1'
    Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.AwsCiemConnector' -FileName 'ISC.AwsCiemConnector.psm1'
    Import-SourceSetupModule -ModuleRoot $ModuleRoot -Name 'ISC.AwsSaasConnector' -FileName 'ISC.AwsSaasConnector.psm1'
    Initialize-OperatorConsole -NonInteractive:$NonInteractive
    Initialize-AwsConnectorData
    Initialize-AwsCiemConnectorData
    Initialize-AwsSaasConnectorData
}

function Get-AwsAgentCatalog {
    param([Parameter(Mandatory)][ValidateSet('aws-saas', 'aws-ciem')][string]$Variant)

    Initialize-AwsSourceSetup
    if ($Variant -eq 'aws-saas') {
        $catalog = Get-AwsSaasCatalog
        return [ordered]@{
            variant        = $Variant
            featurePacks   = @($catalog.FeaturePacks.Keys)
            policySets     = @('Mgo', 'NonMgo')
            scopes         = @('CurrentAccount', 'Organization')
            requiredConfig = @('externalId')
            optionalConfig = @('profileName', 'region', 'cloud', 'roleName', 'features', 'scope', 'cloudTrailBucket')
        }
    }

    return [ordered]@{
        variant        = $Variant
        ciemActivities = @('None', 'OrganizationManagement', 'ExistingCloudTrail', 'NewCloudTrailExistingBucket', 'NewCloudTrailAndBucket')
        scopes         = @('CurrentAccount', 'Organization')
        requiredConfig = @('externalId', 'cloudTrailBucket')
        optionalConfig = @('profileName', 'region', 'cloud', 'roleName', 'ciemActivity', 'scope')
    }
}

function Get-AwsResolvedConfig {
    param(
        [Parameter(Mandatory)]$Request,
        [Parameter(Mandatory)][string]$Variant
    )

    Import-AgentAdapterModule
    $config = Get-AgentRequestValue -Object $Request -Name 'config' -Default @{}
    $decisions = Get-AgentRequestValue -Object $Request -Name 'decisions' -Default @{}
    $sourceType = if ($Variant -eq 'aws-ciem') { 'CiemAws' } else { 'AwsSaas' }
    $roleDefault = if ($sourceType -eq 'CiemAws') { Get-DefaultCiemRoleName } else { Get-DefaultSaasRoleName }
    $outputDir = Get-AgentRequestValue -Object $config -Name 'outputDirectory'
    $output = Get-ConnectionSettingsOutput -Type $sourceType -Directory $outputDir

    return [PSCustomObject]@{
        SourceType                = $sourceType
        ProfileName               = $(if (Get-AgentRequestValue -Object $config -Name 'profileName') { [string](Get-AgentRequestValue -Object $config -Name 'profileName') } else { $null })
        Region                    = if (Get-AgentRequestValue -Object $config -Name 'region') { [string](Get-AgentRequestValue -Object $config -Name 'region') } else { 'us-east-1' }
        Cloud                     = if (Get-AgentRequestValue -Object $config -Name 'cloud') { [string](Get-AgentRequestValue -Object $config -Name 'cloud') } else { 'Commercial' }
        TrustPrincipal            = @($(Get-AgentRequestValue -Object $config -Name 'trustPrincipal' -Default @()))
        RoleName                  = if (Get-AgentRequestValue -Object $config -Name 'roleName') { [string](Get-AgentRequestValue -Object $config -Name 'roleName') } else { $roleDefault }
        ExternalId                = [string](Get-AgentRequestValue -Object $config -Name 'externalId')
        PolicySet                 = if (Get-AgentRequestValue -Object $config -Name 'policySet') { [string](Get-AgentRequestValue -Object $config -Name 'policySet') } else { 'Mgo' }
        Feature                   = @($(Get-AgentRequestValue -Object $config -Name 'features' -Default @()))
        AggregationOnly           = [bool](Get-AgentRequestValue -Object $config -Name 'aggregationOnly' -Default $false)
        CloudTrailBucket          = $(if (Get-AgentRequestValue -Object $config -Name 'cloudTrailBucket') { [string](Get-AgentRequestValue -Object $config -Name 'cloudTrailBucket') } else { $null })
        CloudTrailBucketAccountId = $(if (Get-AgentRequestValue -Object $config -Name 'cloudTrailBucketAccountId') { [string](Get-AgentRequestValue -Object $config -Name 'cloudTrailBucketAccountId') } else { $null })
        CloudTrailArn             = @($(Get-AgentRequestValue -Object $config -Name 'cloudTrailArn' -Default @()))
        Scope                     = if (Get-AgentRequestValue -Object $config -Name 'scope') { [string](Get-AgentRequestValue -Object $config -Name 'scope') } else { 'CurrentAccount' }
        MemberAssumeRole          = $(if (Get-AgentRequestValue -Object $config -Name 'memberAssumeRole') { [string](Get-AgentRequestValue -Object $config -Name 'memberAssumeRole') } else { $null })
        CiemActivity              = if (Get-AgentRequestValue -Object $config -Name 'ciemActivity') { [string](Get-AgentRequestValue -Object $config -Name 'ciemActivity') } else { 'None' }
        EnableIdentityStoreReadOnly = if (Get-AgentRequestValue -Object $config -Name 'enableIdentityStoreReadOnly') { [string](Get-AgentRequestValue -Object $config -Name 'enableIdentityStoreReadOnly') } else { 'true' }
        EnableIdentityStoreProvision = if (Get-AgentRequestValue -Object $config -Name 'enableIdentityStoreProvision') { [string](Get-AgentRequestValue -Object $config -Name 'enableIdentityStoreProvision') } else { 'true' }
        TrailName                 = if (Get-AgentRequestValue -Object $config -Name 'trailName') { [string](Get-AgentRequestValue -Object $config -Name 'trailName') } else { 'sailpoint-ciem-cloud-trail' }
        InventoryStackName        = if (Get-AgentRequestValue -Object $config -Name 'inventoryStackName') { [string](Get-AgentRequestValue -Object $config -Name 'inventoryStackName') } else { 'SailPointCiemInventory' }
        ActivityStackName         = if (Get-AgentRequestValue -Object $config -Name 'activityStackName') { [string](Get-AgentRequestValue -Object $config -Name 'activityStackName') } else { 'SailPointCiemActivity' }
        OutputDirectory           = $output.Directory
        SettingsPath              = $output.FilePath
        UpdateExistingRole        = $(if ($null -ne (Get-AgentRequestValue -Object $decisions -Name 'updateExistingRole')) { [bool](Get-AgentRequestValue -Object $decisions -Name 'updateExistingRole') } else { $null })
        ApproveOrganizationScope  = [bool](Get-AgentRequestValue -Object $decisions -Name 'approveOrganizationScope' -Default $false)
    }
}

function New-AwsAgentPlan {
    param(
        [Parameter(Mandatory)]$Request,
        [Parameter(Mandatory)][ValidateSet('aws-saas', 'aws-ciem')][string]$Variant
    )

    Initialize-AwsSourceSetup
    $resolved = Get-AwsResolvedConfig -Request $Request -Variant $Variant
    $needsInput = [System.Collections.Generic.List[object]]::new()
    $warnings = [System.Collections.Generic.List[string]]::new()
    $discoveries = [ordered]@{}

    if ([string]::IsNullOrWhiteSpace($resolved.ExternalId)) {
        $needsInput.Add([ordered]@{ field = 'externalId'; reason = 'External ID from ISC Connection Settings is required.' })
    }
    if ($Variant -eq 'aws-ciem' -and [string]::IsNullOrWhiteSpace($resolved.CloudTrailBucket)) {
        $needsInput.Add([ordered]@{ field = 'cloudTrailBucket'; reason = 'CIEM AWS source requires a CloudTrail bucket name.' })
    }
    if ($resolved.Scope -eq 'Organization' -and -not $resolved.ApproveOrganizationScope) {
        $needsInput.Add([ordered]@{ field = 'decisions.approveOrganizationScope'; reason = 'Organization-wide deployment requires explicit approval.' })
    }
    if ($Variant -eq 'aws-saas' -and @($resolved.Feature) -contains 'Ciem' -and [string]::IsNullOrWhiteSpace($resolved.CloudTrailBucket)) {
        $needsInput.Add([ordered]@{ field = 'cloudTrailBucket'; reason = 'AWS SaaS CIEM feature requires CloudTrail bucket name.' })
    }
    $identityCenterFeatures = @(@($resolved.Feature) | Where-Object { $_ -in @('IdentityCenter', 'IdentityCenterProvisioning') })
    if ($Variant -eq 'aws-saas' -and $identityCenterFeatures.Count -gt 0) {
        $warnings.Add("Ignoring $($identityCenterFeatures -join ', '): Identity Center is governed by a CIEM AWS source (variant aws-ciem), not the AWS SaaS source.")
    }

    try {
        Ensure-AwsToolsModules
        if ($Variant -eq 'aws-ciem') {
            Ensure-AwsToolsModules -ExtraModules @('AWS.Tools.CloudFormation', 'AWS.Tools.CloudTrail', 'AWS.Tools.EC2', 'AWS.Tools.S3')
        }
        $session = Connect-AwsSession -RequestedProfile $resolved.ProfileName -RequestedRegion $resolved.Region
        $discoveries.accountId = $session.Account
        $discoveries.arn = $session.Arn
        $awsParams = Get-IamCmdletParams -RegionName $resolved.Region
        try {
            $existingRole = Get-IAMRole @awsParams -RoleName $resolved.RoleName
            $discoveries.existingRoleArn = $existingRole.Arn
            if ($null -eq $resolved.UpdateExistingRole) {
                $needsInput.Add([ordered]@{
                    field  = 'decisions.updateExistingRole'
                    reason = "IAM role '$($resolved.RoleName)' already exists."
                    options = @(
                        [ordered]@{ value = $true; label = 'Update existing role' }
                        [ordered]@{ value = $false; label = 'Abort and choose a different role name' }
                    )
                })
            }
        }
        catch {
            if ($_.Exception.Message -notmatch 'NoSuchEntity|cannot be found') {
                $warnings.Add("Role discovery failed: $($_.Exception.Message)")
            }
        }
        $orgContext = Get-AwsOrganizationContext -CurrentAccountId $session.Account -AwsParams $awsParams
        $discoveries.organization = $orgContext
    }
    catch {
        $warnings.Add("Live discovery skipped: $($_.Exception.Message)")
    }

    $mutations = if ($Variant -eq 'aws-ciem') {
        @('connect-aws', 'deploy-ciem-cloudformation', 'sync-ciem-trust')
    } else {
        @('connect-aws', 'deploy-iam-role', 'verify-role-trust')
    }

    return [ordered]@{
        status      = if ($needsInput.Count -gt 0) { 'needsInput' } else { 'ready' }
        requestHash = (Get-AgentRequestHash -Request $Request)
        resolved    = $resolved
        discoveries = $discoveries
        mutations   = $mutations
        prerequisites = @('AWS Tools for PowerShell', 'IAM permissions in target account')
        warnings    = @($warnings)
        manualSteps = @('Paste connection settings into ISC after Apply.')
        needsInput  = @($needsInput)
    }
}

function Invoke-AwsSaasSourceApply {
    param(
        [Parameter(Mandatory)]$Resolved,
        [switch]$WhatIf
    )

    $trustConfig = Get-SailPointTrustConfig -Cloud $Resolved.Cloud
    $partition = $trustConfig.Partition
    $principals = if ($Resolved.TrustPrincipal.Count -gt 0) {
        @(Resolve-TrustPrincipal -Value $Resolved.TrustPrincipal -Partition $partition)
    } else {
        @($trustConfig.Principals)
    }
    $allowTagSession = @($Resolved.Feature) -contains 'ActivityInsights'
    $trustDoc = New-TrustPolicyDocument -PrincipalArn $principals -ExternalIdValue $Resolved.ExternalId -AllowTagSession:$allowTagSession
    $policyDocs = Get-SelectedPolicyDocuments -Set $Resolved.PolicySet -FeatureNames $Resolved.Feature `
        -SkipProvisioning:$Resolved.AggregationOnly -BucketName $Resolved.CloudTrailBucket -Partition $partition
    $session = Connect-AwsSession -RequestedProfile $Resolved.ProfileName -RequestedRegion $Resolved.Region
    $awsParams = Get-IamCmdletParams -RegionName $Resolved.Region

    if ($WhatIf) {
        return [ordered]@{ status = 'whatIf'; roleName = $Resolved.RoleName; accountId = $session.Account }
    }

    $arn = Set-SailPointIamRole -Name $Resolved.RoleName -TrustDocument $trustDoc -PolicyDocuments $policyDocs `
        -AccountId $session.Account -Partition $partition -AwsParams $awsParams
    Test-SailPointRoleConfiguration -Name $Resolved.RoleName -ExpectedPrincipal $principals `
        -ExpectedExternalId $Resolved.ExternalId -AwsParams $awsParams

    $orgContext = Get-AwsOrganizationContext -CurrentAccountId $session.Account -AwsParams $awsParams
    $managementAccountId = if ($orgContext.MasterAccountId) { $orgContext.MasterAccountId } else { $session.Account }
    $fields = [ordered]@{
        'Role Name'             = $Resolved.RoleName
        'Region'                = $Resolved.Region
        'External ID'           = $Resolved.ExternalId
        'Management Account ID' = $managementAccountId
    }
    if ($orgContext.AccountIds.Count -gt 0) {
        $fields['AWS Accounts (Cloud Scope)'] = ($orgContext.AccountIds -join ', ')
    }

    if (-not (Test-Path -LiteralPath $Resolved.OutputDirectory)) {
        New-Item -ItemType Directory -Path $Resolved.OutputDirectory -Force | Out-Null
    }
    Write-ConnectionSettings -Fields $fields -Title 'ISC source Connection Settings' -Path $Resolved.SettingsPath

    return [ordered]@{
        status             = 'ok'
        connectionSettings = $fields
        artifacts          = @([ordered]@{ label = 'Connection settings'; path = $Resolved.SettingsPath })
        secretArtifacts    = @()
        verification       = [ordered]@{ roleArn = $arn; externalId = $Resolved.ExternalId }
        situation          = @('AWS IAM setup is complete. Paste Connection Settings into ISC.')
    }
}

function Invoke-AwsCiemSourceApply {
    param(
        [Parameter(Mandatory)]$Resolved,
        [switch]$WhatIf
    )

    if ($WhatIf) {
        return [ordered]@{ status = 'whatIf'; roleName = $Resolved.RoleName; bucket = $Resolved.CloudTrailBucket }
    }

    $session = Connect-AwsSession -RequestedProfile $Resolved.ProfileName -RequestedRegion $Resolved.Region
    $awsParams = Get-IamCmdletParams -RegionName $Resolved.Region
    $orgContext = Get-AwsOrganizationContext -CurrentAccountId $session.Account -AwsParams $awsParams
    $managementAccountId = if ($orgContext.MasterAccountId) { $orgContext.MasterAccountId } else { $session.Account }
    $identityCenterRequested = $Resolved.Scope -eq 'Organization'
    $deployment = Apply-DedicatedCiemSourceDeployment -Cloud $Resolved.Cloud -RegionName $Resolved.Region -Scope $Resolved.Scope `
        -RoleName $Resolved.RoleName -ExternalId $Resolved.ExternalId -BucketName $Resolved.CloudTrailBucket `
        -ActivityMode $Resolved.CiemActivity -InventoryStackName $Resolved.InventoryStackName `
        -ActivityStackName $Resolved.ActivityStackName -TrailNameValue $Resolved.TrailName `
        -EnableIdentityStoreReadOnlyFlag $Resolved.EnableIdentityStoreReadOnly `
        -EnableIdentityStoreProvisionFlag $Resolved.EnableIdentityStoreProvision `
        -ExtraCloudTrailArn $Resolved.CloudTrailArn -TrustPrincipalList $Resolved.TrustPrincipal `
        -AwsParams $awsParams -CurrentAccountId $session.Account -ManagementAccountId $managementAccountId `
        -IdentityCenterRequested:$identityCenterRequested

    $settings = Build-DedicatedCiemConnectionSettings -DeploymentResult $deployment -SessionAccountId $session.Account
    if (-not (Test-Path -LiteralPath $Resolved.OutputDirectory)) {
        New-Item -ItemType Directory -Path $Resolved.OutputDirectory -Force | Out-Null
    }
    Write-ConnectionSettings -Fields $settings.Fields -Title 'CIEM AWS source Connection Settings' -Path $Resolved.SettingsPath

    return [ordered]@{
        status             = 'ok'
        connectionSettings = $settings.Fields
        artifacts          = @([ordered]@{ label = 'Connection settings'; path = $Resolved.SettingsPath })
        secretArtifacts    = @()
        verification       = [ordered]@{ roleArn = $deployment.RoleArn; externalId = $deployment.ExternalId }
        situation          = @('CIEM AWS deployment is complete. Paste Connection Settings into ISC.')
    }
}

function Invoke-AwsAgentApply {
    param(
        [Parameter(Mandatory)]$Request,
        [Parameter(Mandatory)]$Plan,
        [Parameter(Mandatory)][ValidateSet('aws-saas', 'aws-ciem')][string]$Variant,
        [switch]$WhatIf
    )

    Initialize-AwsSourceSetup
    Ensure-AwsToolsModules
    if ($Variant -eq 'aws-ciem') {
        Ensure-AwsToolsModules -ExtraModules @('AWS.Tools.CloudFormation', 'AWS.Tools.CloudTrail', 'AWS.Tools.EC2', 'AWS.Tools.S3')
    }

    $resolved = Get-AwsResolvedConfig -Request $Request -Variant $Variant
    if ($Plan.resolved) {
        foreach ($prop in $Plan.resolved.PSObject.Properties) {
            $resolved.$($prop.Name) = $prop.Value
        }
    }

    if ($Variant -eq 'aws-ciem') {
        return Invoke-AwsCiemSourceApply -Resolved $resolved -WhatIf:$WhatIf
    }
    return Invoke-AwsSaasSourceApply -Resolved $resolved -WhatIf:$WhatIf
}

Export-ModuleMember -Function @(
    'Initialize-AwsSourceSetup'
    'Get-AwsAgentCatalog'
    'Get-AwsResolvedConfig'
    'New-AwsAgentPlan'
    'Invoke-AwsSaasSourceApply'
    'Invoke-AwsCiemSourceApply'
    'Invoke-AwsAgentApply'
)