Public/Connect-CIEM.ps1

function Connect-CIEM {
    <#
    .SYNOPSIS
        Establishes authentication to all configured cloud providers for CIEM scans.

    .DESCRIPTION
        Reads authentication configuration from config.json and connects to each
        enabled cloud provider. This function must be called once before running
        any CIEM scans. The connection is cached for the duration of the PowerShell
        session.

        Currently supports:
        - Azure (ServicePrincipalSecret, ServicePrincipalCertificate, ManagedIdentity, Interactive)
        - AWS (coming soon)

    .PARAMETER Provider
        Optional. Connect only to specific provider(s). If not specified, connects
        to the provider defined in config.json's cloudProvider setting.

    .PARAMETER Force
        Force re-authentication even if already connected.

    .OUTPUTS
        [PSCustomObject] Connection summary showing status for each provider.

    .EXAMPLE
        Connect-CIEM
        # Connects to the default provider from config.json

    .EXAMPLE
        Connect-CIEM -Provider Azure
        # Connects only to Azure

    .EXAMPLE
        Connect-CIEM -Force
        # Forces re-authentication even if already connected

    .NOTES
        This function must be called before running Invoke-CIEMScan or any other
        scan functions. Use Test-CIEMAuthenticated to check connection status.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('Azure', 'AWS')]
        [string[]]$Provider,

        [Parameter()]
        [switch]$Force
    )

    $ErrorActionPreference = 'Stop'

    Write-CIEMLog -Message "Connect-CIEM called with Provider=[$($Provider -join ',')] Force=$Force" -Severity INFO -Component 'Connect-CIEM'

    # Initialize auth context storage if not exists
    if (-not $script:AuthContext) {
        $script:AuthContext = @{}
        Write-CIEMLog -Message "Initialized empty AuthContext" -Severity DEBUG -Component 'Connect-CIEM'
    }

    # Determine which providers to connect
    if (-not $Provider) {
        $Provider = @($script:Config.cloudProvider)
        Write-CIEMLog -Message "No provider specified, using config default: $($script:Config.cloudProvider)" -Severity DEBUG -Component 'Connect-CIEM'
    }

    Write-CIEMLog -Message "Providers to connect: $($Provider -join ', ')" -Severity INFO -Component 'Connect-CIEM'

    $results = @()

    foreach ($p in $Provider) {
        Write-CIEMLog -Message "Processing provider: $p" -Severity INFO -Component 'Connect-CIEM'
        Write-Verbose "Connecting to provider: $p"

        # Skip if already connected and not forcing
        if (-not $Force -and $script:AuthContext[$p]) {
            Write-CIEMLog -Message "$p is already connected (AccountId: $($script:AuthContext[$p].AccountId)). Skipping." -Severity INFO -Component 'Connect-CIEM'
            Write-Verbose "$p is already connected. Use -Force to re-authenticate."
            $results += [PSCustomObject]@{
                Provider = $p
                Status   = 'AlreadyConnected'
                Account  = $script:AuthContext[$p].AccountId
                TenantId = $script:AuthContext[$p].TenantId
                Message  = 'Already authenticated. Use -Force to re-authenticate.'
            }
            continue
        }

        try {
            switch ($p) {
                'Azure' {
                    Write-CIEMLog -Message "Calling Connect-CIEMAzure..." -Severity INFO -Component 'Connect-CIEM'
                    $authContext = Connect-CIEMAzure
                    $script:AuthContext['Azure'] = $authContext
                    Write-CIEMLog -Message "Azure connection successful. AccountId: $($authContext.AccountId), TenantId: $($authContext.TenantId), Subscriptions: $($authContext.SubscriptionIds.Count)" -Severity INFO -Component 'Connect-CIEM'

                    $results += [PSCustomObject]@{
                        Provider      = 'Azure'
                        Status        = 'Connected'
                        Account       = $authContext.AccountId
                        TenantId      = $authContext.TenantId
                        Subscriptions = $authContext.SubscriptionIds.Count
                        Message       = "Connected as $($authContext.AccountType)"
                    }
                }
                'AWS' {
                    Write-CIEMLog -Message "AWS provider not yet supported" -Severity WARNING -Component 'Connect-CIEM'
                    # AWS support coming soon
                    $results += [PSCustomObject]@{
                        Provider = 'AWS'
                        Status   = 'NotSupported'
                        Account  = $null
                        TenantId = $null
                        Message  = 'AWS provider support coming soon'
                    }
                }
            }
        }
        catch {
            $script:AuthContext[$p] = $null
            Write-CIEMLog -Message "Failed to connect to $p : $($_.Exception.Message)" -Severity ERROR -Component 'Connect-CIEM'
            Write-CIEMLog -Message "Stack trace: $($_.ScriptStackTrace)" -Severity DEBUG -Component 'Connect-CIEM'
            $results += [PSCustomObject]@{
                Provider = $p
                Status   = 'Failed'
                Account  = $null
                TenantId = $null
                Message  = $_.Exception.Message
            }
            Write-Error "Failed to connect to $p : $_"
        }
    }

    # Display summary
    Write-Host "`nCIEM Connection Summary:" -ForegroundColor Cyan
    foreach ($r in $results) {
        $color = switch ($r.Status) {
            'Connected' { 'Green' }
            'AlreadyConnected' { 'Yellow' }
            'Failed' { 'Red' }
            default { 'Gray' }
        }
        Write-Host " $($r.Provider): " -NoNewline
        Write-Host $r.Status -ForegroundColor $color -NoNewline
        if ($r.Account) {
            Write-Host " ($($r.Account))" -NoNewline
        }
        Write-Host ""
    }

    Write-CIEMLog -Message "Connect-CIEM completed. Results: $(($results | ForEach-Object { "$($_.Provider)=$($_.Status)" }) -join ', ')" -Severity INFO -Component 'Connect-CIEM'

    [PSCustomObject]@{
        Providers = $results
        Timestamp = Get-Date
    }
}

function Connect-CIEMAzure {
    <#
    .SYNOPSIS
        Internal function to establish Azure authentication.

    .DESCRIPTION
        Credential resolution order:
        1. PSU secrets (when running in PowerShell Universal)
        2. .env file in module directory (for local development)
        3. config.json values (legacy, typically null)
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '',
        Justification = 'Secret is already in memory from secure source; conversion to SecureString is required for Connect-AzAccount')]
    [OutputType([PSCustomObject])]
    param()

    $ErrorActionPreference = 'Stop'

    Write-CIEMLog -Message "Connect-CIEMAzure started" -Severity INFO -Component 'Connect-CIEMAzure'

    # Clear any existing Az context to ensure clean authentication
    Write-CIEMLog -Message "Clearing existing Az context..." -Severity DEBUG -Component 'Connect-CIEMAzure'
    Clear-AzContext -Force -ErrorAction SilentlyContinue | Out-Null

    # Note: $script:Config should be updated by Save-CIEMConfig before calling Connect-CIEM
    if (-not $script:Config) {
        Write-CIEMLog -Message "Config not loaded, calling Get-CIEMConfig..." -Severity DEBUG -Component 'Connect-CIEMAzure'
        $script:Config = Get-CIEMConfig
    }

    $authConfig = $script:Config.azure.authentication
    $authMethod = $authConfig.method

    Write-CIEMLog -Message "Authentication method from config: $authMethod" -Severity INFO -Component 'Connect-CIEMAzure'
    Write-Verbose "Azure authentication method: $authMethod"

    # Check if running in PSU context (Secret: drive available)
    $inPSUContext = $null -ne (Get-PSDrive -Name 'Secret' -ErrorAction SilentlyContinue)
    Write-CIEMLog -Message "PSU context detected: $inPSUContext" -Severity INFO -Component 'Connect-CIEMAzure'

    # Load .env file for local development
    $envVars = @{}
    $envPath = Join-Path -Path $script:ModuleRoot -ChildPath '.env'
    if (Test-Path $envPath) {
        Write-CIEMLog -Message "Loading credentials from .env file: $envPath" -Severity DEBUG -Component 'Connect-CIEMAzure'
        Write-Verbose "Loading credentials from .env file..."
        Get-Content $envPath | ForEach-Object {
            if ($_ -match '^\s*([^#][^=]+)=(.*)$') {
                $envVars[$matches[1].Trim()] = $matches[2].Trim()
            }
        }
        Write-CIEMLog -Message ".env keys found: $($envVars.Keys -join ', ')" -Severity DEBUG -Component 'Connect-CIEMAzure'
    } else {
        Write-CIEMLog -Message "No .env file found at: $envPath" -Severity DEBUG -Component 'Connect-CIEMAzure'
    }

    switch ($authMethod) {
        'ServicePrincipalSecret' {
            Write-CIEMLog -Message "Processing ServicePrincipalSecret authentication..." -Severity INFO -Component 'Connect-CIEMAzure'
            $tenantId = $null
            $clientId = $null
            $clientSecret = $null

            # Priority 1: PSU secrets
            if ($inPSUContext) {
                Write-CIEMLog -Message "Checking PSU secrets..." -Severity DEBUG -Component 'Connect-CIEMAzure'
                Write-Verbose "PSU context detected, checking for secrets..."
                $tenantId = $Secret:CIEM_Azure_TenantId
                $clientId = $Secret:CIEM_Azure_ClientId
                $clientSecret = $Secret:CIEM_Azure_ClientSecret
                Write-CIEMLog -Message "PSU secrets - TenantId: $(if($tenantId){'found'}else{'null'}), ClientId: $(if($clientId){'found'}else{'null'}), ClientSecret: $(if($clientSecret){'found'}else{'null'})" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }

            # Priority 2: .env file
            if (-not $tenantId) {
                $tenantId = $envVars['CIEM_AZURE_TENANT_ID']
                Write-CIEMLog -Message "TenantId from .env: $(if($tenantId){'found'}else{'null'})" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }
            if (-not $clientId) {
                $clientId = $envVars['CIEM_AZURE_CLIENT_ID']
                Write-CIEMLog -Message "ClientId from .env: $(if($clientId){'found'}else{'null'})" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }
            if (-not $clientSecret) {
                $clientSecret = $envVars['CIEM_AZURE_CLIENT_SECRET']
                Write-CIEMLog -Message "ClientSecret from .env: $(if($clientSecret){'found'}else{'null'})" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }

            # Priority 3: config.json (legacy fallback)
            if (-not $tenantId) {
                $tenantId = $authConfig.tenantId
                Write-CIEMLog -Message "TenantId from config.json: $(if($tenantId){'found'}else{'null'})" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }
            if (-not $clientId) {
                $clientId = $authConfig.servicePrincipal.clientId
                Write-CIEMLog -Message "ClientId from config.json: $(if($clientId){'found'}else{'null'})" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }
            if (-not $clientSecret) {
                $clientSecret = $authConfig.servicePrincipal.clientSecret
                Write-CIEMLog -Message "ClientSecret from config.json: $(if($clientSecret){'found'}else{'null'})" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }

            # Final credential summary
            Write-CIEMLog -Message "Final credentials - TenantId: $(if($tenantId){$tenantId.Substring(0,8)+'...'}else{'MISSING'}), ClientId: $(if($clientId){$clientId.Substring(0,8)+'...'}else{'MISSING'}), ClientSecret: $(if($clientSecret){'SET'}else{'MISSING'})" -Severity INFO -Component 'Connect-CIEMAzure'

            if (-not $clientId -or -not $clientSecret -or -not $tenantId) {
                $errorMsg = @"
Authentication method is 'ServicePrincipalSecret' but credentials not found.

Credential sources checked (in order):
$(if ($inPSUContext) { " 1. PSU secrets: CIEM_Azure_TenantId, CIEM_Azure_ClientId, CIEM_Azure_ClientSecret" } else { " 1. PSU secrets: (not in PSU context)" })
  2. .env file: $(if (Test-Path $envPath) { $envPath } else { '(not found)' })
  3. config.json: tenantId, servicePrincipal.clientId, servicePrincipal.clientSecret

For local development, create a .env file in the module directory with:
  CIEM_AZURE_TENANT_ID=<tenant-id>
  CIEM_AZURE_CLIENT_ID=<client-id>
  CIEM_AZURE_CLIENT_SECRET=<client-secret>
"@

                Write-CIEMLog -Message "Credential validation failed: TenantId=$(if($tenantId){'ok'}else{'MISSING'}), ClientId=$(if($clientId){'ok'}else{'MISSING'}), ClientSecret=$(if($clientSecret){'ok'}else{'MISSING'})" -Severity ERROR -Component 'Connect-CIEMAzure'
                throw $errorMsg
            }

            $secureSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force
            $credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)

            Write-CIEMLog -Message "Calling Connect-AzAccount as service principal: $($clientId.Substring(0,8))..." -Severity INFO -Component 'Connect-CIEMAzure'
            Write-Verbose "Connecting as service principal: $clientId"
            Connect-AzAccount -ServicePrincipal -Credential $credential -TenantId $tenantId -ErrorAction Stop | Out-Null
            Write-CIEMLog -Message "Connect-AzAccount completed successfully" -Severity INFO -Component 'Connect-CIEMAzure'
        }
        'ServicePrincipalCertificate' {
            Write-CIEMLog -Message "Processing ServicePrincipalCertificate authentication..." -Severity INFO -Component 'Connect-CIEMAzure'
            $tenantId = $null
            $clientId = $null
            $thumbprint = $null

            # Priority 1: PSU secrets
            if ($inPSUContext) {
                Write-CIEMLog -Message "Checking PSU secrets for certificate auth..." -Severity DEBUG -Component 'Connect-CIEMAzure'
                Write-Verbose "PSU context detected, checking for certificate secrets..."
                $tenantId = $Secret:CIEM_Azure_TenantId
                $clientId = $Secret:CIEM_Azure_ClientId
                $thumbprint = $Secret:CIEM_Azure_CertThumbprint
            }

            # Priority 2: .env file
            if (-not $tenantId) { $tenantId = $envVars['CIEM_AZURE_TENANT_ID'] }
            if (-not $clientId) { $clientId = $envVars['CIEM_AZURE_CLIENT_ID'] }
            if (-not $thumbprint) { $thumbprint = $envVars['CIEM_AZURE_CERT_THUMBPRINT'] }

            Write-CIEMLog -Message "Certificate auth credentials - TenantId: $(if($tenantId){'found'}else{'null'}), ClientId: $(if($clientId){'found'}else{'null'}), Thumbprint: $(if($thumbprint){'found'}else{'null'})" -Severity INFO -Component 'Connect-CIEMAzure'

            if (-not $clientId -or -not $tenantId) {
                Write-CIEMLog -Message "Certificate auth failed: missing TenantId or ClientId" -Severity ERROR -Component 'Connect-CIEMAzure'
                throw "Authentication method is 'ServicePrincipalCertificate' but tenantId or clientId not found in PSU secrets or .env file"
            }

            $connectParams = @{
                ServicePrincipal = $true
                ApplicationId    = $clientId
                TenantId         = $tenantId
            }

            if ($thumbprint) {
                $connectParams.CertificateThumbprint = $thumbprint
                Write-CIEMLog -Message "Using certificate thumbprint for authentication" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }
            else {
                Write-CIEMLog -Message "Certificate auth failed: no thumbprint provided" -Severity ERROR -Component 'Connect-CIEMAzure'
                throw "Certificate authentication requires thumbprint in PSU secrets (CIEM_Azure_CertThumbprint) or .env file (CIEM_AZURE_CERT_THUMBPRINT)"
            }

            Write-CIEMLog -Message "Calling Connect-AzAccount with certificate..." -Severity INFO -Component 'Connect-CIEMAzure'
            Write-Verbose "Connecting with certificate for: $clientId"
            Connect-AzAccount @connectParams -ErrorAction Stop | Out-Null
            Write-CIEMLog -Message "Certificate authentication completed successfully" -Severity INFO -Component 'Connect-CIEMAzure'
        }
        'ManagedIdentity' {
            Write-CIEMLog -Message "Processing ManagedIdentity authentication..." -Severity INFO -Component 'Connect-CIEMAzure'
            $connectParams = @{ Identity = $true }

            # Check for user-assigned managed identity config (optional)
            $miClientId = $null
            if ($authConfig.PSObject.Properties['managedIdentity'] -and $authConfig.managedIdentity.clientId) {
                $miClientId = $authConfig.managedIdentity.clientId
            }

            if ($miClientId) {
                $connectParams.AccountId = $miClientId
                Write-CIEMLog -Message "Using user-assigned managed identity: $miClientId" -Severity INFO -Component 'Connect-CIEMAzure'
                Write-Verbose "Connecting with user-assigned managed identity: $miClientId"
            }
            else {
                Write-CIEMLog -Message "Using system-assigned managed identity" -Severity INFO -Component 'Connect-CIEMAzure'
                Write-Verbose "Connecting with system-assigned managed identity"
            }

            Write-CIEMLog -Message "Calling Connect-AzAccount with -Identity..." -Severity INFO -Component 'Connect-CIEMAzure'
            Connect-AzAccount @connectParams -ErrorAction Stop | Out-Null
            Write-CIEMLog -Message "Managed identity authentication completed successfully" -Severity INFO -Component 'Connect-CIEMAzure'
        }
        'DeviceCode' {
            Write-CIEMLog -Message "Processing DeviceCode authentication..." -Severity INFO -Component 'Connect-CIEMAzure'
            $connectParams = @{ UseDeviceAuthentication = $true }
            if ($authConfig.tenantId) {
                $connectParams.TenantId = $authConfig.tenantId
                Write-CIEMLog -Message "Using tenant: $($authConfig.tenantId)" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }
            Write-CIEMLog -Message "Calling Connect-AzAccount with device code..." -Severity INFO -Component 'Connect-CIEMAzure'
            Write-Verbose "Connecting with device code authentication"
            Connect-AzAccount @connectParams -ErrorAction Stop | Out-Null
            Write-CIEMLog -Message "Device code authentication completed successfully" -Severity INFO -Component 'Connect-CIEMAzure'
        }
        'Interactive' {
            Write-CIEMLog -Message "Processing Interactive authentication..." -Severity INFO -Component 'Connect-CIEMAzure'
            $connectParams = @{}
            if ($authConfig.tenantId) {
                $connectParams.TenantId = $authConfig.tenantId
                Write-CIEMLog -Message "Using tenant: $($authConfig.tenantId)" -Severity DEBUG -Component 'Connect-CIEMAzure'
            }
            Write-CIEMLog -Message "Calling Connect-AzAccount interactively..." -Severity INFO -Component 'Connect-CIEMAzure'
            Write-Verbose "Connecting with interactive authentication"
            Connect-AzAccount @connectParams -ErrorAction Stop | Out-Null
            Write-CIEMLog -Message "Interactive authentication completed successfully" -Severity INFO -Component 'Connect-CIEMAzure'
        }
        default {
            Write-CIEMLog -Message "Unknown authentication method: $authMethod" -Severity ERROR -Component 'Connect-CIEMAzure'
            throw "Unknown authentication method '$authMethod'. Valid values: ServicePrincipalSecret, ServicePrincipalCertificate, ManagedIdentity, DeviceCode, Interactive"
        }
    }

    Write-CIEMLog -Message "Getting Azure context..." -Severity DEBUG -Component 'Connect-CIEMAzure'
    $context = Get-AzContext -ErrorAction Stop
    Write-CIEMLog -Message "Azure context obtained: Account=$($context.Account.Id), Tenant=$($context.Tenant.Id)" -Severity INFO -Component 'Connect-CIEMAzure'

    # Get all accessible subscriptions
    Write-CIEMLog -Message "Getting accessible subscriptions..." -Severity DEBUG -Component 'Connect-CIEMAzure'
    $subscriptions = Get-AzSubscription -TenantId $context.Tenant.Id -ErrorAction SilentlyContinue
    Write-CIEMLog -Message "Found $($subscriptions.Count) subscriptions" -Severity DEBUG -Component 'Connect-CIEMAzure'

    # Filter to configured subscriptions if specified
    $subscriptionFilter = $script:Config.azure.subscriptionFilter
    if ($subscriptionFilter -and $subscriptionFilter.Count -gt 0) {
        Write-CIEMLog -Message "Applying subscription filter: $($subscriptionFilter -join ', ')" -Severity DEBUG -Component 'Connect-CIEMAzure'
        $subscriptions = $subscriptions | Where-Object { $subscriptionFilter -contains $_.Id }
    }

    $subscriptionIds = @($subscriptions | Select-Object -ExpandProperty Id)

    if ($subscriptionIds.Count -eq 0) {
        Write-CIEMLog -Message "No accessible subscriptions found in tenant $($context.Tenant.Id)" -Severity WARNING -Component 'Connect-CIEMAzure'
        Write-Warning "No accessible subscriptions found in tenant $($context.Tenant.Id)"
    }
    else {
        Write-CIEMLog -Message "Accessible subscriptions: $($subscriptionIds.Count)" -Severity INFO -Component 'Connect-CIEMAzure'
        Write-Verbose "Found $($subscriptionIds.Count) accessible subscription(s)"
    }

    # Determine account type
    $accountType = switch ($context.Account.Type) {
        'User' { 'User' }
        'ServicePrincipal' { 'ServicePrincipal' }
        'ManagedService' { 'ManagedIdentity' }
        default { $context.Account.Type }
    }
    Write-CIEMLog -Message "Account type determined: $accountType" -Severity DEBUG -Component 'Connect-CIEMAzure'

    Write-CIEMLog -Message "Connect-CIEMAzure completed successfully" -Severity INFO -Component 'Connect-CIEMAzure'

    [PSCustomObject]@{
        TenantId        = $context.Tenant.Id
        SubscriptionIds = $subscriptionIds
        AccountId       = $context.Account.Id
        AccountType     = $accountType
        ConnectedAt     = Get-Date
    }
}