Private/AD/Checks/Invoke-ADGroupPolicyChecks.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-ADGroupPolicyChecks { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$AuditData ) $checkDefs = Get-AuditCategoryDefinitions -Category 'ADGroupPolicyChecks' $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) } # ── ADGPO-001: GPO Inventory with Link Status ──────────────────────────── function Test-ReconADGPO001 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData -or -not $gpoData.GPOs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpos = @($gpoData.GPOs) if ($gpos.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'No GPOs found in the domain' ` -Details @{ TotalGPOs = 0 } } $linkedCount = @($gpos | Where-Object { $_.IsLinked -eq $true }).Count $unlinkedCount = @($gpos | Where-Object { $_.IsLinked -ne $true }).Count $disabledCount = @($gpos | Where-Object { $_.Flags -eq 3 }).Count $emptyCount = @($gpos | Where-Object { $_.IsEmpty -eq $true }).Count $currentValue = "$($gpos.Count) GPO(s) total: $linkedCount linked, $unlinkedCount unlinked, $disabledCount fully disabled, $emptyCount empty" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ TotalGPOs = $gpos.Count LinkedCount = $linkedCount UnlinkedCount = $unlinkedCount DisabledCount = $disabledCount EmptyCount = $emptyCount GPOList = @($gpos | ForEach-Object { @{ DisplayName = $_.DisplayName GUID = $_.GUID IsLinked = $_.IsLinked Flags = $_.Flags FlagDescription = $_.FlagDescription IsEmpty = $_.IsEmpty } }) } } # ── ADGPO-002: Empty GPOs ───────────────────────────────────────────────── function Test-ReconADGPO002 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData -or -not $gpoData.GPOs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpos = @($gpoData.GPOs) $sysvolContent = $gpoData.SYSVOLContent # Empty GPOs: Flags=3 (both disabled) OR IsEmpty=$true (no SYSVOL content beyond GPT.INI) $emptyGPOs = @($gpos | Where-Object { $_.IsEmpty -eq $true -or $_.Flags -eq 3 }) if ($emptyGPOs.Count -gt 0) { $names = @($emptyGPOs | ForEach-Object { $_.DisplayName }) $currentValue = "$($emptyGPOs.Count) empty or fully disabled GPO(s) found: $($names -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ EmptyGPOCount = $emptyGPOs.Count EmptyGPOs = @($emptyGPOs | ForEach-Object { @{ DisplayName = $_.DisplayName GUID = $_.GUID Flags = $_.Flags FlagDescription = $_.FlagDescription IsEmpty = $_.IsEmpty } }) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No empty GPOs found among $($gpos.Count) GPO(s)" ` -Details @{ TotalGPOs = $gpos.Count } } # ── ADGPO-003: Unlinked GPOs ────────────────────────────────────────────── function Test-ReconADGPO003 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData -or -not $gpoData.GPOs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpos = @($gpoData.GPOs) $unlinkedGPOs = @($gpos | Where-Object { $_.IsLinked -ne $true }) if ($unlinkedGPOs.Count -gt 0) { $names = @($unlinkedGPOs | ForEach-Object { $_.DisplayName }) $currentValue = "$($unlinkedGPOs.Count) unlinked GPO(s) found: $($names -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ UnlinkedCount = $unlinkedGPOs.Count UnlinkedGPOs = @($unlinkedGPOs | ForEach-Object { @{ DisplayName = $_.DisplayName GUID = $_.GUID WhenChanged = $_.WhenChanged } }) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "All $($gpos.Count) GPO(s) are linked to at least one container" ` -Details @{ TotalGPOs = $gpos.Count } } # ── ADGPO-004: Disabled GPOs with Content ───────────────────────────────── function Test-ReconADGPO004 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData -or -not $gpoData.GPOs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpos = @($gpoData.GPOs) # GPOs that have some section disabled (Flags 1, 2, or 3) but are not empty $disabledWithContent = @($gpos | Where-Object { $_.Flags -gt 0 -and $_.IsEmpty -ne $true }) if ($disabledWithContent.Count -gt 0) { $names = @($disabledWithContent | ForEach-Object { "$($_.DisplayName) ($($_.FlagDescription))" }) $currentValue = "$($disabledWithContent.Count) GPO(s) have disabled sections but contain settings: $($names -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ Count = $disabledWithContent.Count GPOs = @($disabledWithContent | ForEach-Object { @{ DisplayName = $_.DisplayName GUID = $_.GUID Flags = $_.Flags FlagDescription = $_.FlagDescription } }) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No GPOs with disabled sections containing active settings" ` -Details @{ TotalGPOs = $gpos.Count } } # ── ADGPO-005: Duplicated GPOs ──────────────────────────────────────────── function Test-ReconADGPO005 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData -or -not $gpoData.GPOs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpos = @($gpoData.GPOs) if ($gpos.Count -lt 2) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'Fewer than 2 GPOs; no duplicates possible' ` -Details @{ TotalGPOs = $gpos.Count } } # Simplified duplicate detection: look for GPOs with similar naming patterns # (e.g., "Policy - Copy", "Policy (2)", "Policy_old", "Policy_backup") $potentialDuplicates = [System.Collections.Generic.List[hashtable]]::new() $duplicatePatterns = @('[\s_-]*(copy|backup|old|v\d|test|temp|clone|dup)', '\s*\(\d+\)\s*$') foreach ($gpo in $gpos) { $name = $gpo.DisplayName if (-not $name) { continue } foreach ($pattern in $duplicatePatterns) { if ($name -match $pattern) { # Find what the base name would be $baseName = $name -replace $pattern, '' $baseName = $baseName.Trim() # Check if a GPO with the base name exists $baseGPO = $gpos | Where-Object { $_.DisplayName -eq $baseName -and $_.GUID -ne $gpo.GUID } if ($baseGPO) { $potentialDuplicates.Add(@{ DisplayName = $name GUID = $gpo.GUID BaseName = $baseName Pattern = $pattern }) } break } } } if ($potentialDuplicates.Count -gt 0) { $names = @($potentialDuplicates | ForEach-Object { $_.DisplayName }) $currentValue = "$($potentialDuplicates.Count) potentially duplicated GPO(s) detected: $($names -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ PotentialDuplicates = @($potentialDuplicates) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No obvious duplicate GPOs detected among $($gpos.Count) GPO(s)" ` -Details @{ TotalGPOs = $gpos.Count } } # ── ADGPO-006: GPOs with Broken Links ──────────────────────────────────── function Test-ReconADGPO006 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpoLinks = $gpoData.GPOLinks if (-not $gpoLinks -or $gpoLinks.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'GPO link data not available' } $gpos = @($gpoData.GPOs) # Build a set of known GPO DNs (lowercase for comparison) $knownGPODNs = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($gpo in $gpos) { if ($gpo.DN) { [void]$knownGPODNs.Add($gpo.DN) } } $brokenLinks = [System.Collections.Generic.List[hashtable]]::new() foreach ($containerDN in $gpoLinks.Keys) { $links = @($gpoLinks[$containerDN]) foreach ($link in $links) { if ($link.GPODN -and -not $knownGPODNs.Contains($link.GPODN)) { $brokenLinks.Add(@{ ContainerDN = $containerDN GPODN = $link.GPODN IsEnabled = $link.IsEnabled }) } } } if ($brokenLinks.Count -gt 0) { $currentValue = "$($brokenLinks.Count) broken GPO link(s) found referencing non-existent GPOs" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ BrokenLinkCount = $brokenLinks.Count BrokenLinks = @($brokenLinks) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'All GPO links reference valid GPOs' ` -Details @{ TotalLinksChecked = ($gpoLinks.Values | ForEach-Object { $_.Count } | Measure-Object -Sum).Sum } } # ── ADGPO-007: GPO Permission Inconsistencies ──────────────────────────── function Test-ReconADGPO007 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpoPerms = $gpoData.GPOPermissions if (-not $gpoPerms -or $gpoPerms.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'GPO permission data not available' } # Well-known admin SIDs and names that are expected to have edit rights $trustedEditors = @( 'Domain Admins', 'Enterprise Admins', 'SYSTEM', 'ENTERPRISE DOMAIN CONTROLLERS', 'S-1-5-18', 'S-1-5-9' ) $issues = [System.Collections.Generic.List[hashtable]]::new() foreach ($gpoName in $gpoPerms.Keys) { $perm = $gpoPerms[$gpoName] $canEdit = @($perm.CanEdit) foreach ($editor in $canEdit) { $isTrusted = $false foreach ($trusted in $trustedEditors) { if ($editor -eq $trusted -or $editor -match "^$([regex]::Escape($trusted))\\b") { $isTrusted = $true break } } if (-not $isTrusted -and $editor -notmatch '\bAdmins?\b') { $issues.Add(@{ GPOName = $gpoName Principal = $editor Permission = 'Edit' }) } } } if ($issues.Count -gt 0) { $summary = @($issues | ForEach-Object { "$($_.Principal) can edit '$($_.GPOName)'" }) $displaySummary = if ($summary.Count -le 5) { $summary -join '; ' } else { ($summary | Select-Object -First 5) -join '; ' + " and $($summary.Count - 5) more" } $currentValue = "$($issues.Count) non-admin principal(s) with GPO edit permissions: $displaySummary" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ IssueCount = $issues.Count Issues = @($issues) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "GPO edit permissions are restricted to expected admin principals across $($gpoPerms.Count) GPO(s)" ` -Details @{ GPOsChecked = $gpoPerms.Count } } # ── ADGPO-008: GPOs Not Applied Due to WMI Filters ────────────────────── function Test-ReconADGPO008 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData -or -not $gpoData.GPOs) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $wmiFilters = @($gpoData.WMIFilters) if ($wmiFilters.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No WMI filters are configured in the domain' ` -Details @{ WMIFilterCount = 0 } } # Identify GPOs that reference WMI filters by checking GPO properties # The GPO objects from the collector do not have a direct WMIFilter field, # but WMI filters being present is itself worth noting $gpos = @($gpoData.GPOs) $gposWithWMI = [System.Collections.Generic.List[hashtable]]::new() # WMI filters can restrict application; report all GPOs that have linked ones # Since the data model may not directly link GPO->WMI, report the filters that exist $currentValue = "$($wmiFilters.Count) WMI filter(s) exist in the domain that may restrict GPO application. Review to ensure security-critical GPOs are not blocked" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ WMIFilterCount = $wmiFilters.Count WMIFilters = @($wmiFilters | ForEach-Object { @{ Name = $_.Name Description = $_.Description Query = $_.Query } }) } } # ── ADGPO-009: GPOs with No Apply Permission ───────────────────────────── function Test-ReconADGPO009 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $gpoPerms = $gpoData.GPOPermissions if (-not $gpoPerms -or $gpoPerms.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'GPO permission data not available' } $gpos = @($gpoData.GPOs) $noApplyGPOs = [System.Collections.Generic.List[hashtable]]::new() foreach ($gpoName in $gpoPerms.Keys) { $perm = $gpoPerms[$gpoName] $canApply = @($perm.CanApply) # A GPO with no Apply principals will not be processed by anyone if ($canApply.Count -eq 0) { # Check if this GPO is actually linked (unlinked GPOs without apply are expected) $gpoObj = $gpos | Where-Object { $_.DisplayName -eq $gpoName } | Select-Object -First 1 if ($gpoObj -and $gpoObj.IsLinked) { $noApplyGPOs.Add(@{ GPOName = $gpoName GUID = if ($gpoObj) { $gpoObj.GUID } else { '' } }) } } } if ($noApplyGPOs.Count -gt 0) { $names = @($noApplyGPOs | ForEach-Object { $_.GPOName }) $currentValue = "$($noApplyGPOs.Count) linked GPO(s) have no Apply Group Policy permission granted: $($names -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ NoApplyCount = $noApplyGPOs.Count NoApplyGPOs = @($noApplyGPOs) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "All linked GPOs have Apply Group Policy permission granted to at least one principal" ` -Details @{ GPOsChecked = $gpoPerms.Count } } # ── ADGPO-010: SYSVOL/AD GPO Version Mismatch ─────────────────────────── function Test-ReconADGPO010 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $mismatches = @($gpoData.GPOVersionMismatch) if ($mismatches.Count -eq 0 -or ($mismatches.Count -eq 1 -and $null -eq $mismatches[0])) { # Check if SYSVOL was accessible $sysvolContent = $gpoData.SYSVOLContent $sysvolErrors = $false if ($sysvolContent) { foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and $entry.ContainsKey('Error')) { $sysvolErrors = $true break } } } if ($sysvolErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL was not accessible; version comparison could not be performed' } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'All GPO versions match between AD and SYSVOL' ` -Details @{ MismatchCount = 0 } } $names = @($mismatches | Where-Object { $_ } | ForEach-Object { $_.DisplayName }) $currentValue = "$($mismatches.Count) GPO(s) have AD/SYSVOL version mismatches: $($names -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ MismatchCount = $mismatches.Count Mismatches = @($mismatches | Where-Object { $_ } | ForEach-Object { @{ DisplayName = $_.DisplayName GUID = $_.GUID ADVersionUser = $_.ADVersionUser ADVersionComputer = $_.ADVersionComputer SYSVOLVersionUser = $_.SYSVOLVersionUser SYSVOLVersionComputer = $_.SYSVOLVersionComputer } }) } } # ── ADGPO-011: GPO Settings Security Analysis ─────────────────────────── function Test-ReconADGPO011 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for security analysis' } # Check all SYSVOL content entries for error markers indicating inaccessibility $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot analyze GPO security settings' } $securityFindings = [System.Collections.Generic.List[hashtable]]::new() $gposWithSecSettings = 0 foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } $hasRegPol = $content.HasRegistryPol -eq $true $hasPrefs = $content.HasPreferences -eq $true if ($hasRegPol -or $hasPrefs) { $gposWithSecSettings++ } } # This is an informational check; report what was found $currentValue = "$gposWithSecSettings of $($sysvolContent.Count) GPO(s) contain registry policies or preference settings that may affect security configuration. Manual review of GPO reports recommended" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ TotalGPOs = $sysvolContent.Count GPOsWithSecSettings = $gposWithSecSettings Note = 'Detailed GPO report analysis (Get-GPOReport -All -ReportType XML) recommended for comprehensive security settings review' } } # ── ADGPO-012: cPassword/GPP Password Detection ───────────────────────── function Test-ReconADGPO012 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for cPassword scanning' } # Check all entries for SYSVOL access errors $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot scan for cPassword values' } $affectedGPOs = [System.Collections.Generic.List[hashtable]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } if ($content.CPasswordFound -eq $true) { $affectedGPOs.Add(@{ GPOName = $gpoName CPasswordFiles = @($content.CPasswordLocations) }) } } if ($affectedGPOs.Count -gt 0) { $totalFiles = ($affectedGPOs | ForEach-Object { $_.CPasswordFiles.Count } | Measure-Object -Sum).Sum $names = @($affectedGPOs | ForEach-Object { $_.GPOName }) $currentValue = "CRITICAL: $($affectedGPOs.Count) GPO(s) contain cPassword values (MS14-025) in $totalFiles file(s): $($names -join '; '). These passwords are trivially decryptable by any domain user" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ AffectedGPOCount = $affectedGPOs.Count TotalCPassFiles = $totalFiles AffectedGPOs = @($affectedGPOs) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No cPassword values found in SYSVOL GPP XML files' ` -Details @{ GPOsScanned = $sysvolContent.Count } } # ── ADGPO-013: Scripts in GPOs Analysis ────────────────────────────────── function Test-ReconADGPO013 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for script analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot analyze GPO scripts' } $gposWithScripts = [System.Collections.Generic.List[hashtable]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } if ($content.HasScripts -eq $true -and $content.ScriptFiles.Count -gt 0) { $gposWithScripts.Add(@{ GPOName = $gpoName ScriptCount = $content.ScriptFiles.Count ScriptFiles = @($content.ScriptFiles) }) } } if ($gposWithScripts.Count -gt 0) { $totalScripts = ($gposWithScripts | ForEach-Object { $_.ScriptCount } | Measure-Object -Sum).Sum $names = @($gposWithScripts | ForEach-Object { "$($_.GPOName) ($($_.ScriptCount) scripts)" }) $currentValue = "$($gposWithScripts.Count) GPO(s) contain $totalScripts script file(s) in SYSVOL: $($names -join '; '). Review for hardcoded credentials, unsafe operations, and unauthorized commands" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ GPOsWithScripts = $gposWithScripts.Count TotalScriptFiles = $totalScripts GPOs = @($gposWithScripts) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No script files found in GPO SYSVOL folders' ` -Details @{ GPOsScanned = $sysvolContent.Count } } # ── ADGPO-014: MSI Packages in GPOs ────────────────────────────────────── function Test-ReconADGPO014 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for MSI analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot check for MSI packages' } $gposWithMSI = [System.Collections.Generic.List[hashtable]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } # MSI packages are typically in Preferences or direct paths; check preference files for .msi references $msiFiles = [System.Collections.Generic.List[string]]::new() if ($content.PreferenceFiles -and $content.PreferenceFiles.Count -gt 0) { foreach ($prefFile in $content.PreferenceFiles) { if ($prefFile -match '\.msi$|\.msp$|\.mst$') { $msiFiles.Add($prefFile) } } } # Also check script files for .msi references if ($content.ScriptFiles -and $content.ScriptFiles.Count -gt 0) { foreach ($scriptFile in $content.ScriptFiles) { if ($scriptFile -match '\.msi$|\.msp$|\.mst$') { $msiFiles.Add($scriptFile) } } } if ($msiFiles.Count -gt 0) { $gposWithMSI.Add(@{ GPOName = $gpoName MSIFiles = @($msiFiles) }) } } if ($gposWithMSI.Count -gt 0) { $totalMSI = ($gposWithMSI | ForEach-Object { $_.MSIFiles.Count } | Measure-Object -Sum).Sum $names = @($gposWithMSI | ForEach-Object { $_.GPOName }) $currentValue = "$($gposWithMSI.Count) GPO(s) contain $totalMSI MSI/software deployment file(s): $($names -join '; '). Verify package sources and integrity" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ GPOsWithMSI = $gposWithMSI.Count TotalMSIFiles = $totalMSI GPOs = @($gposWithMSI) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No MSI package files found in GPO SYSVOL folders' ` -Details @{ GPOsScanned = $sysvolContent.Count } } # ── ADGPO-015: Scheduled Tasks in GPOs ─────────────────────────────────── function Test-ReconADGPO015 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for scheduled task analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot check for scheduled tasks in GPOs' } $gposWithTasks = [System.Collections.Generic.List[hashtable]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } # Scheduled tasks in GPP are stored in ScheduledTasks.xml under Preferences $taskFiles = [System.Collections.Generic.List[string]]::new() if ($content.PreferenceFiles -and $content.PreferenceFiles.Count -gt 0) { foreach ($prefFile in $content.PreferenceFiles) { if ($prefFile -match 'ScheduledTasks\.xml$|ScheduledTasks\\') { $taskFiles.Add($prefFile) } } } if ($taskFiles.Count -gt 0) { $gposWithTasks.Add(@{ GPOName = $gpoName TaskFiles = @($taskFiles) }) } } if ($gposWithTasks.Count -gt 0) { $names = @($gposWithTasks | ForEach-Object { $_.GPOName }) $currentValue = "$($gposWithTasks.Count) GPO(s) deploy scheduled tasks via Group Policy Preferences: $($names -join '; '). Review for unauthorized tasks, stored credentials, and least-privilege execution" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ GPOsWithTasks = $gposWithTasks.Count GPOs = @($gposWithTasks) } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No scheduled task configurations found in GPO preferences' ` -Details @{ GPOsScanned = $sysvolContent.Count } } # ── ADGPO-016: Registry Settings Security Review ──────────────────────── function Test-ReconADGPO016 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for registry analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot analyze GPO registry settings' } $gposWithRegPol = [System.Collections.Generic.List[string]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } if ($content.HasRegistryPol -eq $true) { $gposWithRegPol.Add($gpoName) } } if ($gposWithRegPol.Count -gt 0) { $currentValue = "$($gposWithRegPol.Count) GPO(s) contain Registry.pol files with registry-based policy settings: $($gposWithRegPol -join '; '). Review for settings that may weaken security defaults" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ GPOsWithRegistryPol = $gposWithRegPol.Count GPONames = @($gposWithRegPol) Note = 'Use Get-GPOReport to extract and review specific registry settings deployed by these GPOs' } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No Registry.pol files found in GPO SYSVOL folders' ` -Details @{ GPOsScanned = $sysvolContent.Count } } # ── ADGPO-017: Restricted Groups Analysis ──────────────────────────────── function Test-ReconADGPO017 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for Restricted Groups analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot analyze Restricted Groups settings' } # Restricted Groups are in GptTmpl.inf under Machine\Microsoft\Windows NT\SecEdit # or via Preferences Groups. Check for Groups.xml in preference files $gposWithGroups = [System.Collections.Generic.List[hashtable]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } $groupFiles = [System.Collections.Generic.List[string]]::new() # Check preference files for Groups.xml if ($content.PreferenceFiles -and $content.PreferenceFiles.Count -gt 0) { foreach ($prefFile in $content.PreferenceFiles) { if ($prefFile -match 'Groups\.xml$|Groups\\') { $groupFiles.Add($prefFile) } } } if ($groupFiles.Count -gt 0) { $gposWithGroups.Add(@{ GPOName = $gpoName GroupFiles = @($groupFiles) }) } } if ($gposWithGroups.Count -gt 0) { $names = @($gposWithGroups | ForEach-Object { $_.GPOName }) $currentValue = "$($gposWithGroups.Count) GPO(s) configure group membership via Preferences: $($names -join '; '). Verify that local Administrators membership is appropriately restricted" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ GPOsWithGroupConfig = $gposWithGroups.Count GPOs = @($gposWithGroups) Note = 'Review Groups.xml and Restricted Groups settings to ensure least-privilege local admin membership' } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'No Restricted Groups or Group Policy Preferences group membership configurations found. Consider configuring Restricted Groups to enforce local Administrators membership' ` -Details @{ GPOsScanned = $sysvolContent.Count } } # ── ADGPO-018: Audit Policy Configuration via GPO ─────────────────────── function Test-ReconADGPO018 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for audit policy analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot verify audit policy configuration' } # Advanced Audit Policy is configured via Registry.pol or audit.csv in SYSVOL # Check for GPOs that have audit-related content $auditGPOsFound = $false $gposWithAuditContent = [System.Collections.Generic.List[string]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } # Audit policy settings are in SecuritySettings or Registry.pol if ($content.HasRegistryPol -eq $true) { # Registry.pol may contain Advanced Audit Policy settings $gposWithAuditContent.Add($gpoName) $auditGPOsFound = $true } # Also check for audit.csv in preferences or script paths if ($content.PreferenceFiles -and $content.PreferenceFiles.Count -gt 0) { foreach ($prefFile in $content.PreferenceFiles) { if ($prefFile -match 'audit\.csv$|Audit\\') { if (-not $gposWithAuditContent.Contains($gpoName)) { $gposWithAuditContent.Add($gpoName) } $auditGPOsFound = $true } } } } if (-not $auditGPOsFound) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue 'No GPOs with audit policy configuration detected. Configure Advanced Audit Policy via GPO to enable comprehensive security event logging on all domain-joined systems' ` -Details @{ GPOsScanned = $sysvolContent.Count Note = 'Advanced Audit Policy should be configured under Computer Configuration > Windows Settings > Security Settings > Advanced Audit Policy Configuration' } } $currentValue = "$($gposWithAuditContent.Count) GPO(s) contain registry policies that may include audit configuration: $($gposWithAuditContent -join '; '). Verify Advanced Audit Policy covers all required categories" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ GPOsWithAuditConfig = $gposWithAuditContent.Count GPONames = @($gposWithAuditContent) Note = 'Use gpresult /h on a representative system to verify effective audit policy. Ensure Advanced Audit Policy (not legacy) is used' } } # ── ADGPO-019: Windows Firewall Configuration via GPO ──────────────────── function Test-ReconADGPO019 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for firewall analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot verify Windows Firewall GPO configuration' } # Windows Firewall settings are typically in Registry.pol under # HKLM\SOFTWARE\Policies\Microsoft\WindowsFirewall # Without parsing Registry.pol binary, we check for GPOs named with firewall keywords # or that have registry policies $gpos = @($gpoData.GPOs) $firewallGPOs = @($gpos | Where-Object { $_.DisplayName -match 'firewall|fw|network.*(protect|secur)' }) # Also check for GPOs with registry policies that might contain firewall settings $gposWithRegPol = [System.Collections.Generic.List[string]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if ($content -is [hashtable] -and $content.HasRegistryPol -eq $true) { $gposWithRegPol.Add($gpoName) } } if ($firewallGPOs.Count -gt 0) { $names = @($firewallGPOs | ForEach-Object { $_.DisplayName }) $currentValue = "$($firewallGPOs.Count) GPO(s) appear to configure Windows Firewall: $($names -join '; '). Verify firewall is enabled for all profiles with deny-by-default inbound" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ FirewallGPOCount = $firewallGPOs.Count FirewallGPOs = @($names) GPOsWithRegPol = $gposWithRegPol.Count Note = 'Use gpresult or GPO report to verify firewall is enabled for Domain, Private, and Public profiles' } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'No GPOs with obvious Windows Firewall configuration detected. Windows Defender Firewall should be enabled for all profiles (Domain, Private, Public) via GPO with deny-by-default inbound rules' ` -Details @{ GPOsScanned = $sysvolContent.Count GPOsWithRegPol = $gposWithRegPol.Count Note = 'Firewall settings may be in Registry.pol files. Verify with gpresult /h on a representative system' } } # ── ADGPO-020: PowerShell Execution Policy via GPO ────────────────────── function Test-ReconADGPO020 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for PowerShell execution policy analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot verify PowerShell execution policy' } # PowerShell execution policy GPO setting: # Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell > Turn on Script Execution # Registry: HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ExecutionPolicy # Without parsing Registry.pol, we report based on available data and recommend manual check # Check GPO names for PowerShell-related policies $gpos = @($gpoData.GPOs) $psGPOs = @($gpos | Where-Object { $_.DisplayName -match 'powershell|script.*polic|execution.*polic' }) $gposWithRegPol = 0 foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if ($content -is [hashtable] -and $content.HasRegistryPol -eq $true) { $gposWithRegPol++ } } if ($psGPOs.Count -gt 0) { $names = @($psGPOs | ForEach-Object { $_.DisplayName }) $currentValue = "$($psGPOs.Count) GPO(s) appear to manage PowerShell execution policy: $($names -join '; '). Verify policy is set to AllSigned or RemoteSigned, not Unrestricted or Bypass" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ PSPolicyGPOs = @($names) GPOsWithRegPol = $gposWithRegPol Note = 'Verify execution policy via gpresult or by checking the registry key HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell on target systems' } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "No GPOs with obvious PowerShell execution policy configuration found. Execution policy may be set within $gposWithRegPol GPO(s) containing registry policies. Verify PowerShell execution policy is set to AllSigned or RemoteSigned via GPO" ` -Details @{ GPOsScanned = $sysvolContent.Count GPOsWithRegPol = $gposWithRegPol Note = 'Check Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell > Turn on Script Execution' } } # ── ADGPO-021: PowerShell Logging Configuration ───────────────────────── function Test-ReconADGPO021 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for PowerShell logging analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot verify PowerShell logging configuration' } # PowerShell logging GPO settings: # Module Logging: HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging # Script Block Logging: HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging # Transcription: HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription # Without parsing Registry.pol binary, we check for GPOs named with logging/PowerShell keywords $gpos = @($gpoData.GPOs) $loggingGPOs = @($gpos | Where-Object { $_.DisplayName -match 'powershell|logging|audit|monitor|transcript' }) $gposWithRegPol = 0 foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if ($content -is [hashtable] -and $content.HasRegistryPol -eq $true) { $gposWithRegPol++ } } if ($loggingGPOs.Count -gt 0) { $names = @($loggingGPOs | ForEach-Object { $_.DisplayName }) $currentValue = "$($loggingGPOs.Count) GPO(s) may configure PowerShell logging: $($names -join '; '). Verify Module Logging, Script Block Logging, and Transcription are all enabled" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ LoggingGPOs = @($names) GPOsWithRegPol = $gposWithRegPol RequiredSettings = @( 'Module Logging enabled with * for all modules' 'Script Block Logging enabled' 'PowerShell Transcription enabled with secure output directory' ) Note = 'Verify with gpresult or by checking registry keys under HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell on target systems' } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue "No GPOs with obvious PowerShell logging configuration detected. Module Logging, Script Block Logging, and Transcription should be enabled via GPO on all systems to detect PowerShell-based attacks" ` -Details @{ GPOsScanned = $sysvolContent.Count GPOsWithRegPol = $gposWithRegPol RequiredSettings = @( 'Module Logging: Computer Configuration > Admin Templates > Windows Components > Windows PowerShell > Turn on Module Logging' 'Script Block Logging: Computer Configuration > Admin Templates > Windows Components > Windows PowerShell > Turn on PowerShell Script Block Logging' 'Transcription: Computer Configuration > Admin Templates > Windows Components > Windows PowerShell > Turn on PowerShell Transcription' ) } } # ── ADGPO-022: AppLocker/WDAC Policy Assessment ───────────────────────── function Test-ReconADGPO022 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for AppLocker/WDAC analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot check for AppLocker/WDAC policies' } # AppLocker policies are in Registry.pol and also have XML in SYSVOL # WDAC policies may be deployed via Registry.pol or as .p7b files # Check GPO names and preference files for application control indicators $gpos = @($gpoData.GPOs) $appControlGPOs = @($gpos | Where-Object { $_.DisplayName -match 'applocker|app.*control|wdac|application.*whit|SRP|software.*restrict|code.*integrit' }) # Also check for AppLocker XML files in preferences $gposWithAppControl = [System.Collections.Generic.List[string]]::new() foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if (-not ($content -is [hashtable])) { continue } if ($content.PreferenceFiles -and $content.PreferenceFiles.Count -gt 0) { foreach ($prefFile in $content.PreferenceFiles) { if ($prefFile -match 'AppLocker|SRP|CodeIntegrity|\.p7b$') { if (-not $gposWithAppControl.Contains($gpoName)) { $gposWithAppControl.Add($gpoName) } } } } } $totalFound = $appControlGPOs.Count + $gposWithAppControl.Count if ($totalFound -gt 0) { $allNames = [System.Collections.Generic.List[string]]::new() foreach ($gpo in $appControlGPOs) { $allNames.Add($gpo.DisplayName) } foreach ($name in $gposWithAppControl) { if (-not $allNames.Contains($name)) { $allNames.Add($name) } } $currentValue = "$($allNames.Count) GPO(s) appear to configure application control (AppLocker/WDAC/SRP): $($allNames -join '; '). Verify policies are in enforce mode on all targeted systems" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ AppControlGPOs = @($allNames) Note = 'Verify application control policies are in Enforce mode (not Audit Only) using gpresult or Get-AppLockerPolicy on target systems' } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue 'No AppLocker, WDAC, or Software Restriction Policies detected in GPOs. Application control is a critical defense against malware execution and lateral movement' ` -Details @{ GPOsScanned = $sysvolContent.Count Note = 'Deploy AppLocker or Windows Defender Application Control via GPO. Start in audit mode, build a baseline, then transition to enforce mode' } } # ── ADGPO-023: LAPS GPO Configuration ─────────────────────────────────── function Test-ReconADGPO023 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $sysvolContent = $gpoData.SYSVOLContent if (-not $sysvolContent -or $sysvolContent.Count -eq 0) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL content data not available for LAPS configuration analysis' } $allErrors = $true foreach ($key in $sysvolContent.Keys) { $entry = $sysvolContent[$key] if ($entry -is [hashtable] -and -not $entry.ContainsKey('Error')) { $allErrors = $false break } } if ($allErrors) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'SYSVOL not accessible; cannot check for LAPS GPO configuration' } # LAPS GPO settings are in Registry.pol under: # Legacy LAPS: HKLM\SOFTWARE\Policies\Microsoft Services\AdmPwd # Windows LAPS: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\LAPS # Check GPO names for LAPS indicators $gpos = @($gpoData.GPOs) $lapsGPOs = @($gpos | Where-Object { $_.DisplayName -match 'LAPS|local.*admin.*password|AdmPwd' }) $gposWithRegPol = 0 foreach ($gpoName in $sysvolContent.Keys) { $content = $sysvolContent[$gpoName] if ($content -is [hashtable] -and $content.HasRegistryPol -eq $true) { $gposWithRegPol++ } } if ($lapsGPOs.Count -gt 0) { $names = @($lapsGPOs | ForEach-Object { $_.DisplayName }) $linkedCount = @($lapsGPOs | Where-Object { $_.IsLinked }).Count $status = if ($linkedCount -gt 0) { 'PASS' } else { 'WARN' } $currentValue = "$($lapsGPOs.Count) GPO(s) appear to configure LAPS: $($names -join '; '). $linkedCount of $($lapsGPOs.Count) are linked" if ($linkedCount -eq 0) { $currentValue += '. WARNING: None are linked to any OU' } return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status ` -CurrentValue $currentValue ` -Details @{ LAPSGPOs = @($lapsGPOs | ForEach-Object { @{ DisplayName = $_.DisplayName GUID = $_.GUID IsLinked = $_.IsLinked } }) LinkedCount = $linkedCount Note = 'Verify LAPS is enabled with minimum 24-character passwords and 30-day maximum age. Check ms-Mcs-AdmPwdExpirationTime attributes to confirm LAPS is functioning' } } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue "No GPOs with obvious LAPS configuration detected. LAPS settings may be within $gposWithRegPol GPO(s) containing registry policies. Verify LAPS is deployed to all domain-joined systems" ` -Details @{ GPOsScanned = $sysvolContent.Count GPOsWithRegPol = $gposWithRegPol Note = 'Deploy LAPS (legacy or Windows LAPS) via GPO to manage local administrator passwords across all domain-joined systems' } } # ── ADGPO-024: GPO WMI Filter Review ──────────────────────────────────── function Test-ReconADGPO024 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $gpoData = $AuditData.GroupPolicies if (-not $gpoData) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Group Policy data not available' } $wmiFilters = @($gpoData.WMIFilters) if ($wmiFilters.Count -eq 0 -or ($wmiFilters.Count -eq 1 -and $null -eq $wmiFilters[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No WMI filters defined in the domain' ` -Details @{ WMIFilterCount = 0 } } $filterSummary = @($wmiFilters | Where-Object { $_ } | ForEach-Object { @{ Name = $_.Name Description = $_.Description Query = $_.Query WhenCreated = $_.WhenCreated } }) $names = @($filterSummary | ForEach-Object { $_.Name }) $currentValue = "$($filterSummary.Count) WMI filter(s) defined: $($names -join '; '). Review queries for correctness and ensure they do not block security-critical GPOs" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ WMIFilterCount = $filterSummary.Count WMIFilters = @($filterSummary) } } |