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

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

# ── ADDOM-001: Forest Functional Level ─────────────────────────────────────
function Test-ReconADDOM001 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $level = [int]$domain.ForestFunctionalLevel
    $levelName = $domain.ForestFunctionalLevelName

    $status = if ($level -ge 7) { 'PASS' }
              elseif ($level -ge 6) { 'WARN' }
              else { 'FAIL' }

    $currentValue = "Forest functional level: $levelName (level $level)"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue `
        -Details @{
            ForestFunctionalLevel     = $level
            ForestFunctionalLevelName = $levelName
        }
}

# ── ADDOM-002: Domain Functional Level ─────────────────────────────────────
function Test-ReconADDOM002 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $level = [int]$domain.DomainFunctionalLevel
    $levelName = $domain.DomainFunctionalLevelName

    $status = if ($level -ge 7) { 'PASS' }
              elseif ($level -ge 6) { 'WARN' }
              else { 'FAIL' }

    $currentValue = "Domain functional level: $levelName (level $level)"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue `
        -Details @{
            DomainFunctionalLevel     = $level
            DomainFunctionalLevelName = $levelName
        }
}

# ── ADDOM-003: Schema Version ──────────────────────────────────────────────
function Test-ReconADDOM003 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $schemaVersion = [int]$domain.SchemaVersion
    $schemaName = $domain.SchemaVersionName

    if ($schemaVersion -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Schema version could not be determined'
    }

    $status = if ($schemaVersion -ge 88) { 'PASS' }
              elseif ($schemaVersion -eq 87) { 'WARN' }
              else { 'FAIL' }

    $currentValue = "Schema version: $schemaVersion ($schemaName)"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue `
        -Details @{
            SchemaVersion     = $schemaVersion
            SchemaVersionName = $schemaName
        }
}

# ── ADDOM-004: DC Inventory ────────────────────────────────────────────────
function Test-ReconADDOM004 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $dcs = @($AuditData.DomainControllers)
    if ($dcs.Count -eq 0 -or ($dcs.Count -eq 1 -and $null -eq $dcs[0])) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain controller data not available'
    }

    $gcCount = @($dcs | Where-Object { $_.IsGlobalCatalog }).Count
    $rodcCount = @($dcs | Where-Object { $_.IsRODC }).Count
    $obsoleteCount = @($dcs | Where-Object { $_.ObsoleteOS }).Count

    $status = if ($dcs.Count -eq 1) { 'WARN' } else { 'PASS' }

    $currentValue = "$($dcs.Count) domain controller(s): $gcCount GC, $rodcCount RODC"
    if ($obsoleteCount -gt 0) {
        $currentValue += ", $obsoleteCount running obsolete OS"
    }
    if ($dcs.Count -eq 1) {
        $currentValue += ' (single DC - no redundancy)'
    }

    $dcSummary = @($dcs | ForEach-Object {
        @{
            Name            = $_.Name
            FQDN            = $_.FQDN
            OperatingSystem = $_.OperatingSystem
            IsGlobalCatalog = $_.IsGlobalCatalog
            IsRODC          = $_.IsRODC
            ObsoleteOS      = $_.ObsoleteOS
        }
    })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue `
        -Details @{
            TotalDCs      = $dcs.Count
            GCCount       = $gcCount
            RODCCount     = $rodcCount
            ObsoleteCount = $obsoleteCount
            DCSummary     = $dcSummary
        }
}

# ── ADDOM-005: Obsolete OS on DCs ──────────────────────────────────────────
function Test-ReconADDOM005 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $dcs = @($AuditData.DomainControllers)
    if ($dcs.Count -eq 0 -or ($dcs.Count -eq 1 -and $null -eq $dcs[0])) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain controller data not available'
    }

    $obsoleteDCs = @($dcs | Where-Object { $_.ObsoleteOS -eq $true })

    if ($obsoleteDCs.Count -gt 0) {
        $dcNames = @($obsoleteDCs | ForEach-Object { "$($_.Name) ($($_.OperatingSystem))" })
        $currentValue = "$($obsoleteDCs.Count) DC(s) running obsolete OS: $($dcNames -join '; ')"

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue $currentValue `
            -Details @{
                ObsoleteDCs = @($obsoleteDCs | ForEach-Object {
                    @{ Name = $_.Name; FQDN = $_.FQDN; OperatingSystem = $_.OperatingSystem }
                })
            }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "All $($dcs.Count) DC(s) running supported operating systems" `
        -Details @{ TotalDCs = $dcs.Count }
}

# ── ADDOM-006: FSMO Role Identification ────────────────────────────────────
function Test-ReconADDOM006 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain -or -not $domain.FSMORoles) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'FSMO role data not available'
    }

    $roles = $domain.FSMORoles
    $roleList = @(
        "Schema Master: $($roles.SchemaMaster)"
        "Domain Naming Master: $($roles.DomainNamingMaster)"
        "PDC Emulator: $($roles.PDCEmulator)"
        "RID Master: $($roles.RIDMaster)"
        "Infrastructure Master: $($roles.InfrastructureMaster)"
    )

    # Check if all roles are on the same DC
    $uniqueHolders = @($roles.Values | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
        Sort-Object -Unique)

    $status = 'PASS'
    $currentValue = "FSMO roles distributed across $($uniqueHolders.Count) DC(s)"

    if ($uniqueHolders.Count -eq 1 -and $uniqueHolders[0]) {
        $status = 'WARN'
        $currentValue = "All 5 FSMO roles held by single DC: $($uniqueHolders[0])"
    } elseif ($uniqueHolders.Count -eq 0) {
        $status = 'WARN'
        $currentValue = 'Unable to determine FSMO role holders'
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue `
        -Details @{
            SchemaMaster        = $roles.SchemaMaster
            DomainNamingMaster  = $roles.DomainNamingMaster
            PDCEmulator         = $roles.PDCEmulator
            RIDMaster           = $roles.RIDMaster
            InfrastructureMaster = $roles.InfrastructureMaster
            UniqueHolders       = @($uniqueHolders)
        }
}

# ── ADDOM-007: AD Replication Health ───────────────────────────────────────
function Test-ReconADDOM007 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    # ReplicationHealth may not be collected depending on access level
    if (-not $domain.ContainsKey('ReplicationHealth') -or $null -eq $domain.ReplicationHealth) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Replication health data not collected. Run repadmin /replsummary for manual verification'
    }

    $replHealth = $domain.ReplicationHealth

    # If replication health data is available, evaluate it
    $failures = @()
    if ($replHealth -is [array]) {
        $failures = @($replHealth | Where-Object {
            $_.Status -and $_.Status -ne 'Success' -and $_.Status -ne 'OK'
        })
    } elseif ($replHealth -is [hashtable]) {
        if ($replHealth.ContainsKey('Failures')) {
            $failures = @($replHealth.Failures)
        }
    }

    if ($failures.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "$($failures.Count) replication failure(s) detected" `
            -Details @{ Failures = $failures }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue 'AD replication is healthy' `
        -Details @{ ReplicationHealth = $replHealth }
}

# ── ADDOM-008: Tombstone Lifetime ──────────────────────────────────────────
function Test-ReconADDOM008 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $tombstone = [int]$domain.TombstoneLifetime

    if ($tombstone -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'Tombstone lifetime could not be determined' `
            -Details @{ TombstoneLifetime = 0 }
    }

    $status = if ($tombstone -ge 180) { 'PASS' }
              elseif ($tombstone -ge 60) { 'WARN' }
              else { 'FAIL' }

    $currentValue = "Tombstone lifetime: $tombstone days"

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue `
        -Details @{ TombstoneLifetime = $tombstone }
}

# ── ADDOM-009: AD Recycle Bin ──────────────────────────────────────────────
function Test-ReconADDOM009 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $enabled = $domain.RecycleBinEnabled

    if ($enabled) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue 'AD Recycle Bin is enabled' `
            -Details @{ RecycleBinEnabled = $true }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
        -CurrentValue 'AD Recycle Bin is not enabled. Deleted objects cannot be fully recovered without authoritative restore' `
        -Details @{ RecycleBinEnabled = $false }
}

# ── ADDOM-010: Sites and Subnets ───────────────────────────────────────────
function Test-ReconADDOM010 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $sites = @($domain.Sites)
    if ($sites.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No site data available'
    }

    $sitesWithNoSubnets = @($sites | Where-Object {
        $null -eq $_.Subnets -or @($_.Subnets).Count -eq 0
    })

    $totalSubnets = 0
    foreach ($site in $sites) {
        $totalSubnets += @($site.Subnets).Count
    }

    if ($sitesWithNoSubnets.Count -gt 0) {
        $emptyNames = @($sitesWithNoSubnets | ForEach-Object { $_.Name })
        $currentValue = "$($sitesWithNoSubnets.Count) of $($sites.Count) site(s) have no subnets assigned: $($emptyNames -join ', ')"

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue $currentValue `
            -Details @{
                TotalSites         = $sites.Count
                TotalSubnets       = $totalSubnets
                SitesWithNoSubnets = $emptyNames
            }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "All $($sites.Count) site(s) have subnets assigned ($totalSubnets total subnets)" `
        -Details @{
            TotalSites   = $sites.Count
            TotalSubnets = $totalSubnets
        }
}

# ── ADDOM-011: Site Link Configuration ─────────────────────────────────────
function Test-ReconADDOM011 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $sites = @($domain.Sites)
    if ($sites.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No site data available'
    }

    # Collect unique site link names across all sites
    $allSiteLinks = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    foreach ($site in $sites) {
        if ($site.SiteLinks) {
            foreach ($link in $site.SiteLinks) {
                [void]$allSiteLinks.Add($link)
            }
        }
    }

    # Sites with no site links are isolated
    $isolatedSites = @($sites | Where-Object {
        $null -eq $_.SiteLinks -or @($_.SiteLinks).Count -eq 0
    })

    $status = 'PASS'
    $currentValue = "$($allSiteLinks.Count) site link(s) connecting $($sites.Count) site(s)"

    if ($isolatedSites.Count -gt 0) {
        $status = 'WARN'
        $isolatedNames = @($isolatedSites | ForEach-Object { $_.Name })
        $currentValue += ". $($isolatedSites.Count) site(s) have no site links: $($isolatedNames -join ', ')"
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $currentValue `
        -Details @{
            TotalSiteLinks = $allSiteLinks.Count
            TotalSites     = $sites.Count
            SiteLinks      = @($allSiteLinks)
            IsolatedSites  = @(if ($isolatedSites.Count -gt 0) { $isolatedSites | ForEach-Object { $_.Name } } else { @() })
        }
}

# ── ADDOM-012: DNS Zone Security ───────────────────────────────────────────
function Test-ReconADDOM012 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $domain = $AuditData.Domain
    if (-not $domain) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Domain information not available'
    }

    $dnsZones = @($domain.DnsZones)
    if ($dnsZones.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No DNS zone data available'
    }

    # Check for zones that might have insecure dynamic update settings.
    # The LDAP query for dnsZone objects doesn't always expose the DynamicUpdate setting
    # directly. We check if the zone data includes a DynamicUpdate property.
    $insecureZones = [System.Collections.Generic.List[string]]::new()
    $checkedZones = 0

    foreach ($zone in $dnsZones) {
        # Only check AD-integrated zones (which should use secure-only updates)
        if ($zone.ZoneType -match '^AD-') {
            $checkedZones++

            if ($zone.ContainsKey('DynamicUpdate')) {
                # DynamicUpdate values: 0=None, 1=Nonsecure+Secure, 2=SecureOnly
                if ($zone.DynamicUpdate -eq 1 -or $zone.DynamicUpdate -eq 'NonsecureAndSecure') {
                    $insecureZones.Add($zone.Name)
                }
            }
        }
    }

    # If no DynamicUpdate property was available, provide guidance
    if ($checkedZones -gt 0 -and $insecureZones.Count -eq 0) {
        $hasDynamicUpdateData = $false
        foreach ($zone in $dnsZones) {
            if ($zone.ContainsKey('DynamicUpdate')) {
                $hasDynamicUpdateData = $true
                break
            }
        }

        if (-not $hasDynamicUpdateData) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
                -CurrentValue "$checkedZones AD-integrated DNS zone(s) found. Dynamic update settings could not be verified programmatically. Verify all zones use Secure Only dynamic updates in DNS Manager" `
                -Details @{
                    TotalDnsZones  = $dnsZones.Count
                    ADIntegrated   = $checkedZones
                    ZoneNames      = @($dnsZones | ForEach-Object { $_.Name })
                }
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue "All $checkedZones AD-integrated DNS zone(s) use secure dynamic updates" `
            -Details @{
                TotalDnsZones = $dnsZones.Count
                ADIntegrated  = $checkedZones
            }
    }

    if ($insecureZones.Count -gt 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue "$($insecureZones.Count) DNS zone(s) allow nonsecure dynamic updates: $($insecureZones -join ', ')" `
            -Details @{
                InsecureZones = @($insecureZones)
                TotalDnsZones = $dnsZones.Count
            }
    }

    # Fallback: no AD-integrated zones found at all
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue "$($dnsZones.Count) DNS zone(s) found but none are AD-integrated. Verify DNS zone configuration manually" `
        -Details @{ TotalDnsZones = $dnsZones.Count }
}

# ── ADDOM-013: LDAP Signing ───────────────────────────────────────────────
function Test-ReconADDOM013 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Attempt to find LDAP signing configuration from GPO SYSVOL content
    $gpoData = $AuditData.GroupPolicies
    $ldapSigningValue = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            # Check for registry-based policy: LDAPServerIntegrity
            # Value 2 = Require signing
            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('SecuritySettings')) {
                $secSettings = $gpoContent.SecuritySettings

                if ($secSettings -is [hashtable] -and $secSettings.ContainsKey('LDAPServerIntegrity')) {
                    $ldapSigningValue = [int]$secSettings.LDAPServerIntegrity
                }
            }

            # Also check the raw registry policy data
            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) {
                foreach ($regPolicy in $gpoContent.RegistryPolicies) {
                    if ($regPolicy.ValueName -eq 'LDAPServerIntegrity' -or
                        $regPolicy.ValueName -eq 'ldapserverintegrity') {
                        $ldapSigningValue = [int]$regPolicy.Value
                    }
                }
            }
        }
    }

    if ($null -ne $ldapSigningValue) {
        # 0 = None, 1 = Require signing (for server), 2 = Require signing (alternate encoding)
        $status = if ($ldapSigningValue -ge 2) { 'PASS' }
                  elseif ($ldapSigningValue -eq 1) { 'WARN' }
                  else { 'FAIL' }

        $valueLabel = switch ($ldapSigningValue) {
            0 { 'None' }
            1 { 'Negotiate signing' }
            2 { 'Require signing' }
            default { "Unknown ($ldapSigningValue)" }
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue "LDAP server signing requirement: $valueLabel" `
            -Details @{ LDAPServerIntegrity = $ldapSigningValue }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'LDAP signing configuration could not be verified from GPO data. Verify Domain controller: LDAP server signing requirements is set to Require signing in Group Policy applied to the Domain Controllers OU' `
        -Details @{ Note = 'GPO SYSVOL content not available or LDAPServerIntegrity setting not found' }
}

# ── ADDOM-014: LDAP Channel Binding ───────────────────────────────────────
function Test-ReconADDOM014 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Check for LdapEnforceChannelBinding registry setting in GPO data
    $gpoData = $AuditData.GroupPolicies
    $channelBindingValue = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) {
                foreach ($regPolicy in $gpoContent.RegistryPolicies) {
                    if ($regPolicy.ValueName -eq 'LdapEnforceChannelBinding' -or
                        $regPolicy.ValueName -eq 'ldapenforcechannelbinding') {
                        $channelBindingValue = [int]$regPolicy.Value
                    }
                }
            }
        }
    }

    if ($null -ne $channelBindingValue) {
        # 0 = Never, 1 = When Supported, 2 = Always
        $status = if ($channelBindingValue -eq 2) { 'PASS' }
                  elseif ($channelBindingValue -eq 1) { 'WARN' }
                  else { 'FAIL' }

        $valueLabel = switch ($channelBindingValue) {
            0 { 'Never' }
            1 { 'When Supported' }
            2 { 'Always' }
            default { "Unknown ($channelBindingValue)" }
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue "LDAP channel binding: $valueLabel" `
            -Details @{ LdapEnforceChannelBinding = $channelBindingValue }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'LDAP channel binding configuration could not be verified from GPO data. Check the LdapEnforceChannelBinding registry value (HKLM\System\CurrentControlSet\Services\NTDS\Parameters) on all DCs. Value should be 2 (Always)' `
        -Details @{ Note = 'GPO SYSVOL content not available or LdapEnforceChannelBinding setting not found' }
}

# ── ADDOM-015: SMB Signing ─────────────────────────────────────────────────
function Test-ReconADDOM015 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Check for SMB signing settings in GPO data
    # Policy: "Microsoft network server: Digitally sign communications (always)"
    # Registry: HKLM\System\CurrentControlSet\Services\LanmanServer\Parameters\RequireSecuritySignature
    $gpoData = $AuditData.GroupPolicies
    $smbSigningRequired = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('SecuritySettings')) {
                $secSettings = $gpoContent.SecuritySettings

                # Check for the security option directly
                if ($secSettings -is [hashtable]) {
                    if ($secSettings.ContainsKey('RequireSecuritySignature')) {
                        $smbSigningRequired = [int]$secSettings.RequireSecuritySignature
                    }
                    if ($secSettings.ContainsKey('LanmanServerRequireSecuritySignature')) {
                        $smbSigningRequired = [int]$secSettings.LanmanServerRequireSecuritySignature
                    }
                }
            }

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) {
                foreach ($regPolicy in $gpoContent.RegistryPolicies) {
                    if ($regPolicy.ValueName -eq 'RequireSecuritySignature' -and
                        $regPolicy.Key -match 'LanmanServer') {
                        $smbSigningRequired = [int]$regPolicy.Value
                    }
                }
            }
        }
    }

    if ($null -ne $smbSigningRequired) {
        $status = if ($smbSigningRequired -eq 1) { 'PASS' } else { 'FAIL' }
        $valueLabel = if ($smbSigningRequired -eq 1) { 'Required (Enabled)' } else { 'Not Required (Disabled)' }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue "SMB signing on domain controllers: $valueLabel" `
            -Details @{ RequireSecuritySignature = $smbSigningRequired }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'SMB signing configuration could not be verified from GPO data. Verify Microsoft network server: Digitally sign communications (always) is Enabled in Group Policy applied to the Domain Controllers OU' `
        -Details @{ Note = 'GPO SYSVOL content not available or SMB signing setting not found' }
}

# ── ADDOM-016: NTLMv1 Detection ────────────────────────────────────────────
function Test-ReconADDOM016 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Check LAN Manager authentication level in GPO data
    # Registry: HKLM\System\CurrentControlSet\Control\Lsa\LmCompatibilityLevel
    $gpoData = $AuditData.GroupPolicies
    $lmLevel = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('SecuritySettings')) {
                $secSettings = $gpoContent.SecuritySettings
                if ($secSettings -is [hashtable] -and $secSettings.ContainsKey('LmCompatibilityLevel')) {
                    $lmLevel = [int]$secSettings.LmCompatibilityLevel
                }
            }

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) {
                foreach ($regPolicy in $gpoContent.RegistryPolicies) {
                    if ($regPolicy.ValueName -eq 'LmCompatibilityLevel' -or
                        $regPolicy.ValueName -eq 'lmcompatibilitylevel') {
                        $lmLevel = [int]$regPolicy.Value
                    }
                }
            }
        }
    }

    if ($null -ne $lmLevel) {
        # Level 0-2: NTLMv1 is allowed; Level 3-5: NTLMv1 is refused
        $status = if ($lmLevel -ge 3) { 'PASS' } else { 'FAIL' }

        $levelDescription = switch ($lmLevel) {
            0 { 'Send LM & NTLM responses' }
            1 { 'Send LM & NTLM - use NTLMv2 session security if negotiated' }
            2 { 'Send NTLM response only' }
            3 { 'Send NTLMv2 response only' }
            4 { 'Send NTLMv2 response only. Refuse LM' }
            5 { 'Send NTLMv2 response only. Refuse LM & NTLM' }
            default { "Unknown ($lmLevel)" }
        }

        $currentValue = "LAN Manager authentication level: $lmLevel ($levelDescription)"

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue $currentValue `
            -Details @{
                LmCompatibilityLevel = $lmLevel
                Description          = $levelDescription
                NTLMv1Allowed        = ($lmLevel -lt 3)
            }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'LAN Manager authentication level could not be verified from GPO data. Check Network security: LAN Manager authentication level in Group Policy. FAIL if level is below 3 (NTLMv1 would be allowed)' `
        -Details @{ Note = 'GPO SYSVOL content not available or LmCompatibilityLevel setting not found' }
}

# ── ADDOM-017: NTLMv2 Enforcement ──────────────────────────────────────────
function Test-ReconADDOM017 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Reuses the same LmCompatibilityLevel data but with stricter thresholds
    $gpoData = $AuditData.GroupPolicies
    $lmLevel = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('SecuritySettings')) {
                $secSettings = $gpoContent.SecuritySettings
                if ($secSettings -is [hashtable] -and $secSettings.ContainsKey('LmCompatibilityLevel')) {
                    $lmLevel = [int]$secSettings.LmCompatibilityLevel
                }
            }

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) {
                foreach ($regPolicy in $gpoContent.RegistryPolicies) {
                    if ($regPolicy.ValueName -eq 'LmCompatibilityLevel' -or
                        $regPolicy.ValueName -eq 'lmcompatibilitylevel') {
                        $lmLevel = [int]$regPolicy.Value
                    }
                }
            }
        }
    }

    if ($null -ne $lmLevel) {
        # Level 5 = PASS (refuse LM & NTLM), Level 3-4 = WARN (NTLMv2 only but not refusing legacy), < 3 = FAIL
        $status = if ($lmLevel -eq 5) { 'PASS' }
                  elseif ($lmLevel -ge 3) { 'WARN' }
                  else { 'FAIL' }

        $levelDescription = switch ($lmLevel) {
            0 { 'Send LM & NTLM responses' }
            1 { 'Send LM & NTLM - use NTLMv2 session security if negotiated' }
            2 { 'Send NTLM response only' }
            3 { 'Send NTLMv2 response only' }
            4 { 'Send NTLMv2 response only. Refuse LM' }
            5 { 'Send NTLMv2 response only. Refuse LM & NTLM' }
            default { "Unknown ($lmLevel)" }
        }

        $currentValue = "LAN Manager authentication level: $lmLevel ($levelDescription)"

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
            -CurrentValue $currentValue `
            -Details @{
                LmCompatibilityLevel = $lmLevel
                Description          = $levelDescription
                FullyEnforced        = ($lmLevel -eq 5)
            }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'NTLMv2 enforcement level could not be verified from GPO data. Verify Network security: LAN Manager authentication level is set to level 5 (Send NTLMv2 response only. Refuse LM & NTLM)' `
        -Details @{ Note = 'GPO SYSVOL content not available or LmCompatibilityLevel setting not found' }
}

# ── ADDOM-018: Null Session Enumeration ────────────────────────────────────
function Test-ReconADDOM018 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Check RestrictAnonymous / RestrictAnonymousSAM settings in GPO data
    $gpoData = $AuditData.GroupPolicies
    $restrictAnonymous = $null
    $restrictAnonymousSAM = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('SecuritySettings')) {
                $secSettings = $gpoContent.SecuritySettings
                if ($secSettings -is [hashtable]) {
                    if ($secSettings.ContainsKey('RestrictAnonymous')) {
                        $restrictAnonymous = [int]$secSettings.RestrictAnonymous
                    }
                    if ($secSettings.ContainsKey('RestrictAnonymousSAM')) {
                        $restrictAnonymousSAM = [int]$secSettings.RestrictAnonymousSAM
                    }
                }
            }

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) {
                foreach ($regPolicy in $gpoContent.RegistryPolicies) {
                    if ($regPolicy.ValueName -eq 'RestrictAnonymous' -or
                        $regPolicy.ValueName -eq 'restrictanonymous') {
                        $restrictAnonymous = [int]$regPolicy.Value
                    }
                    if ($regPolicy.ValueName -eq 'RestrictAnonymousSAM' -or
                        $regPolicy.ValueName -eq 'restrictanonymoussam') {
                        $restrictAnonymousSAM = [int]$regPolicy.Value
                    }
                }
            }
        }
    }

    if ($null -ne $restrictAnonymous -or $null -ne $restrictAnonymousSAM) {
        $issues = [System.Collections.Generic.List[string]]::new()

        # RestrictAnonymous: 0 = allow, 1 = restrict enumeration of shares, 2 = no access without explicit permission
        if ($null -ne $restrictAnonymous -and $restrictAnonymous -lt 1) {
            $issues.Add('RestrictAnonymous not set (anonymous enumeration of SAM accounts and shares allowed)')
        }

        # RestrictAnonymousSAM: 0 = disabled, 1 = enabled
        if ($null -ne $restrictAnonymousSAM -and $restrictAnonymousSAM -ne 1) {
            $issues.Add('RestrictAnonymousSAM not enabled (anonymous enumeration of SAM accounts allowed)')
        }

        if ($issues.Count -gt 0) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
                -CurrentValue "Null session restrictions insufficient: $($issues -join '; ')" `
                -Details @{
                    RestrictAnonymous    = $restrictAnonymous
                    RestrictAnonymousSAM = $restrictAnonymousSAM
                    Issues               = @($issues)
                }
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue "Null session enumeration is restricted (RestrictAnonymous=$restrictAnonymous, RestrictAnonymousSAM=$restrictAnonymousSAM)" `
            -Details @{
                RestrictAnonymous    = $restrictAnonymous
                RestrictAnonymousSAM = $restrictAnonymousSAM
            }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Null session settings could not be verified from GPO data. Verify RestrictAnonymous and RestrictAnonymousSAM are configured in Group Policy to prevent anonymous enumeration' `
        -Details @{ Note = 'GPO SYSVOL content not available or RestrictAnonymous settings not found' }
}

# ── ADDOM-019: Print Spooler on DCs ───────────────────────────────────────
function Test-ReconADDOM019 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Check GPO data for Print Spooler service configuration on DCs
    $gpoData = $AuditData.GroupPolicies
    $spoolerDisabled = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            # Check for system service policies
            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('SystemServices')) {
                $services = $gpoContent.SystemServices
                if ($services -is [hashtable] -and $services.ContainsKey('Spooler')) {
                    # StartupMode: 2=Automatic, 3=Manual, 4=Disabled
                    $spoolerDisabled = ($services.Spooler.StartupMode -eq 4)
                }
                if ($services -is [array]) {
                    $spoolerEntry = $services | Where-Object { $_.ServiceName -eq 'Spooler' }
                    if ($spoolerEntry) {
                        $spoolerDisabled = ($spoolerEntry.StartupMode -eq 4)
                    }
                }
            }

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('SecuritySettings')) {
                $secSettings = $gpoContent.SecuritySettings
                if ($secSettings -is [hashtable] -and $secSettings.ContainsKey('SystemServices')) {
                    $svcSettings = $secSettings.SystemServices
                    if ($svcSettings -is [hashtable] -and $svcSettings.ContainsKey('Spooler')) {
                        $spoolerDisabled = ($svcSettings.Spooler -eq 4 -or $svcSettings.Spooler -eq 'Disabled')
                    }
                }
            }
        }
    }

    if ($null -ne $spoolerDisabled) {
        if ($spoolerDisabled) {
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
                -CurrentValue 'Print Spooler service is disabled on domain controllers via Group Policy' `
                -Details @{ SpoolerDisabledByGPO = $true }
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'Print Spooler service is NOT disabled on domain controllers. This exposes DCs to PrintNightmare and SpoolSample attacks' `
            -Details @{ SpoolerDisabledByGPO = $false }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue 'Print Spooler service status on DCs could not be verified from GPO data. Manually confirm the Spooler service is disabled on all domain controllers to mitigate PrintNightmare (CVE-2021-34527) and coercion attacks' `
        -Details @{ Note = 'GPO SYSVOL content not available or Spooler service configuration not found. Remote service query requires direct DC access.' }
}

# ── ADDOM-020: DSRM Password ──────────────────────────────────────────────
function Test-ReconADDOM020 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # DSRM password cannot be verified remotely via LDAP. We can check:
    # 1. DsrmAdminLogonBehavior registry value from GPO data (should be 0)
    # 2. Number of DCs to inform the auditor how many need manual verification
    $gpoData = $AuditData.GroupPolicies
    $dsrmLogonBehavior = $null

    if ($gpoData -and $gpoData.ContainsKey('SYSVOLContent')) {
        $sysvolContent = $gpoData.SYSVOLContent

        foreach ($gpoId in $sysvolContent.Keys) {
            $gpoContent = $sysvolContent[$gpoId]

            if ($gpoContent -is [hashtable] -and $gpoContent.ContainsKey('RegistryPolicies')) {
                foreach ($regPolicy in $gpoContent.RegistryPolicies) {
                    if ($regPolicy.ValueName -eq 'DsrmAdminLogonBehavior' -or
                        $regPolicy.ValueName -eq 'dsrmadminlogonbehavior') {
                        $dsrmLogonBehavior = [int]$regPolicy.Value
                    }
                }
            }
        }
    }

    $dcCount = 0
    if ($AuditData.DomainControllers) {
        $dcCount = @($AuditData.DomainControllers).Count
    }

    $details = @{
        DCCount = $dcCount
        Note    = 'DSRM password age and uniqueness cannot be verified remotely. Use ntdsutil on each DC to reset and document DSRM passwords.'
    }

    if ($null -ne $dsrmLogonBehavior) {
        $details['DsrmAdminLogonBehavior'] = $dsrmLogonBehavior

        if ($dsrmLogonBehavior -ne 0) {
            # Value 1 or 2 allows DSRM account to be used for network logon, which is dangerous
            return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
                -CurrentValue "DsrmAdminLogonBehavior is set to $dsrmLogonBehavior (allows network DSRM logon). This should be 0 to prevent DSRM account from being used remotely. DSRM password rotation on $dcCount DC(s) requires manual verification" `
                -Details $details
        }

        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue "DsrmAdminLogonBehavior is correctly set to 0 (network DSRM logon prevented). DSRM password rotation and uniqueness across $dcCount DC(s) requires manual verification" `
            -Details $details
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue "DSRM password configuration requires manual verification on $dcCount DC(s). Verify DsrmAdminLogonBehavior is set to 0 (HKLM\System\CurrentControlSet\Control\Lsa) and DSRM passwords are unique per DC and rotated regularly" `
        -Details $details
}