Private/AD/Checks/Invoke-ADAttackPathChecks.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-ADAttackPathChecks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$AuditData
    )

    $checkDefs = Get-AuditCategoryDefinitions -Category 'ADAttackPathChecks'
    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($check in $checkDefs.checks) {
        $funcName = "Test-Recon$($check.id -replace '-', '')"
        if (Get-Command $funcName -ErrorAction SilentlyContinue) {
            try {
                $finding = & $funcName -AuditData $AuditData -CheckDefinition $check
                if ($finding) { $findings.Add($finding) }
            } catch {
                $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' `
                    -CurrentValue "Check failed: $_"))
            }
        } else {
            $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' `
                -CurrentValue 'Check not yet implemented'))
        }
    }

    return @($findings)
}

# ── ADPATH-001: Escalation Paths to Tier-0 ─────────────────────────────────
function Test-ReconADPATH001 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )

    $analysis = Get-ADAttackPath -AuditData $AuditData

    if (-not $analysis.DataAvailable) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'ACL data not available — run with the ACLDelegation (or All) category enabled to compute attack paths'
    }

    $paths = @($analysis.Paths)
    # Expected paths are by-design Tier-0 service accounts (Azure AD Connect MSOL_*),
    # already tracked by ADTIER-001 — they don't constitute a finding on their own.
    $genuine  = @($paths | Where-Object { -not $_.Expected })
    $expected = @($paths | Where-Object { $_.Expected })

    if ($genuine.Count -eq 0) {
        $cv = 'No non-default principals have control over Tier-0 objects — no one-hop escalation paths found'
        if ($expected.Count -gt 0) {
            $cv += " ($($expected.Count) expected service-account path(s) — e.g. Azure AD Connect — are tracked by ADTIER-001)"
        }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue $cv `
            -Details @{ PathCount = 0; ExpectedCount = $expected.Count; Paths = @($paths) }
    }

    $nonPriv = @($genuine | Where-Object { -not $_.SourceIsPrivileged })
    $preview = @($genuine | Select-Object -First 6 | ForEach-Object { $_.Path }) -join ' | '

    $currentValue = "$($genuine.Count) escalation path(s) to Tier-0"
    if ($nonPriv.Count -gt 0) {
        $currentValue += " — $($nonPriv.Count) from NON-privileged principals (highest risk)"
    }
    if ($expected.Count -gt 0) {
        $currentValue += "; plus $($expected.Count) expected service-account path(s) (see ADTIER-001)"
    }
    $currentValue += ": $preview"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
        -CurrentValue $currentValue `
        -Details @{
            PathCount          = $genuine.Count
            NonPrivilegedCount = $nonPriv.Count
            ExpectedCount      = $expected.Count
            AffectedLabel      = 'Escalation paths to Tier-0'
            AffectedItems      = @($genuine | ForEach-Object { $_.Path })
            Paths              = @($paths)
        }
}