Public/Invoke-Reconnaissance.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-Reconnaissance {
    [CmdletBinding()]
    param(
        [ValidateSet('All', 'DomainForest', 'Trusts', 'PrivilegedAccounts', 'PasswordPolicy',
                     'Kerberos', 'ACLDelegation', 'GroupPolicy', 'LogonScripts',
                     'CertificateServices', 'StaleObjects', 'Network', 'TierZero', 'Logging', 'Tradecraft')]
        [string[]]$Categories = @('All'),

        [string]$Server,
        [pscredential]$Credential,

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

        [string]$NtdsPath,
        [string]$WeakPasswordList,

        [int]$InactiveDays = 90,
        [int]$PasswordAgeDays = 365
    )

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

        # Resolve AD credentials from vault
        $adRef = $missionCfg.Config.credentials.references.activeDirectory
        if ($adRef -and $adRef.type -eq 'serviceAccount' -and -not $PSBoundParameters.ContainsKey('Credential')) {
            try {
                $Credential = Get-GuerrillaCredential -VaultKey ($adRef.vaultKey ?? 'GUERRILLA_AD_CREDENTIAL') -VaultName $vaultName
            } catch {
                Write-Verbose "AD credential not found in vault — will use current user context."
            }
        }

        # Apply categories from mission config
        if (-not $PSBoundParameters.ContainsKey('Categories')) {
            $adEnv = $missionCfg.EnabledEnvironments['activeDirectory']
            if ($adEnv -and $adEnv.audit -and $adEnv.audit.categories) {
                $missionCats = @($adEnv.audit.categories.GetEnumerator() | Where-Object { $_.Value } | ForEach-Object { $_.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 ($cfgPath -and (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' }

    # --- Operation header ---
    if (-not $Quiet) {
        $targetLabel = if ($Server) { $Server } else { 'Current Domain' }
        Write-OperationHeader -Operation 'RECONNAISSANCE AUDIT' -Mode 'AD Security' -Target $targetLabel -DaysBack 0
    }

    # --- Connect to AD ---
    if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Connecting to Active Directory' }
    try {
        $connParams = @{}
        if ($Server) { $connParams['Server'] = $Server }
        if ($Credential) { $connParams['Credential'] = $Credential }
        $connection = New-LdapConnection @connParams
    } catch {
        throw "Failed to connect to Active Directory: $_"
    }

    $domainName = $connection.DomainDN -replace 'DC=', '' -replace ',', '.'
    if (-not $Quiet) {
        Write-ProgressLine -Phase RECON -Message "Connected to $domainName"
    }

    # --- Collect data ---
    if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Beginning data collection' }
    $auditData = Get-ReconnaissanceData `
        -Connection $connection `
        -Categories $Categories `
        -InactiveDays $InactiveDays `
        -PasswordAgeDays $PasswordAgeDays `
        -NtdsPath $NtdsPath `
        -WeakPasswordList $WeakPasswordList `
        -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 RECON -Message 'Evaluating security checks' }
    $allFindings = [System.Collections.Generic.List[PSCustomObject]]::new()

    $categoryMap = @{
        DomainForest       = 'Invoke-ADDomainForestChecks'
        Trusts             = 'Invoke-ADTrustChecks'
        PrivilegedAccounts = 'Invoke-ADPrivilegedAccountChecks'
        PasswordPolicy     = 'Invoke-ADPasswordPolicyChecks'
        Kerberos           = 'Invoke-ADKerberosChecks'
        ACLDelegation      = 'Invoke-ADAclDelegationChecks'
        GroupPolicy        = 'Invoke-ADGroupPolicyChecks'
        LogonScripts       = 'Invoke-ADLogonScriptChecks'
        CertificateServices = 'Invoke-ADCertificateServicesChecks'
        StaleObjects       = 'Invoke-ADStaleObjectChecks'
        Network            = 'Invoke-ADNetworkChecks'
        TierZero           = 'Invoke-TierZeroChecks'
        Logging            = 'Invoke-ADLoggingChecks'
        Tradecraft         = 'Invoke-ADTradecraftChecks'
    }

    $categoriesToRun = if ($Categories -contains 'All') { $categoryMap.Keys } else { $Categories }

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

        if (Get-Command $funcName -ErrorAction SilentlyContinue) {
            if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message $cat }
            try {
                $catFindings = & $funcName -AuditData $auditData
                foreach ($f in @($catFindings)) {
                    $allFindings.Add($f)
                }
                $passed = @($catFindings | Where-Object Status -eq 'PASS').Count
                $failed = @($catFindings | Where-Object Status -eq 'FAIL').Count
                if (-not $Quiet) {
                    Write-ProgressLine -Phase RECON -Message $cat -Detail "P:$passed F:$failed / $($catFindings.Count)"
                }
            } catch {
                Write-Warning "Category $cat failed: $_"
            }
        }
    }

    # --- Score ---
    if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Calculating posture score' }
    $scoreResult = Get-AuditPostureScore -Findings @($allFindings)
    $overallScore = $scoreResult.OverallScore
    $scoreLabel = Get-FortificationScoreLabel -Score $overallScore

    # --- Delta comparison ---
    $delta = $null
    if (-not $NoDelta) {
        $stateDir = Split-Path $cfgPath -Parent
        $statePath = Join-Path $stateDir 'reconnaissance-state.json'
        if (Test-Path $statePath) {
            if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Comparing against previous scan' }
            try {
                $previousState = Get-Content -Path $statePath -Raw | ConvertFrom-Json -AsHashtable
                $delta = Compare-FortificationState -CurrentFindings @($allFindings) -PreviousState $previousState
            } catch {
                Write-Verbose "Delta comparison failed: $_"
            }
        }
    }

    # --- Severity counts ---
    $failFindings = @($allFindings | Where-Object Status -eq 'FAIL')
    $critCount = @($failFindings | Where-Object Severity -eq 'Critical').Count
    $highCount = @($failFindings | Where-Object Severity -eq 'High').Count
    $medCount  = @($failFindings | Where-Object Severity -eq 'Medium').Count
    $lowCount  = @($failFindings | Where-Object Severity -eq 'Low').Count
    $passCount = @($allFindings | Where-Object Status -eq 'PASS').Count
    $failCount = $failFindings.Count
    $warnCount = @($allFindings | Where-Object Status -eq 'WARN').Count
    $skipCount = @($allFindings | Where-Object Status -in @('SKIP', 'ERROR')).Count

    # --- Console report ---
    if (-not $Quiet) {
        Write-ReconnaissanceReport `
            -OverallScore $overallScore `
            -ScoreLabel $scoreLabel `
            -CategoryScores $scoreResult.CategoryScores `
            -TotalChecks $allFindings.Count `
            -PassCount $passCount `
            -FailCount $failCount `
            -WarnCount $warnCount `
            -SkipCount $skipCount `
            -CriticalCount $critCount `
            -HighCount $highCount `
            -MediumCount $medCount `
            -LowCount $lowCount `
            -TopFindings @($allFindings) `
            -DomainName $domainName
    }

    # --- Generate reports ---
    $csvPath = $null; $htmlPath = $null; $jsonPath = $null
    if (-not $NoReports) {
        if (-not (Test-Path $outDir)) {
            New-Item -Path $outDir -ItemType Directory -Force | Out-Null
        }
        $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'

        $genCsv  = if ($config -and $null -ne $config.output.generateCsv) { $config.output.generateCsv } else { $true }
        $genHtml = if ($config -and $null -ne $config.output.generateHtml) { $config.output.generateHtml } else { $true }
        $genJson = if ($config -and $null -ne $config.output.generateJson) { $config.output.generateJson } else { $true }

        if ($genCsv) {
            $csvPath = Join-Path $outDir "reconnaissance_report_$timestamp.csv"
            Export-ReconnaissanceReportCsv -Findings @($allFindings) -FilePath $csvPath
            if (-not $Quiet) { Write-ProgressLine -Phase REPORTING -Message 'CSV report' -Detail $csvPath }
        }
        if ($genHtml) {
            $htmlPath = Join-Path $outDir "reconnaissance_report_$timestamp.html"
            Export-ReconnaissanceReportHtml `
                -Findings @($allFindings) `
                -OverallScore $overallScore `
                -ScoreLabel $scoreLabel `
                -CategoryScores $scoreResult.CategoryScores `
                -DomainName $domainName `
                -Delta $delta `
                -FilePath $htmlPath
            if (-not $Quiet) { Write-ProgressLine -Phase REPORTING -Message 'HTML report' -Detail $htmlPath }
        }
        if ($genJson) {
            $jsonPath = Join-Path $outDir "reconnaissance_report_$timestamp.json"
            Export-ReconnaissanceReportJson `
                -Findings @($allFindings) `
                -OverallScore $overallScore `
                -ScoreLabel $scoreLabel `
                -CategoryScores $scoreResult.CategoryScores `
                -DomainName $domainName `
                -ScanId $scanId `
                -Delta $delta `
                -FilePath $jsonPath
            if (-not $Quiet) { Write-ProgressLine -Phase REPORTING -Message 'JSON report' -Detail $jsonPath }
        }
    }

    # --- Save state for future delta ---
    if (-not $NoDelta) {
        $stateDir = Split-Path $cfgPath -Parent
        if (-not (Test-Path $stateDir)) {
            New-Item -Path $stateDir -ItemType Directory -Force | Out-Null
        }
        $statePath = Join-Path $stateDir 'reconnaissance-state.json'
        $newState = @{
            schemaVersion     = 1
            lastScanTimestamp = [datetime]::UtcNow.ToString('o')
            lastScanId        = $scanId
            overallScore      = $overallScore
            findings          = @($allFindings | ForEach-Object {
                @{
                    checkId      = $_.CheckId
                    status       = $_.Status
                    currentValue = $_.CurrentValue
                    orgUnitPath  = $_.OrgUnitPath
                    severity     = $_.Severity
                    category     = $_.Category
                }
            })
            categoryScores    = $scoreResult.CategoryScores
        }
        $newState | ConvertTo-Json -Depth 5 | Set-Content -Path $statePath -Encoding UTF8
    }

    # --- Emit result object ---
    $result = [PSCustomObject]@{
        PSTypeName     = 'PSGuerrilla.ReconResult'
        ScanId         = $scanId
        Timestamp      = $scanStart
        DomainName     = $domainName
        OverallScore   = $overallScore
        ScoreLabel     = $scoreLabel
        CategoryScores = $scoreResult.CategoryScores
        TotalChecks    = $allFindings.Count
        PassCount      = $passCount
        FailCount      = $failCount
        WarnCount      = $warnCount
        SkipCount      = $skipCount
        CriticalCount  = $critCount
        HighCount      = $highCount
        MediumCount    = $medCount
        LowCount       = $lowCount
        Findings       = @($allFindings)
        Delta          = $delta
        HtmlReportPath = $htmlPath
        CsvReportPath  = $csvPath
        JsonReportPath = $jsonPath
    }

    return $result
}