Public/Invoke-Infiltration.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Invoke-Infiltration {
    <#
    .SYNOPSIS
        Performs a comprehensive Entra ID, Azure, Intune, and M365 security assessment.
 
    .DESCRIPTION
        Invoke-Infiltration executes a thorough audit of Microsoft cloud identity and device
        management configuration. It evaluates Conditional Access policies, authentication methods,
        privileged identity management, application security, federation configuration, tenant
        settings, Azure IAM, Intune endpoint management, and M365 service configurations.
 
        Emulates: ScubaGear, Maester (EIDSCA), EntraFalcon, Mandiant Azure AD Investigator,
        M365SAT, ROADtools, AADInternals (defensive), Azure AD Assessment, and others.
 
    .PARAMETER Categories
        Specifies which audit categories to run. Default is 'All'.
        Valid values: All, ConditionalAccess, AuthenticationMethods, PIM, Applications,
        Federation, TenantConfig, AzureIAM, Intune, M365Services
 
    .PARAMETER TenantId
        The Azure AD / Entra ID tenant ID.
 
    .PARAMETER ClientId
        The application (client) ID for authentication.
 
    .PARAMETER CertificateThumbprint
        Certificate thumbprint for app-only authentication.
 
    .PARAMETER ClientSecret
        Client secret for app-only authentication.
 
    .PARAMETER DeviceCode
        Use device code flow for interactive authentication.
 
    .PARAMETER OutputDirectory
        Directory for report output. Default: per-user data dir + /PSGuerrilla/Reports
        (Windows: $env:APPDATA; macOS: ~/Library/Application Support; Linux: $XDG_CONFIG_HOME or ~/.config)
 
    .PARAMETER NoReports
        Skip report generation.
 
    .PARAMETER NoDelta
        Skip delta comparison with previous scan.
 
    .PARAMETER Quiet
        Suppress console output.
 
    .PARAMETER ConfigPath
        Path to PSGuerrilla configuration file.
 
    .PARAMETER ConfigFile
        Path to a guerrilla-config.json mission config generated by the PSGuerrilla website.
        When provided, resolves credentials from the SecretManagement vault and applies category
        filtering from the mission config.
 
    .EXAMPLE
        Invoke-Infiltration -TenantId 'contoso.onmicrosoft.com' -ClientId $appId -ClientSecret $secret
 
    .EXAMPLE
        Invoke-Infiltration -TenantId $tenantId -ClientId $appId -DeviceCode -Categories ConditionalAccess, PIM
    #>

    [CmdletBinding()]
    param(
        [ValidateSet('All', 'ConditionalAccess', 'AuthenticationMethods', 'PIM', 'Applications',
                     'Federation', 'TenantConfig', 'AzureIAM', 'Intune', 'M365Services')]
        [string[]]$Categories = @('All'),

        [string]$TenantId,

        [string]$ClientId,

        [string]$CertificateThumbprint,
        [securestring]$ClientSecret,
        [switch]$DeviceCode,

        [string]$OutputDirectory,
        [switch]$NoReports,
        [switch]$NoDelta,
        [switch]$Quiet,
        [Alias('RuntimeConfig')]
        [string]$ConfigPath,
        [Alias('MissionConfig')]
        [string]$ConfigFile
    )

    # --- Resolve mission config (guerrilla-config.json) ---
    if ($ConfigFile) {
        $missionCfg = Read-MissionConfig -Path $ConfigFile
        $vaultName = $missionCfg.VaultName

        # Resolve Microsoft Graph credentials from vault
        $graphRef = $missionCfg.Config.credentials.references.microsoftGraph
        if ($graphRef) {
            if ($graphRef.tenantIdVaultKey -and -not $PSBoundParameters.ContainsKey('TenantId')) {
                try {
                    $TenantId = Get-GuerrillaCredential -VaultKey $graphRef.tenantIdVaultKey -VaultName $vaultName
                } catch {
                    Write-Warning "Failed to resolve TenantId from vault: $_"
                }
            }
            if ($graphRef.clientIdVaultKey -and -not $PSBoundParameters.ContainsKey('ClientId')) {
                try {
                    $ClientId = Get-GuerrillaCredential -VaultKey $graphRef.clientIdVaultKey -VaultName $vaultName
                } catch {
                    Write-Warning "Failed to resolve ClientId from vault: $_"
                }
            }
            if ($graphRef.vaultKey -and -not $PSBoundParameters.ContainsKey('CertificateThumbprint') -and -not $PSBoundParameters.ContainsKey('ClientSecret')) {
                try {
                    $secretVal = Get-GuerrillaCredential -VaultKey $graphRef.vaultKey -VaultName $vaultName
                    if ($graphRef.authMethod -eq 'certificate') {
                        $CertificateThumbprint = $secretVal
                    } else {
                        $ClientSecret = $secretVal | ConvertTo-SecureString -AsPlainText -Force
                    }
                } catch {
                    Write-Warning "Failed to resolve Graph auth credential from vault: $_"
                }
            }
        }

        # Apply categories from mission config (combine entraAzure + m365 + intune)
        if (-not $PSBoundParameters.ContainsKey('Categories')) {
            $missionCats = [System.Collections.Generic.List[string]]::new()
            foreach ($envKey in @('entraAzure', 'm365', 'intune')) {
                $envCfg = $missionCfg.EnabledEnvironments[$envKey]
                if ($envCfg -and $envCfg.audit -and $envCfg.audit.categories) {
                    foreach ($entry in $envCfg.audit.categories.GetEnumerator()) {
                        if ($entry.Value -and $entry.Key -notin $missionCats) {
                            $missionCats.Add($entry.Key)
                        }
                    }
                }
            }
            if ($missionCats.Count -gt 0) { $Categories = @($missionCats) }
        }
    }

    $scanId = [guid]::NewGuid().ToString()
    $scanStart = [datetime]::UtcNow

    # --- Load config ---
    $cfgPath = if ($ConfigPath) { $ConfigPath } else { $script:ConfigPath }
    $config = $null
    if (Test-Path $cfgPath) {
        $config = Get-Content -Path $cfgPath -Raw | ConvertFrom-Json -AsHashtable
    }

    $outDir = if ($OutputDirectory) { $OutputDirectory }
              elseif ($config -and $config.output.directory) { $config.output.directory }
              else { Join-Path (Get-PSGuerrillaDataRoot) 'Reports' }

    # Merge TenantId / ClientId from config.json if not already set
    if (-not $TenantId -and $config -and $config.entra.tenantId) {
        $TenantId = $config.entra.tenantId
    }
    if (-not $ClientId -and $config -and $config.entra.clientId) {
        $ClientId = $config.entra.clientId
    }

    # Validate required parameters
    if (-not $TenantId) { throw 'TenantId is required. Provide -TenantId, -ConfigFile, or set entra.tenantId in config.' }
    if (-not $ClientId) { throw 'ClientId is required. Provide -ClientId, -ConfigFile, or set entra.clientId in config.' }

    # --- Operation header ---
    if (-not $Quiet) {
        Write-OperationHeader -Operation 'INFILTRATION AUDIT' -Mode 'Entra / Azure / M365' `
            -Target $TenantId -DaysBack 0
    }

    # --- Authenticate to Microsoft Graph ---
    if (-not $Quiet) {
        Write-ProgressLine -Phase INFILTRATE -Message 'Authenticating to Microsoft Graph'
    }

    $authParams = @{
        TenantId = $TenantId
        ClientId = $ClientId
    }
    if ($CertificateThumbprint) { $authParams['CertificateThumbprint'] = $CertificateThumbprint }
    if ($ClientSecret) { $authParams['ClientSecret'] = $ClientSecret }
    if ($DeviceCode) { $authParams['DeviceCode'] = $true }

    try {
        $graphToken = Get-GraphAccessToken @authParams `
            -Scopes @('https://graph.microsoft.com/.default')
    } catch {
        throw "Failed to authenticate to Microsoft Graph: $_"
    }

    if (-not $Quiet) {
        Write-ProgressLine -Phase INFILTRATE -Message 'Authenticated to Microsoft Graph'
    }

    # --- Authenticate to Azure Resource Manager (if Azure IAM checks needed) ---
    $armToken = $null
    $categoriesToRun = if ($Categories -contains 'All') {
        @('ConditionalAccess', 'AuthenticationMethods', 'PIM', 'Applications',
          'Federation', 'TenantConfig', 'AzureIAM', 'Intune', 'M365Services')
    } else { $Categories }

    if ('AzureIAM' -in $categoriesToRun) {
        if (-not $Quiet) {
            Write-ProgressLine -Phase INFILTRATE -Message 'Authenticating to Azure Resource Manager'
        }
        try {
            $armToken = Get-GraphAccessToken @authParams `
                -Scopes @('https://management.azure.com/.default') `
                -ResourceUrl 'https://management.azure.com'
        } catch {
            Write-Warning "ARM authentication failed — Azure IAM checks will be skipped: $_"
        }
    }

    # --- Collect data ---
    if (-not $Quiet) {
        Write-ProgressLine -Phase INFILTRATE -Message 'Beginning data collection'
    }

    $auditData = Get-InfiltrationData `
        -AccessToken $graphToken `
        -ArmAccessToken $armToken `
        -Categories $Categories `
        -Quiet:$Quiet

    # Report collection errors
    if ($auditData.Errors.Count -gt 0 -and -not $Quiet) {
        Write-ProgressLine -Phase INFO -Message "Data collection had $($auditData.Errors.Count) error(s)"
        foreach ($errKey in $auditData.Errors.Keys) {
            Write-ProgressLine -Phase INFO -Message " $errKey" -Detail $auditData.Errors[$errKey]
        }
    }

    # --- Run checks ---
    if (-not $Quiet) {
        Write-ProgressLine -Phase INFILTRATE -Message 'Evaluating security checks'
    }

    $allFindings = [System.Collections.Generic.List[PSCustomObject]]::new()

    $categoryMap = @{
        ConditionalAccess     = 'Invoke-EntraCAChecks'
        AuthenticationMethods = 'Invoke-EntraAuthChecks'
        PIM                   = 'Invoke-EntraPIMChecks'
        Applications          = 'Invoke-EntraAppChecks'
        Federation            = 'Invoke-EntraFedChecks'
        TenantConfig          = 'Invoke-EntraTenantChecks'
        AzureIAM              = 'Invoke-AzureIAMChecks'
        Intune                = 'Invoke-IntuneChecks'
        M365Services          = @(
            'Invoke-M365ExchangeChecks'
            'Invoke-M365SharePointChecks'
            'Invoke-M365TeamsChecks'
            'Invoke-M365DefenderChecks'
            'Invoke-M365AuditChecks'
            'Invoke-M365PowerPlatformChecks'
        )
    }

    foreach ($cat in $categoriesToRun) {
        $funcNames = $categoryMap[$cat]
        if (-not $funcNames) { continue }

        # Handle M365Services which maps to multiple check functions
        foreach ($funcName in @($funcNames)) {
            if (-not $Quiet) {
                Write-ProgressLine -Phase INFILTRATE -Message "Running $funcName"
            }

            if (Get-Command $funcName -ErrorAction SilentlyContinue) {
                try {
                    $findings = & $funcName -AuditData $auditData
                    foreach ($f in $findings) { $allFindings.Add($f) }
                } catch {
                    Write-Warning "Check category '$funcName' failed: $_"
                }
            } else {
                if (-not $Quiet) {
                    Write-ProgressLine -Phase INFO -Message "$funcName not available (module not loaded)"
                }
            }
        }
    }

    # --- Score ---
    $score = Get-AuditPostureScore -Findings @($allFindings)

    # --- Summary ---
    $scanEnd = [datetime]::UtcNow
    $scanDuration = $scanEnd - $scanStart

    $result = [PSCustomObject]@{
        PSTypeName     = 'PSGuerrilla.InfiltrationResult'
        ScanId         = $scanId
        TenantId       = $TenantId
        ScanStart      = $scanStart
        ScanEnd        = $scanEnd
        Duration       = $scanDuration
        Categories     = $categoriesToRun
        Findings       = @($allFindings)
        Score          = $score
        DataErrors     = $auditData.Errors
        AuditData      = $auditData
    }

    # --- Console output ---
    if (-not $Quiet) {
        Write-InfiltrationReport -Result $result
    }

    # --- Reports ---
    if (-not $NoReports) {
        if (-not (Test-Path $outDir)) {
            New-Item -Path $outDir -ItemType Directory -Force | Out-Null
        }

        $timestamp = $scanStart.ToString('yyyyMMdd-HHmmss')
        $tenantLabel = $TenantId -replace '[^a-zA-Z0-9]', '_'

        # Delta comparison
        $previousFindings = $null
        if (-not $NoDelta) {
            $previousFile = Get-ChildItem -Path $outDir -Filter "infiltration-$tenantLabel-*.json" `
                -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
            if ($previousFile) {
                try {
                    $previousData = Get-Content -Path $previousFile.FullName -Raw | ConvertFrom-Json
                    $previousFindings = $previousData.Findings
                    if (-not $Quiet) {
                        Write-ProgressLine -Phase INFILTRATE -Message "Delta comparison with $($previousFile.Name)"
                    }
                } catch {
                    Write-Verbose "Could not load previous scan for delta: $_"
                }
            }
        }

        # Export reports
        $baseName = "infiltration-$tenantLabel-$timestamp"

        if (-not $Quiet) {
            Write-ProgressLine -Phase INFILTRATE -Message 'Generating reports'
        }

        try {
            Export-InfiltrationReportHtml -Result $result -OutputPath (Join-Path $outDir "$baseName.html") `
                -PreviousFindings $previousFindings
        } catch {
            Write-Warning "HTML report generation failed: $_"
        }

        try {
            Export-InfiltrationReportCsv -Result $result -OutputPath (Join-Path $outDir "$baseName.csv")
        } catch {
            Write-Warning "CSV report generation failed: $_"
        }

        try {
            Export-InfiltrationReportJson -Result $result -OutputPath (Join-Path $outDir "$baseName.json")
        } catch {
            Write-Warning "JSON report generation failed: $_"
        }

        if (-not $Quiet) {
            Write-ProgressLine -Phase INFILTRATE -Message "Reports saved to $outDir"
        }
    }

    # --- Complete ---
    if (-not $Quiet) {
        Write-ProgressLine -Phase INFILTRATE -Message "Infiltration audit complete in $([Math]::Round($scanDuration.TotalSeconds, 1))s"
    }

    return $result
}