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

    $checkDefs = Get-AuditCategoryDefinitions -Category 'ADLoggingChecks'
    $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)
}

# Helper: shared with ADNetworkChecks but redefined locally so this file doesn't depend
# on dot-source ordering. PowerShell hashtables and OrderedDictionary support .Value on
# the parser's Entry shape; we just want the raw string value as an int.
function ConvertTo-LogPolicyRegInt {
    param($Entry)
    if (-not $Entry) { return $null }
    $raw = "$($Entry.Value)".Trim()
    if (-not $raw) { return $null }
    if ($raw -match '^0x([0-9a-fA-F]+)$') {
        try { return [int]([Convert]::ToInt32($Matches[1], 16)) } catch { return $null }
    }
    if ($raw -match '^-?\d+$') {
        try { return [int]$raw } catch { return $null }
    }
    return $null
}

# Helper: produce a "Registry.pol not parsed, verify manually" WARN. Many of the logging
# settings live in admin templates which this MVP doesn't read.
function New-LogManualVerifyFinding {
    param(
        [Parameter(Mandatory)][hashtable]$CheckDefinition,
        [Parameter(Mandatory)][string]$RegistryPath,
        [Parameter(Mandatory)][string]$ExpectedValueDescription
    )
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue ("Could not verify from SYSVOL security-settings INI. This setting is typically delivered via administrative template (Registry.pol) which this MVP does not parse. Verify on a member host: Get-ItemProperty '$RegistryPath'. Expected: $ExpectedValueDescription.") `
        -Details @{ ManualVerifyPath = $RegistryPath; Caveat = 'Registry.pol not parsed in MVP' }
}

# ── ADLOG-001: Advanced Audit Policy Configured ────────────────────────────
# Detected by checking whether audit.csv exists in the Default Domain Controllers Policy
# SYSVOL folder. The collector doesn't read this file currently, so we derive the DNS
# name and check ourselves. (We deliberately don't extend the collector for one check.)
function Test-ReconADLOG001 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )
    $conn = $AuditData.Connection
    if (-not $conn) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Connection metadata not present in AuditData.'
    }
    $domainDns = ($conn.DomainDN -replace '^DC=', '' -replace ',DC=', '.').ToLower()
    $ddcpGuid = '{6AC1786C-016F-11D2-945F-00C04fB984F9}'
    $auditCsvPath = "\\$domainDns\SYSVOL\$domainDns\Policies\$ddcpGuid\MACHINE\Microsoft\Windows NT\Audit\Audit.csv"

    try {
        if (Test-Path -LiteralPath $auditCsvPath -ErrorAction Stop) {
            $lineCount = 0
            try {
                $lineCount = @(Get-Content -LiteralPath $auditCsvPath -ErrorAction Stop |
                    Where-Object { $_ -and -not $_.StartsWith('Machine Name') }).Count
            } catch { }
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
                -CurrentValue "Advanced Audit Policy configured in Default Domain Controllers Policy (audit.csv present, $lineCount subcategory row(s))" `
                -Details @{ AuditCsvPath = $auditCsvPath; Rows = $lineCount }
        }
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'No audit.csv in Default Domain Controllers Policy — legacy nine-category audit policy is in use, which is too coarse for modern investigations' `
            -Details @{ AuditCsvPath = $auditCsvPath }
    } catch {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue "Could not test SYSVOL path: $($_.Exception.Message)"
    }
}

# ── ADLOG-002: PowerShell Script Block Logging ─────────────────────────────
function Test-ReconADLOG002 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )
    $net = $AuditData.Network
    if ($net -and $net.DefaultDomainPolicy) {
        $val = $null
        $entry = $net.DefaultDomainPolicy.Registry['MACHINE\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging\EnableScriptBlockLogging']
        if ($entry) { $val = ConvertTo-LogPolicyRegInt -Entry $entry }
        if ($val -eq 1) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
                -CurrentValue 'PowerShell Script Block Logging enabled via Default Domain Policy security settings (EnableScriptBlockLogging = 1)' `
                -Details @{ ConfiguredValue = 1 }
        }
        if ($val -eq 0) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
                -CurrentValue 'PowerShell Script Block Logging explicitly disabled in Default Domain Policy (EnableScriptBlockLogging = 0)' `
                -Details @{ ConfiguredValue = 0 }
        }
    }
    return New-LogManualVerifyFinding -CheckDefinition $CheckDefinition `
        -RegistryPath 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' `
        -ExpectedValueDescription 'EnableScriptBlockLogging = 1'
}

# ── ADLOG-003: PowerShell Module Logging ───────────────────────────────────
function Test-ReconADLOG003 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )
    $net = $AuditData.Network
    if ($net -and $net.DefaultDomainPolicy) {
        $val = $null
        $entry = $net.DefaultDomainPolicy.Registry['MACHINE\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\EnableModuleLogging']
        if ($entry) { $val = ConvertTo-LogPolicyRegInt -Entry $entry }
        if ($val -eq 1) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
                -CurrentValue 'PowerShell Module Logging enabled via Default Domain Policy security settings' `
                -Details @{ ConfiguredValue = 1 }
        }
    }
    return New-LogManualVerifyFinding -CheckDefinition $CheckDefinition `
        -RegistryPath 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' `
        -ExpectedValueDescription 'EnableModuleLogging = 1 and a ModuleNames list (* recommended)'
}

# ── ADLOG-004: Process Creation Auditing with Command Line ─────────────────
function Test-ReconADLOG004 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )
    $net = $AuditData.Network
    if ($net -and $net.DefaultDomainPolicy) {
        $entry = $net.DefaultDomainPolicy.Registry['MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit\ProcessCreationIncludeCmdLine_Enabled']
        $val = if ($entry) { ConvertTo-LogPolicyRegInt -Entry $entry } else { $null }
        if ($val -eq 1) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
                -CurrentValue 'Process creation events include command line (ProcessCreationIncludeCmdLine_Enabled = 1)' `
                -Details @{ ConfiguredValue = 1 }
        }
    }
    return New-LogManualVerifyFinding -CheckDefinition $CheckDefinition `
        -RegistryPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit' `
        -ExpectedValueDescription 'ProcessCreationIncludeCmdLine_Enabled = 1, and Advanced Audit Policy "Audit Process Creation" = Success'
}

# ── ADLOG-005: Microsoft Defender Tamper Protection Policy ─────────────────
function Test-ReconADLOG005 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Defender Tamper Protection state is set in the MDE cloud portal (Settings > Endpoints > Advanced features) and not visible from SYSVOL. Verify out-of-band. Also enumerate exclusion entries: every entry under HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions on a representative endpoint is a hole an attacker will find — keep the list tight.' `
        -Details @{ Caveat = 'Tamper Protection not detectable from AD'; ManualPortal = 'security.microsoft.com > Settings > Endpoints > Advanced features' }
}

# ── ADLOG-006: Windows Event Forwarding SubscriptionManager ────────────────
function Test-ReconADLOG006 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )
    return New-LogManualVerifyFinding -CheckDefinition $CheckDefinition `
        -RegistryPath 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\EventLog\EventForwarding\SubscriptionManager' `
        -ExpectedValueDescription 'A SubscriptionManager URL pointing at your WEF collector (e.g. Server=http://wec-server.domain.tld:5985/wsman/SubscriptionManager/WEC,Refresh=60)'
}

# ── ADLOG-007: Sysmon Deployment Indicator ─────────────────────────────────
function Test-ReconADLOG007 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$AuditData,
        [Parameter(Mandatory)][hashtable]$CheckDefinition
    )
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Sysmon presence is not detectable from SYSVOL / LDAP. Verify on representative hosts: Get-CimInstance Win32_Service -Filter "Name=''Sysmon64''". If you have a WEF collector, query for "Microsoft-Windows-Sysmon/Operational" events in the last 7 days.' `
        -Details @{ Caveat = 'Endpoint-level service state not detectable from AD'; ManualVerify = "Get-CimInstance Win32_Service -Filter `"Name='Sysmon64'`"" }
}