# .SYNOPSIS Complete verification of all applied hardening settings .DESCRIPTION Verifies 100% of all settings ALWAYS - regardless of config.json: - 335 Registry settings (Computer + User) [SecurityBaseline] - 67 Security Template settings (79 parsed, 12 metadata excluded) [SecurityBaseline] - 23 Audit Policies [SecurityBaseline] - 19 ASR Rules [ASR] - 5 DNS Checks [DNS] - 67 Privacy Checks [Privacy] - 43 registry (37 Privacy + 6 OneDrive/Store) + 24 bloatware - 32 AntiAI Policies [AntiAI] - includes 4-layer Copilot defense, Recall, Edge Sidebar, CapabilityAccessManager, Explorer AI - 24 Edge Policies [EdgeHardening] - dynamic count based on extensions setting - 50 Advanced Settings [AdvancedSecurity] - optional RDP/AdminShares/UPnP/WirelessDisplay/DiscoveryProtocols/IPv6 decisions are always counted as Pass NOTE: This shows the TRUTH about what is configured in your system. Total: 633 settings (Paranoid mode) SecurityBaseline: 425 (335 Registry + 67 SecTemplate + 23 Audit) ASR: 19 DNS: 5 Privacy: 81 (57 registry Paranoid + 24 bloatware) AntiAI: 32 compliance checks (15 features) EdgeHardening: 24 (22-23 applied depending on extensions) AdvancedSecurity: 50 (15 features incl. Discovery Protocols + IPv6) .NOTES Author: NexusOne23 Version: 2.2.3 #> #Requires -Version 5.1 #Requires -RunAsAdministrator param( [Parameter(Mandatory = $false)] [string]$ExportPath ) $ErrorActionPreference = 'Stop' # Constants for verification counts $EXPECTED_REGISTRY_COUNT = 335 $EXPECTED_SECURITY_COUNT = 67 $EXPECTED_AUDIT_COUNT = 23 $EXPECTED_ASR_COUNT = 19 $EXPECTED_EDGE_COUNT = 24 # 24 total Edge policies from EdgePolicies.json $EXPECTED_ADVANCED_COUNT = 50 # 50 total AdvancedSecurity policy checks (incl. Discovery Protocols WSD/mDNS + IPv6) $EXPECTED_DNS_COUNT = 5 $EXPECTED_PRIVACY_COUNT = 78 # 54 registry from Privacy-MSRecommended.json + 24 bloatware apps $EXPECTED_ANTIAI_COUNT = 32 # 32 AntiAI registry policy checks (15 features) Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host " NoID Privacy - Verification" -ForegroundColor Cyan Write-Host " 100% Complete Settings Check" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" $startTime = Get-Date # Get root path (since script is in Tools/ subdirectory) $rootPath = Split-Path $PSScriptRoot -Parent # VERIFY ALWAYS CHECKS ALL SETTINGS - Regardless of config.json # This shows TRUTH: What is actually configured in the system $totalSettings = $EXPECTED_REGISTRY_COUNT + $EXPECTED_ASR_COUNT + $EXPECTED_DNS_COUNT + $EXPECTED_PRIVACY_COUNT + $EXPECTED_ANTIAI_COUNT + $EXPECTED_EDGE_COUNT + $EXPECTED_ADVANCED_COUNT + $EXPECTED_SECURITY_COUNT + $EXPECTED_AUDIT_COUNT $results = [PSCustomObject]@{ TotalSettings = $totalSettings RegistrySettings = $EXPECTED_REGISTRY_COUNT SecurityTemplate = $EXPECTED_SECURITY_COUNT AuditPolicies = $EXPECTED_AUDIT_COUNT ASRRules = $EXPECTED_ASR_COUNT EdgeHardeningPolicies = $EXPECTED_EDGE_COUNT AdvancedSecuritySettings = $EXPECTED_ADVANCED_COUNT DNSChecks = $EXPECTED_DNS_COUNT PrivacyChecks = $EXPECTED_PRIVACY_COUNT AntiAIPolicies = $EXPECTED_ANTIAI_COUNT Verified = 0 Failed = 0 FailedSettings = @() AllSettings = @() # Track ALL settings for complete HTML report Duration = $null } # Load configuration files $baseConfigPath = Join-Path $rootPath "Modules\SecurityBaseline\ParsedSettings" $asrConfigPath = Join-Path $rootPath "Modules\ASR\Config" # ============================================================================= # HELPER FUNCTION: Extract Registry Checks from JSON Configuration # ============================================================================= # This function recursively parses module JSON files and extracts registry checks. # Supports both Privacy-style (Category > Path > Value) and AntiAI-style (Features > Registry > Path > Value) # # Returns array of: @{ Path = "HKLM:\..."; Name = "ValueName"; Value = expected; Desc = "Description"; Type = "DWord" } # function Get-RegistryChecksFromJson { param( [Parameter(Mandatory = $true)] [string]$JsonPath, [Parameter(Mandatory = $false)] [string[]]$ExcludeCategories = @() ) $checks = @() if (-not (Test-Path $JsonPath)) { Write-Warning "JSON file not found: $JsonPath" return $checks } $config = Get-Content $JsonPath -Raw | ConvertFrom-Json # Recursive function to find registry paths in any JSON structure function Find-RegistrySettings { param($Object, $ParentPath = "") $foundChecks = @() if ($null -eq $Object) { return $foundChecks } foreach ($prop in $Object.PSObject.Properties) { $propName = $prop.Name $propValue = $prop.Value # Skip metadata and excluded categories # NOTE: EnterpriseProtection is NOT skipped - it contains valid registry paths! if ($propName -in @('Mode', 'Description', 'BestFor', 'Warnings', 'Services', 'ScheduledTasks', 'Summary', 'AutomaticallyBlockedByMasterSwitch', 'ModuleName', 'Version', 'TotalFeatures', 'TotalPolicies', 'URIHandlers', 'Note', 'FilePath', 'HostsEntries', 'CloudBased', 'RequiresReboot', 'RequiresADMX', 'Impact', 'Name')) { continue } # Skip excluded categories if ($propName -in $ExcludeCategories) { continue } # Check if this is a registry path (starts with HK) if ($propName -match '^HK(LM|CU|CR|U):\\') { $regPath = $propName # Iterate through values under this registry path if ($propValue -is [PSCustomObject]) { foreach ($valueProp in $propValue.PSObject.Properties) { $valueName = $valueProp.Name $valueDef = $valueProp.Value # Extract expected value and description if ($valueDef -is [PSCustomObject]) { $expectedValue = $null $description = $valueName $valueType = "DWord" # Handle different property names for the value if ($null -ne $valueDef.Value) { $expectedValue = $valueDef.Value } if ($null -ne $valueDef.value) { $expectedValue = $valueDef.value } if ($valueDef.Description) { $description = $valueDef.Description } if ($valueDef.Type) { $valueType = $valueDef.Type } if ($valueDef.type) { $valueType = $valueDef.type } # Only add if we have an expected value if ($null -ne $expectedValue) { $foundChecks += [PSCustomObject]@{ Path = $regPath Name = $valueName Value = $expectedValue Desc = $description Type = $valueType } } } } } } # Recurse into nested objects (Categories, Features, Registry blocks) elseif ($propValue -is [PSCustomObject]) { $foundChecks += Find-RegistrySettings -Object $propValue -ParentPath "$ParentPath/$propName" } } return $foundChecks } $checks = Find-RegistrySettings -Object $config return $checks } # Helper function for testing a single registry value # Supports "at least as strict" logic for Privacy settings function Test-RegistryValue { param( [string]$Path, [string]$Name, $ExpectedValue, [switch]$AllowStricter # If true, stricter values than expected are also accepted ) try { if (Test-Path $Path) { $actual = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name # Handle MultiString arrays if ($ExpectedValue -is [array]) { if ($actual -is [array]) { # Check if all expected items are present (order-independent) $allPresent = $true foreach ($item in $ExpectedValue) { if ($actual -notcontains $item) { $allPresent = $false break } } return $allPresent } return $false } # Exact match if ($actual -eq $ExpectedValue) { return $true } # "At least as strict" logic for Privacy settings # If user has a STRICTER setting than MSRecommended, that's still a PASS if ($AllowStricter -and $null -ne $actual) { # LetApps* settings: 0=User decides, 1=Force Allow, 2=Force Deny # 2 (Force Deny) is stricter than 0 (User decides) if ($Name -like "LetApps*") { if ($ExpectedValue -eq 0 -and $actual -eq 2) { return $true } } # Telemetry/AllowTelemetry: 0=Off, 1=Required, 2=Enhanced, 3=Full # 0 (Off) is stricter than 1 (Required) if ($Name -eq "AllowTelemetry") { if ($ExpectedValue -ge 1 -and $actual -lt $ExpectedValue) { return $true } } # DisableLocation: 0=Enabled, 1=Disabled - 1 is stricter if ($Name -eq "DisableLocation" -or $Name -eq "DisableLocationScripting") { if ($ExpectedValue -eq 0 -and $actual -eq 1) { return $true } } # Sync settings: DisableSettingSync 1=Force Off is stricter than 2=User decides if ($Name -like "*Sync*" -or $Name -like "*SettingSync*") { if ($ExpectedValue -eq 2 -and $actual -eq 1) { return $true } if ($ExpectedValue -eq 0 -and $actual -eq 1) { return $true } } # General disable patterns: 1 (disabled) is often stricter than 0 (enabled) # This covers many privacy settings if ($Name -like "Disable*" -or $Name -like "*Disabled" -or $Name -like "No*") { if ($ExpectedValue -eq 0 -and $actual -eq 1) { return $true } } # General allow patterns: 0 (disabled) is stricter than 1 (enabled) if ($Name -like "Allow*" -or $Name -like "*Allowed" -or $Name -like "Enable*") { if ($ExpectedValue -eq 1 -and $actual -eq 0) { return $true } } } return $false } return $false } catch { return $false } } # Helper function to get actual registry value function Get-ActualRegistryValue { param( [string]$Path, [string]$Name ) try { if (Test-Path $Path) { $actual = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name if ($null -ne $actual) { return $actual } } return "Not set" } catch { return "Error reading" } } $totalSteps = 9 # ALL modules: Registry + Audit + SecTemplate + ASR + DNS + Privacy + AntiAI + EdgeHardening + AdvancedSecurity Write-Host "[1/$totalSteps] Verifying Registry Settings (335)..." -ForegroundColor Yellow try { # Detect if system is domain-joined for standalone adjustments $isDomainJoined = $false try { $computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem $isDomainJoined = ($computerSystem.PartOfDomain -eq $true) } catch { Write-Host " Warning: Could not detect domain membership, assuming standalone" -ForegroundColor Yellow } # Load registry settings $computerSettings = Get-Content (Join-Path $baseConfigPath "Computer-RegistryPolicies.json") -Raw | ConvertFrom-Json $userSettings = Get-Content (Join-Path $baseConfigPath "User-RegistryPolicies.json") -Raw | ConvertFrom-Json $registryFailed = @() $registryPassed = @() # Verify computer settings foreach ($setting in $computerSettings) { # Build full registry path - KeyName has format "[SOFTWARE\..." $keyName = $setting.KeyName -replace '^\[', '' -replace '\]$', '' $keyPath = "Registry::HKEY_LOCAL_MACHINE\$keyName" try { if (Test-Path $keyPath) { $property = Get-ItemProperty -Path $keyPath -Name $setting.ValueName -ErrorAction SilentlyContinue if ($null -ne $property -and $property.PSObject.Properties.Name -contains $setting.ValueName) { $actualValue = $property.$($setting.ValueName) $expectedValue = $setting.Data # Apply standalone workstation adjustments if (-not $isDomainJoined) { # LocalAccountTokenFilterPolicy: 0 (domain) -> 1 (standalone) for remote admin if ($setting.ValueName -eq "LocalAccountTokenFilterPolicy") { $expectedValue = 1 } } # ASR Module Override: PSExec/WMI rule can be upgraded from Audit (2) to Block (1) # Security Baseline sets it to Audit, but ASR module may upgrade to Block if user chose "No management tools" if ($setting.ValueName -eq "d1e49aac-8f56-4280-b9ba-993a6d77406c" -and $expectedValue -eq 2 -and $actualValue -eq 1) { # ASR upgraded from Audit to Block - this is intentional and correct $expectedValue = 1 } if ($actualValue -eq $expectedValue) { $results.Verified++ $registryPassed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $expectedValue Actual = $actualValue } } else { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $expectedValue Actual = $actualValue Reason = "Value mismatch" } } } else { # Check if this is a DELETE operation (**del..., **delvals) # For DELETE operations, "Value not found" means SUCCESS (value was deleted or never existed) if ($setting.ValueName -like "**del*") { $results.Verified++ $registryPassed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = "Deleted/Not present" Actual = "Value not found (Success)" } } else { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = "Value not found" Reason = "Value does not exist" } } } } else { # Check if this is a DELETE operation (**del..., **delvals) # For DELETE operations, "Key not found" means SUCCESS (key was deleted or never existed) if ($setting.ValueName -like "**del*") { $results.Verified++ $registryPassed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = "Deleted/Not present" Actual = "Key not found (Success)" } } else { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = "Key not found" Reason = "Key does not exist" } } } } catch { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = "Error" Reason = $_.Exception.Message } } } # Verify user settings foreach ($setting in $userSettings) { # Build full registry path - KeyName has format "[SOFTWARE\..." $keyName = $setting.KeyName -replace '^\[', '' -replace '\]$', '' $keyPath = "Registry::HKEY_CURRENT_USER\$keyName" try { if (Test-Path $keyPath) { $property = Get-ItemProperty -Path $keyPath -Name $setting.ValueName -ErrorAction SilentlyContinue if ($null -ne $property -and $property.PSObject.Properties.Name -contains $setting.ValueName) { $actualValue = $property.$($setting.ValueName) if ($actualValue -eq $setting.Data) { $results.Verified++ $registryPassed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = $actualValue } } else { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = $actualValue Reason = "Value mismatch" } } } else { # Check if this is a DELETE operation (**del..., **delvals) # For DELETE operations, "Value not found" means SUCCESS (value was deleted or never existed) if ($setting.ValueName -like "**del*") { $results.Verified++ $registryPassed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = "Deleted/Not present" Actual = "Value not found (Success)" } } else { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = "Value not found" Reason = "Value does not exist" } } } } else { # Check if this is a DELETE operation (**del..., **delvals) # For DELETE operations, "Key not found" means SUCCESS (key was deleted or never existed) if ($setting.ValueName -like "**del*") { $results.Verified++ $registryPassed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = "Deleted/Not present" Actual = "Key not found (Success)" } } else { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = "Key not found" Reason = "Key does not exist" } } } } catch { $results.Failed++ $registryFailed += [PSCustomObject]@{ Path = $keyPath Name = $setting.ValueName Expected = $setting.Data Actual = "Error" Reason = $_.Exception.Message } } } # Add to AllSettings for HTML report (with category summary) $registryPassedCount = $results.RegistrySettings - $registryFailed.Count $results.AllSettings += [PSCustomObject]@{ Category = "Registry" Total = $results.RegistrySettings Passed = $registryPassedCount Failed = $registryFailed.Count PassedDetails = $registryPassed FailedDetails = $registryFailed } if ($registryFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "Registry" Count = $registryFailed.Count Details = $registryFailed } } Write-Host " Registry: $($results.RegistrySettings - $registryFailed.Count)/$($results.RegistrySettings) verified" -ForegroundColor $(if ($registryFailed.Count -eq 0) { "Green" } else { "Yellow" }) } catch { Write-Host " ERROR: $_" -ForegroundColor Red } Write-Host "[2/$totalSteps] Verifying Audit Policies (23)..." -ForegroundColor Yellow try { # Get current audit policies $auditOutput = auditpol /get /category:* /r | ConvertFrom-Csv # Load expected audit policies $auditSettings = Get-Content (Join-Path $baseConfigPath "AuditPolicies.json") -Raw | ConvertFrom-Json $auditFailed = @() $auditPassed = @() foreach ($policy in $auditSettings) { # Skip if Subcategory is null or empty if ([string]::IsNullOrWhiteSpace($policy.Subcategory)) { continue } # Use GUID directly from JSON (already includes braces and correct case) $guid = $policy.SubcategoryGUID if ($guid) { # Language-independent column detection # Find column containing "GUID" (works for English, German, French, etc.) $guidColumn = ($auditOutput[0].PSObject.Properties.Name | Where-Object { $_ -like "*GUID*" }) | Select-Object -First 1 # Find column for inclusion setting (various languages) # English: "Inclusion Setting", German: "Aufnahmeeinstellung", etc. $inclusionColumn = ($auditOutput[0].PSObject.Properties.Name | Where-Object { $_ -like "*Inclusion*" -or $_ -like "*Aufnahme*" -or $_ -like "*Setting*" }) | Select-Object -First 1 if (-not $guidColumn -or -not $inclusionColumn) { Write-Host " WARNING: Could not detect CSV column names - language compatibility issue" -ForegroundColor Yellow continue } # Case-insensitive comparison for GUID matching $currentPolicy = $auditOutput | Where-Object { $_.$guidColumn -eq $guid } if ($currentPolicy) { # Use language-independent numeric comparison # SettingValue: 0=No Auditing, 1=Success, 2=Failure, 3=Success and Failure $expectedValue = [int]$policy.SettingValue # Get actual value from auditpol output # Convert text to numeric (language-independent) $actualText = $currentPolicy.$inclusionColumn $actualValue = 0 # auditpol text values are language-specific, so check all possibilities # English: Success, Failure, Success and Failure, No Auditing # German: Erfolg, Fehler, Erfolg und Fehler, Keine Ueberwachung if ($actualText -match "Success.*Failure|Erfolg.*Fehler") { $actualValue = 3 } elseif ($actualText -match "Success|Erfolg") { $actualValue = 1 } elseif ($actualText -match "Failure|Fehler") { $actualValue = 2 } else { $actualValue = 0 } if ($actualValue -eq $expectedValue) { $results.Verified++ $auditPassed += [PSCustomObject]@{ Policy = $policy.Subcategory Expected = $policy.SettingValue Actual = $actualValue } } else { $results.Failed++ $auditFailed += [PSCustomObject]@{ Policy = $policy.Subcategory Expected = $policy.SettingValue Actual = $actualValue GUID = $guid } } } else { # Policy not found - this should never happen unless GUID mismatch # Treat as "No Auditing" (most likely state if not explicitly configured) $results.Failed++ $auditFailed += [PSCustomObject]@{ Policy = $policy.Subcategory Expected = $policy.InclusionSetting Actual = "No Auditing" GUID = $guid } } } else { # GUID is empty/null - this policy will be skipped } } # Add to AllSettings for HTML report $auditPassedCount = $results.AuditPolicies - $auditFailed.Count $results.AllSettings += [PSCustomObject]@{ Category = "AuditPolicies" Total = $results.AuditPolicies Passed = $auditPassedCount Failed = $auditFailed.Count PassedDetails = $auditPassed FailedDetails = $auditFailed } if ($auditFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "AuditPolicies" Count = $auditFailed.Count Details = $auditFailed } } Write-Host " Audit Policies: $($results.AuditPolicies - $auditFailed.Count)/$($results.AuditPolicies) verified" -ForegroundColor $(if ($auditFailed.Count -eq 0) { "Green" } else { "Yellow" }) } catch { Write-Host " ERROR: $_" -ForegroundColor Red } Write-Host "[3/$totalSteps] Verifying Security Template Settings (67)..." -ForegroundColor Yellow try { # Export current security settings $tempFile = Join-Path $env:TEMP "current_secedit.inf" secedit /export /cfg $tempFile /quiet | Out-Null # Load expected settings $expectedSettings = Get-Content (Join-Path $baseConfigPath "SecurityTemplates.json") -Raw | ConvertFrom-Json # Parse secedit output $currentSettings = Get-Content $tempFile $securityFailed = @() $securityPassed = @() $securityVerified = 0 # Check if domain-joined $isDomainJoined = (Get-CimInstance Win32_ComputerSystem).PartOfDomain # Verify each GPO foreach ($gpoName in $expectedSettings.PSObject.Properties.Name) { # Note: We do NOT skip Domain Security on standalone! # The standalone delta modifies 1 setting (LocalAccountTokenFilterPolicy), # but all 67 settings are still applied and should be verified. $gpo = $expectedSettings.$gpoName foreach ($sectionName in $gpo.PSObject.Properties.Name) { # Skip metadata sections (Unicode, Version) if ($sectionName -in @("Unicode", "Version")) { continue } $section = $gpo.$sectionName # Iterate through actual settings in this section foreach ($settingProperty in $section.PSObject.Properties) { $settingName = $settingProperty.Name $expectedValue = $settingProperty.Value # Find in current settings - look in the matching INI section $inSection = $false $actualValue = $null foreach ($line in $currentSettings) { # Check if we're in the right section if ($line -match "^\[$sectionName\]") { $inSection = $true continue } elseif ($line -match "^\[") { $inSection = $false } # If in right section, look for setting $escapedName = [regex]::Escape($settingName) if ($inSection -and $line -match "^$escapedName\s*=") { $actualValue = ($line -split '=', 2)[1].Trim() break } } if ($null -ne $actualValue) { # Special handling for Privilege Rights - compare SID sets (order-independent) $isMatch = $false if ($sectionName -eq "Privilege Rights") { # Split SIDs and compare as sets $expectedSIDs = $expectedValue -split ',' | ForEach-Object { $_.Trim() } | Sort-Object $actualSIDs = $actualValue -split ',' | ForEach-Object { $_.Trim() } | Sort-Object # Compare arrays (order-independent) if ($expectedSIDs.Count -eq $actualSIDs.Count) { $isMatch = $true for ($i = 0; $i -lt $expectedSIDs.Count; $i++) { if ($expectedSIDs[$i] -ne $actualSIDs[$i]) { $isMatch = $false break } } } } else { # Normal string comparison for non-Privilege Rights $isMatch = ($actualValue -eq $expectedValue) } if ($isMatch) { $securityVerified++ $results.Verified++ $securityPassed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = $expectedValue Actual = $actualValue } } else { $results.Failed++ $securityFailed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = $expectedValue Actual = $actualValue } } } else { # Setting not found in secedit output # There are legitimate cases where "Not found" = SUCCESS: # 1. Xbox services may not exist on clean installations $xboxServices = @("XboxGipSvc", "XblAuthManager", "XblGameSave", "XboxNetApiSvc") if ($sectionName -eq "Service General Setting" -and $settingName -in $xboxServices) { $securityVerified++ $results.Verified++ $securityPassed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = $expectedValue Actual = "Not found (Xbox service not installed - OK)" } } # 2. Privilege Rights with empty expected value (nobody should have this right) # If secedit doesn't list it, it means nobody has it = SUCCESS elseif ($sectionName -eq "Privilege Rights" -and [string]::IsNullOrEmpty($expectedValue)) { $securityVerified++ $results.Verified++ $securityPassed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = "Empty (nobody has right)" Actual = "Not found (Success)" } } # 3. Privilege Rights that are edition/domain-specific and may not exist # These are NOT APPLICABLE on standalone/non-Enterprise systems elseif ($sectionName -eq "Privilege Rights") { $editionSpecificRights = @( "SeEnableDelegationPrivilege", # Enterprise/Domain only "SeTrustedCredManAccessPrivilege", # May not exist on Home "SeRelabelPrivilege", # May not exist on Home "SeSyncAgentPrivilege" # Domain controllers only ) # Domain-specific rights that don't apply to standalone systems # These deny local admin accounts (*S-1-5-113) from remote/network access # On standalone, local admins ARE the only admins, so denying them makes no sense $domainOnlyDenyRights = @( "SeDenyRemoteInteractiveLogonRight", # Deny RDP for local admins (Domain-only) "SeDenyNetworkLogonRight" # Deny network logon for local admins (Domain-only) ) if ($settingName -in $editionSpecificRights) { # Edition-specific right not found = N/A (treat as success) $securityVerified++ $results.Verified++ $securityPassed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = $expectedValue Actual = "Not found (Edition-specific - N/A)" } } elseif (-not $isDomainJoined -and $settingName -in $domainOnlyDenyRights) { # Domain-only deny rights on standalone system = N/A (treat as success) # These settings are meant to separate Domain Admins from Local Admins # On standalone, there are no Domain Admins, so these don't apply $securityVerified++ $results.Verified++ $securityPassed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = $expectedValue Actual = "Not found (Domain-only on standalone - N/A)" } } else { # This privilege SHOULD exist on all editions - it's missing! $results.Failed++ $securityFailed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = $expectedValue Actual = "Not found (should exist on this edition)" } } } else { $results.Failed++ $securityFailed += [PSCustomObject]@{ GPO = $gpoName Section = $sectionName Setting = $settingName Expected = $expectedValue Actual = "Not found" } } } } } } # Add to AllSettings for HTML report $securityPassedCount = $results.SecurityTemplate - $securityFailed.Count $results.AllSettings += [PSCustomObject]@{ Category = "SecurityTemplate" Total = $results.SecurityTemplate Passed = $securityPassedCount Failed = $securityFailed.Count PassedDetails = $securityPassed FailedDetails = $securityFailed } if ($securityFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "SecurityTemplate" Count = $securityFailed.Count Details = $securityFailed } } Remove-Item $tempFile -Force -ErrorAction SilentlyContinue Write-Host " Security Template: $securityVerified/$($results.SecurityTemplate) verified" -ForegroundColor $(if ($securityFailed.Count -eq 0) { "Green" } else { "Yellow" }) } catch { Write-Host " ERROR: $_" -ForegroundColor Red } Write-Host "[4/$totalSteps] Verifying ASR Rules (19)..." -ForegroundColor Yellow try { # Check if Windows Defender is active or if third-party AV is managing security $thirdPartyAV = $null $defenderManaged = $true try { # Check for third-party AV products $avProducts = Get-CimInstance -Namespace "root/SecurityCenter2" -ClassName "AntiVirusProduct" -ErrorAction SilentlyContinue $thirdPartyAV = $avProducts | Where-Object { $_.displayName -notmatch "Windows Defender|Microsoft Defender" } | Select-Object -First 1 if ($thirdPartyAV) { # Try to access Defender - if it fails, third-party AV is managing try { $null = Get-MpPreference -ErrorAction Stop } catch { $defenderManaged = $false } } } catch { # SecurityCenter2 not available - assume Defender is active $null = $null # Intentionally empty - suppress PSScriptAnalyzer warning } # If third-party AV is managing ASR (Defender unavailable) if (-not $defenderManaged -and $thirdPartyAV) { Write-Host " Third-party AV detected: $($thirdPartyAV.displayName)" -ForegroundColor Cyan Write-Host " ASR rules are managed by your antivirus solution" -ForegroundColor Green # Count all ASR rules as verified (AV is handling protection) $results.Verified += $EXPECTED_ASR_COUNT $results.AllSettings += [PSCustomObject]@{ Category = "ASR" Total = $EXPECTED_ASR_COUNT Passed = $EXPECTED_ASR_COUNT Failed = 0 PassedDetails = @([PSCustomObject]@{ Rule = "All rules"; Expected = "Managed by $($thirdPartyAV.displayName)"; Actual = "Protected" }) FailedDetails = @() } Write-Host " ASR: $EXPECTED_ASR_COUNT/$EXPECTED_ASR_COUNT verified (Third-Party AV)" -ForegroundColor Green } else { # Defender is active - verify ASR rules normally $mpPreference = Get-MpPreference $currentASRIds = $mpPreference.AttackSurfaceReductionRules_Ids $currentASRActions = $mpPreference.AttackSurfaceReductionRules_Actions # Load expected ASR rules - JSON is array directly $asrRules = Get-Content (Join-Path $asrConfigPath "ASR-Rules.json") -Raw | ConvertFrom-Json $asrFailed = @() $asrPassed = @() # Check if ASR rules are configured at all if ($null -eq $currentASRIds -or $currentASRIds.Count -eq 0) { # No ASR rules configured - mark all as failed foreach ($rule in $asrRules) { $results.Failed++ $expectedActionText = if ($rule.Action -eq 1) { "Block" } elseif ($rule.Action -eq 2) { "Audit" } else { "Disabled" } $asrFailed += [PSCustomObject]@{ Rule = $rule.Name GUID = $rule.GUID Expected = $expectedActionText Actual = "Not configured" } } } else { # Rules where both BLOCK (1) and AUDIT (2) are considered "Pass" # These are user-configurable rules where either mode is valid $flexibleRules = @( "d1e49aac-8f56-4280-b9ba-993a6d77406c", # PSExec/WMI (Management Tools) "01443614-cd74-433a-b99e-2ecdc07bfc25" # Prevalence (New/Unknown Software) ) foreach ($rule in $asrRules) { # Case-insensitive GUID matching (Get-MpPreference may return different case) $index = -1 for ($i = 0; $i -lt $currentASRIds.Count; $i++) { if ($currentASRIds[$i] -eq $rule.GUID) { $index = $i break } } if ($index -ge 0) { $actualAction = $currentASRActions[$index] $expectedAction = $rule.Action # Check if this is a flexible rule (Block or Audit both count as Pass) $isFlexibleRule = $flexibleRules -contains $rule.GUID $isActiveMode = $actualAction -in @(1, 2) # Block or Audit # For flexible rules: Pass if Block OR Audit # For other rules: Pass only if exact match $rulePassed = if ($isFlexibleRule) { $isActiveMode } else { $actualAction -eq $expectedAction } if ($rulePassed) { $results.Verified++ $actionText = if ($actualAction -eq 1) { "Block" } elseif ($actualAction -eq 2) { "Audit" } else { "Disabled" } $asrPassed += [PSCustomObject]@{ Rule = $rule.Name Expected = $actionText Actual = $actionText } } else { $results.Failed++ $expectedActionText = if ($expectedAction -eq 1) { "Block" } elseif ($expectedAction -eq 2) { "Audit" } else { "Disabled" } $actualActionText = if ($actualAction -eq 1) { "Block" } elseif ($actualAction -eq 2) { "Audit" } else { "Disabled" } $asrFailed += [PSCustomObject]@{ Rule = $rule.Name GUID = $rule.GUID Expected = $expectedActionText Actual = $actualActionText } } } else { $results.Failed++ $expectedActionText = if ($rule.Action -eq 1) { "Block" } elseif ($rule.Action -eq 2) { "Audit" } else { "Disabled" } $asrFailed += [PSCustomObject]@{ Rule = $rule.Name GUID = $rule.GUID Expected = $expectedActionText Actual = "Not configured" } } } } # Add to AllSettings for HTML report $asrPassedCount = $results.ASRRules - $asrFailed.Count $results.AllSettings += [PSCustomObject]@{ Category = "ASR" Total = $results.ASRRules Passed = $asrPassedCount Failed = $asrFailed.Count PassedDetails = $asrPassed FailedDetails = $asrFailed } if ($asrFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "ASR" Count = $asrFailed.Count Details = $asrFailed } } Write-Host " ASR Rules: $($results.ASRRules - $asrFailed.Count)/$($results.ASRRules) verified" -ForegroundColor $(if ($asrFailed.Count -eq 0) { "Green" } else { "Yellow" }) } # End of else (Defender active) } catch { Write-Host " ERROR: $_" -ForegroundColor Red } # [ALWAYS] DNS Configuration (5 checks) Write-Host "[5/$totalSteps] Verifying DNS Configuration (5 checks)..." -ForegroundColor Yellow try { $dnsFailed = @() $dnsPassed = @() # Get all physical network adapters (including Disconnected for offline verification) $adapters = Get-NetAdapter | Where-Object { ($_.Status -eq 'Up' -or $_.Status -eq 'Disconnected') -and $_.Virtual -eq $false } # Ensure $adapters is an array (even if empty) if ($null -eq $adapters) { $adapters = @() } elseif ($adapters -isnot [array]) { $adapters = @($adapters) } if ($adapters.Count -eq 0) { Write-Host " DNS: No physical adapters - marking all 5 checks as FAILED" -ForegroundColor Yellow # CRITICAL: Must count all 5 DNS checks as Failed when no adapters exist! $results.Failed += 5 $dnsFailed += [PSCustomObject]@{ Check = "DNS Configuration (All 5 checks)" Expected = "Physical network adapter required" Actual = "No active physical adapters found" } } else { # Known secure DNS providers used by the framework (IPv4 + IPv6) $knownDNSv4 = @('1.1.1.1', '1.0.0.1', '9.9.9.9', '149.112.112.112', '94.140.14.14', '94.140.15.15') $knownDNSv6 = @('2606:4700:4700::1111', '2606:4700:4700::1001', '2620:fe::fe', '2620:fe::9', '2a10:50c0::ad1:ff', '2a10:50c0::ad2:ff') $knownDNSAll = $knownDNSv4 + $knownDNSv6 # Collect current IPv4 DNS servers on physical adapters $configuredDnsV4 = @() foreach ($adapter in $adapters) { $dnsInfo = Get-DnsClientServerAddress -InterfaceAlias $adapter.Name -AddressFamily IPv4 -ErrorAction SilentlyContinue if ($dnsInfo) { foreach ($entry in $dnsInfo) { if ($entry.ServerAddresses) { $configuredDnsV4 += $entry.ServerAddresses } } } } $configuredDnsV4 = $configuredDnsV4 | Where-Object { $_ } | Select-Object -Unique # Collect DoH configuration for known provider IPs $dohSettings = $null $providerDohEntries = @() try { $dohSettings = Get-DnsClientDohServerAddress -ErrorAction SilentlyContinue } catch { $dohSettings = $null } if ($dohSettings) { $providerDohEntries = $dohSettings | Where-Object { $knownDNSAll -contains $_.ServerAddress } } # Check 1: DNS Servers (IPv4) from supported secure providers $dnsConfigured = $false if ($configuredDnsV4 | Where-Object { $knownDNSv4 -contains $_ }) { $dnsConfigured = $true } if ($dnsConfigured) { $results.Verified++ $providerNames = ($configuredDnsV4 | Where-Object { $knownDNSv4 -contains $_ }) -join ', ' $dnsPassed += [PSCustomObject]@{ Check = "DNS Servers (IPv4)" Expected = "Cloudflare/Quad9/AdGuard" Actual = $providerNames } } else { $results.Failed++ $dnsFailed += [PSCustomObject]@{ Check = "DNS Servers (IPv4)" Expected = "Cloudflare/Quad9/AdGuard" Actual = "Not configured or DHCP" } } # Check 2: DNS over HTTPS (DoH) configured for provider servers $dohConfigured = $false if ($providerDohEntries -and $providerDohEntries.Count -gt 0) { $dohConfigured = $true } else { # Fallback: Check global DoH registry for known provider IPv4 addresses $dohRegPath = "HKLM:\System\CurrentControlSet\Services\Dnscache\Parameters\DohInterfaceSettings\Doh" if (Test-Path $dohRegPath) { $dohKeys = Get-ChildItem -Path $dohRegPath -ErrorAction SilentlyContinue foreach ($key in $dohKeys) { if ($knownDNSv4 -contains $key.PSChildName) { $dohFlags = (Get-ItemProperty -Path $key.PSPath -Name "DohFlags" -ErrorAction SilentlyContinue).DohFlags if ($dohFlags -ge 1) { # 1 = Encrypted Only, 2 = Encrypted Preferred $dohConfigured = $true break } } } } } if ($dohConfigured) { $results.Verified++ $dnsPassed += [PSCustomObject]@{ Check = "DNS over HTTPS (DoH)" Expected = "Enabled (Cloudflare/Quad9/AdGuard)" Actual = "Enabled" } } else { $results.Failed++ $dnsFailed += [PSCustomObject]@{ Check = "DNS over HTTPS (DoH)" Expected = "Enabled (Cloudflare/Quad9/AdGuard)" Actual = "Not configured" } } # Check 3: DoH policy / fallback consistency (REQUIRE vs ALLOW) $policyOk = $false $policyValue = $null try { $dnsClientPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" $policyValue = (Get-ItemProperty -Path $dnsClientPolicyPath -Name "DoHPolicy" -ErrorAction SilentlyContinue).DoHPolicy } catch { $policyValue = $null } if ($dohConfigured -and $policyValue) { switch ([int]$policyValue) { 3 { # REQUIRE mode: all provider DoH entries must have fallback disabled if ($providerDohEntries -and $providerDohEntries.Count -gt 0) { $fallbackFlags = $providerDohEntries | Select-Object -ExpandProperty AllowFallbackToUdp if ($fallbackFlags -and ($fallbackFlags -notcontains $true)) { $policyOk = $true } } } 2 { # ALLOW mode: presence of provider DoH entries is sufficient (user explicitly allowed fallback) if ($providerDohEntries -and $providerDohEntries.Count -gt 0) { $policyOk = $true } } } } if ($policyOk) { $results.Verified++ $policyText = if ([int]$policyValue -eq 3) { "REQUIRE (no fallback)" } else { "ALLOW (with fallback)" } $dnsPassed += [PSCustomObject]@{ Check = "DoH Policy / Fallback" Expected = "REQUIRE (no fallback) or ALLOW with valid provider DoH" Actual = $policyText } } else { $results.Failed++ $dnsFailed += [PSCustomObject]@{ Check = "DoH Policy / Fallback" Expected = "REQUIRE (no fallback) or ALLOW with valid provider DoH" Actual = "Policy missing, unsupported value, or inconsistent with DoH servers" } } # Check 4: DNS connectivity (configured provider servers preferred) $dnsResponds = $false $testDNS = @() if ($configuredDnsV4) { $testDNS = $configuredDnsV4 | Where-Object { $knownDNSv4 -contains $_ } | Select-Object -Unique } if (-not $testDNS) { # Fallback to standard list if no provider DNS is currently configured $testDNS = @('1.1.1.1', '9.9.9.9', '94.140.14.14') } foreach ($dns in $testDNS) { $ping = Test-Connection -ComputerName $dns -Count 1 -Quiet -ErrorAction SilentlyContinue if ($ping) { $dnsResponds = $true break } } if ($dnsResponds) { $results.Verified++ $dnsPassed += [PSCustomObject]@{ Check = "DNS Connectivity" Expected = "At least one DNS server responds" Actual = "DNS server reachable" } } else { $results.Failed++ $dnsFailed += [PSCustomObject]@{ Check = "DNS Connectivity" Expected = "At least one DNS server responds" Actual = "No response (offline or blocked)" } } # Check 5: Static DNS configuration (manual, not DHCP) $staticDNS = $false # In ALLOW mode (DoHPolicy = 2) with valid provider DoH configuration ($policyOk), # static DNS is considered optional (VPN/mobile/enterprise scenarios). if ($policyOk -and $policyValue -and [int]$policyValue -eq 2) { $staticDNS = $true } else { foreach ($adapter in $adapters) { $dnsInfo = Get-DnsClientServerAddress -InterfaceIndex $adapter.InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue if ($dnsInfo) { foreach ($entry in $dnsInfo) { # Accept 'Manual', 'Static', or any configured DNS that matches known providers # Windows may report 'Static' or 'Manual' depending on timing and method if ($entry.ServerAddresses -and $entry.ServerAddresses.Count -gt 0) { # Check if it's not DHCP (empty or localhost fallback) $isDHCP = ($entry.ServerAddresses.Count -eq 0) -or ($entry.ServerAddresses -contains '127.0.0.1') -or ($entry.AddressOrigin -eq 'DHCP') if (-not $isDHCP) { $staticDNS = $true break } } } } if ($staticDNS) { break } } } if ($staticDNS) { $results.Verified++ $staticReason = if ($policyOk -and $policyValue -and [int]$policyValue -eq 2) { "ALLOW mode (optional)" } else { "Manual configuration" } $dnsPassed += [PSCustomObject]@{ Check = "Static DNS Configuration" Expected = "Static DNS servers configured" Actual = $staticReason } } else { $results.Failed++ $dnsFailed += [PSCustomObject]@{ Check = "Static DNS Configuration" Expected = "Static DNS servers configured" Actual = "DNS from DHCP or not configured" } } } # Add to AllSettings for HTML report $dnsPassedCount = $results.DNSChecks - $dnsFailed.Count $results.AllSettings += [PSCustomObject]@{ Category = "DNS" Total = $results.DNSChecks Passed = $dnsPassedCount Failed = $dnsFailed.Count PassedDetails = $dnsPassed FailedDetails = $dnsFailed } if ($dnsFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "DNS" Count = $dnsFailed.Count Details = $dnsFailed } } Write-Host " DNS: $($results.DNSChecks - $dnsFailed.Count)/$($results.DNSChecks) verified" -ForegroundColor $(if ($dnsFailed.Count -eq 0) { "Green" } else { "Yellow" }) } catch { Write-Host " ERROR: $_" -ForegroundColor Red } # [ALWAYS] Privacy Compliance Checks (loaded dynamically from JSON) # Source: Privacy-MSRecommended.json (registry settings) + Bloatware apps list Write-Host "[6/$totalSteps] Verifying Privacy Compliance ($EXPECTED_PRIVACY_COUNT)..." -ForegroundColor Yellow try { $privacyFailed = @() $privacyPassed = @() # ========================================================================== # LOAD REGISTRY CHECKS FROM Privacy-MSRecommended.json (Single Source of Truth) # ========================================================================== $privacyJsonPath = Join-Path $rootPath "Modules\Privacy\Config\Privacy-MSRecommended.json" $privacyChecks = Get-RegistryChecksFromJson -JsonPath $privacyJsonPath # Verify each registry setting from JSON # Uses -AllowStricter to accept values that are MORE restrictive than MSRecommended baseline # Example: If baseline says "User decides" (0) but system has "Force Deny" (2), that's stricter = PASS foreach ($check in $privacyChecks) { # Convert JSON path format (HKLM:\\...) to PowerShell format (HKLM:\...) $regPath = $check.Path -replace '\\\\', '\' # -AllowStricter: Accept stricter values than baseline (e.g., Strict/Paranoid profile applied) $passed = Test-RegistryValue -Path $regPath -Name $check.Name -ExpectedValue $check.Value -AllowStricter $actual = Get-ActualRegistryValue -Path $regPath -Name $check.Name if ($passed) { $results.Verified++ $privacyPassed += [PSCustomObject]@{ Setting = $check.Desc Path = "$regPath\$($check.Name)" Expected = "$($check.Value) (or stricter)" Actual = $actual } } else { $results.Failed++ $privacyFailed += [PSCustomObject]@{ Setting = $check.Desc Path = "$regPath\$($check.Name)" Expected = $check.Value Actual = $actual } } } # ========================================================================== # BLOATWARE CHECKS (loaded from Bloatware.json) # ========================================================================== $bloatwareJsonPath = Join-Path $rootPath "Modules\Privacy\Config\Bloatware.json" $bloatwareApps = @() if (Test-Path $bloatwareJsonPath) { $bloatwareConfig = Get-Content $bloatwareJsonPath -Raw | ConvertFrom-Json # Extract app names from ClassicMethod.RemoveApps list if ($bloatwareConfig.ClassicMethod -and $bloatwareConfig.ClassicMethod.RemoveApps) { $bloatwareApps = $bloatwareConfig.ClassicMethod.RemoveApps } } # Fallback if JSON not found or empty if ($bloatwareApps.Count -eq 0) { $bloatwareApps = @( 'Microsoft.BingNews', 'Microsoft.BingWeather', 'Microsoft.MicrosoftSolitaireCollection', 'Microsoft.MicrosoftStickyNotes', 'Microsoft.GamingApp', 'Microsoft.XboxApp', 'Microsoft.XboxGamingOverlay', 'Microsoft.XboxIdentityProvider', 'Microsoft.XboxSpeechToTextOverlay', 'Microsoft.Xbox.TCUI', 'Microsoft.ZuneMusic', 'Microsoft.ZuneVideo', 'Microsoft.WindowsFeedbackHub', 'Microsoft.GetHelp', 'Microsoft.Getstarted', 'Microsoft.MixedReality.Portal', 'Microsoft.People', 'Microsoft.YourPhone', 'Clipchamp.Clipchamp', 'SpotifyAB.SpotifyMusic', 'TikTok.TikTok', 'king.com.CandyCrushSaga', 'Disney.DisneyPlus', 'Facebook.Facebook' ) } # Apps that are intentionally NOT removed (cannot be reinstalled via winget msstore) $nonRestorableApps = @('Microsoft.Xbox.TCUI', 'Microsoft.XboxSpeechToTextOverlay', 'Microsoft.MicrosoftSolitaireCollection') foreach ($app in $bloatwareApps) { $isInstalled = Get-AppxPackage -Name $app -ErrorAction SilentlyContinue # Non-restorable apps: Pass regardless of installed state (intentionally kept) if ($nonRestorableApps -contains $app) { $results.Verified++ $privacyPassed += [PSCustomObject]@{ Setting = "Bloatware: $app" Path = "AppxPackage" Expected = "Kept (Non-Restorable)" Actual = if ($null -eq $isInstalled) { "Not installed" } else { "Kept (not in winget msstore)" } } continue } if ($null -eq $isInstalled) { $results.Verified++ # App removed = success $privacyPassed += [PSCustomObject]@{ Setting = "Bloatware: $app" Path = "AppxPackage" Expected = "Removed" Actual = "Not installed (Success)" } } else { $results.Failed++ $privacyFailed += [PSCustomObject]@{ Setting = "Bloatware: $app" Path = "AppxPackage" Expected = "Removed" Actual = "Still installed" } } } # Calculate totals $registryCheckCount = $privacyChecks.Count $actualPrivacyTotal = $registryCheckCount + $bloatwareApps.Count $privacyPassedCount = $actualPrivacyTotal - $privacyFailed.Count # Add to AllSettings for HTML report $results.AllSettings += [PSCustomObject]@{ Category = "Privacy" Total = $actualPrivacyTotal Passed = $privacyPassedCount Failed = $privacyFailed.Count PassedDetails = $privacyPassed FailedDetails = $privacyFailed } if ($privacyFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "Privacy" Count = $privacyFailed.Count Details = $privacyFailed } } Write-Host " Privacy: $privacyPassedCount/$actualPrivacyTotal verified ($registryCheckCount registry + $($bloatwareApps.Count) bloatware)" -ForegroundColor $(if ($privacyFailed.Count -eq 0) { "Green" } else { "Yellow" }) # Update global results object with actual Privacy count $results.PrivacyChecks = $actualPrivacyTotal } catch { Write-Host " ERROR: $_" -ForegroundColor Red } # [ALWAYS] AntiAI Policies (loaded dynamically from JSON) # Source: AntiAI-Settings.json (Single Source of Truth) Write-Host "[7/$totalSteps] Verifying AntiAI Policies ($EXPECTED_ANTIAI_COUNT)..." -ForegroundColor Yellow try { $antiAIFailed = @() $antiAIPassed = @() # ========================================================================== # LOAD REGISTRY CHECKS FROM AntiAI-Settings.json (Single Source of Truth) # ========================================================================== $antiAIJsonPath = Join-Path $rootPath "Modules\AntiAI\Config\AntiAI-Settings.json" $antiAIChecks = Get-RegistryChecksFromJson -JsonPath $antiAIJsonPath # Verify each AntiAI registry setting from JSON # MultiString policies count as 1 check (consistent with Test-AntiAICompliance.ps1) $actualCheckCount = 0 foreach ($check in $antiAIChecks) { # Convert JSON path format (HKLM:\\...) to PowerShell format (HKLM:\...) $regPath = $check.Path -replace '\\\\', '\' $actualCheckCount++ if ($check.Value -is [array]) { # MultiString-Policies: count as 1 check, PASS if all expected items present $actual = $null try { if (Test-Path $regPath) { $prop = Get-ItemProperty -Path $regPath -Name $check.Name -ErrorAction SilentlyContinue if ($null -ne $prop -and $prop.PSObject.Properties.Name -contains $check.Name) { $actual = $prop.$($check.Name) } } } catch { $actual = $null } $actualArray = @() if ($actual -is [array]) { $actualArray = $actual } elseif ($null -ne $actual) { $actualArray = @($actual) } # Check if ALL expected items are present $allPresent = $true foreach ($item in $check.Value) { if ($actualArray -notcontains $item) { $allPresent = $false break } } if ($allPresent) { $results.Verified++ $antiAIPassed += [PSCustomObject]@{ Policy = $check.Desc Path = "$regPath\$($check.Name)" Expected = "$($check.Value.Count) items" Actual = "$($actualArray.Count) items" } } else { $results.Failed++ $antiAIFailed += [PSCustomObject]@{ Policy = $check.Desc Path = "$regPath\$($check.Name)" Expected = "$($check.Value.Count) items" Actual = if ($actualArray.Count -gt 0) { "$($actualArray.Count) items" } else { "Not set" } } } } else { # Simple Registry-Policy (DWORD/String) $passed = Test-RegistryValue -Path $regPath -Name $check.Name -ExpectedValue $check.Value if ($passed) { $results.Verified++ $antiAIPassed += [PSCustomObject]@{ Policy = $check.Desc Path = "$regPath\$($check.Name)" Expected = $check.Value Actual = $check.Value } } else { $results.Failed++ $actual = Get-ActualRegistryValue -Path $regPath -Name $check.Name $antiAIFailed += [PSCustomObject]@{ Policy = $check.Desc Path = "$regPath\$($check.Name)" Expected = $check.Value Actual = $actual } } } } # Update AntiAI-Total with actual check count (incl. MultiString individual checks) $results.AntiAIPolicies = $actualCheckCount # Add to AllSettings for HTML report $antiAIPassedCount = $actualCheckCount - $antiAIFailed.Count $results.AllSettings += [PSCustomObject]@{ Category = "AntiAI" Total = $actualCheckCount Passed = $antiAIPassedCount Failed = $antiAIFailed.Count PassedDetails = $antiAIPassed FailedDetails = $antiAIFailed } if ($antiAIFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "AntiAI" Count = $antiAIFailed.Count Details = $antiAIFailed } } Write-Host " AntiAI: $antiAIPassedCount/$actualCheckCount verified" -ForegroundColor $(if ($antiAIFailed.Count -eq 0) { "Green" } else { "Yellow" }) } catch { Write-Host " ERROR: $_" -ForegroundColor Red } # [ALWAYS] EdgeHardening Configuration (24 policies) $edgeStep = 8 Write-Host "[$edgeStep/$totalSteps] Verifying EdgeHardening Policies ($EXPECTED_EDGE_COUNT)..." -ForegroundColor Yellow try { $edgeFailed = @() $edgePassed = @() # Load Edge policies configuration $edgeConfigPath = Join-Path $rootPath "Modules\EdgeHardening\Config\EdgePolicies.json" if (Test-Path $edgeConfigPath) { $edgePolicies = Get-Content $edgeConfigPath -Raw | ConvertFrom-Json foreach ($policy in $edgePolicies) { # Clean key path (remove [ prefix if exists) $keyPath = $policy.KeyName -replace '^\[', '' $fullPath = "HKLM:\$keyPath" # Determine if this policy is optional $isOptional = $false # GPO deletion markers are optional (infrastructure, not real policies) if ($policy.ValueName -like "*delvals*") { $isOptional = $true } # ExtensionInstallBlocklist is optional (user may choose -AllowExtensions) if ($policy.ValueName -eq "1" -and $keyPath -like "*ExtensionInstallBlocklist*") { $isOptional = $true } $actualValue = $null if (Test-Path $fullPath) { $actualValue = (Get-ItemProperty -Path $fullPath -Name $policy.ValueName -ErrorAction SilentlyContinue).($policy.ValueName) } $expectedValue = $policy.Data $passed = $false # Check if value matches expected if ($null -ne $actualValue) { if ($policy.Type -eq "REG_SZ") { $passed = ($actualValue -eq $expectedValue) } else { $passed = ($actualValue -eq $expectedValue) } } elseif ($isOptional) { # Optional policy not set = SUCCESS (user choice) $passed = $true } if ($passed) { $results.Verified++ $edgePassed += [PSCustomObject]@{ Policy = $policy.ValueName Path = $keyPath Expected = $expectedValue Actual = if ($null -eq $actualValue) { "Not set (Optional)" } else { $actualValue } } } else { $results.Failed++ $edgeFailed += [PSCustomObject]@{ Policy = $policy.ValueName Path = $keyPath Expected = $expectedValue Actual = if ($null -eq $actualValue) { "Not set" } else { $actualValue } } } } # Add to AllSettings for HTML report $edgePassedCount = $results.EdgeHardeningPolicies - $edgeFailed.Count $results.AllSettings += [PSCustomObject]@{ Category = "EdgeHardening" Total = $results.EdgeHardeningPolicies Passed = $edgePassedCount Failed = $edgeFailed.Count PassedDetails = $edgePassed FailedDetails = $edgeFailed } if ($edgeFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "EdgeHardening" Count = $edgeFailed.Count Details = $edgeFailed } } Write-Host " EdgeHardening: $($results.EdgeHardeningPolicies - $edgeFailed.Count)/$($results.EdgeHardeningPolicies) verified" -ForegroundColor $(if ($edgeFailed.Count -eq 0) { "Green" } else { "Yellow" }) } else { Write-Host " EdgeHardening: Config not found - marking all $EXPECTED_EDGE_COUNT checks as FAILED" -ForegroundColor Yellow # CRITICAL: Must count all checks as Failed when config missing! $results.Failed += $EXPECTED_EDGE_COUNT $edgeFailed += [PSCustomObject]@{ Policy = "EdgeHardening (All $EXPECTED_EDGE_COUNT policies)" Expected = "EdgePolicies.json required" Actual = "Config file not found" } # Add to AllSettings for HTML report $results.AllSettings += [PSCustomObject]@{ Category = "EdgeHardening" Total = $results.EdgeHardeningPolicies Passed = 0 Failed = $EXPECTED_EDGE_COUNT FailedDetails = $edgeFailed } $results.FailedSettings += [PSCustomObject]@{ Category = "EdgeHardening" Count = $EXPECTED_EDGE_COUNT Details = $edgeFailed } } } catch { Write-Host " ERROR: $_" -ForegroundColor Red } # [ALWAYS] AdvancedSecurity Settings (policy-level checks) $advStep = $totalSteps Write-Host "[$advStep/$totalSteps] Verifying AdvancedSecurity Settings ($EXPECTED_ADVANCED_COUNT)..." -ForegroundColor Yellow try { $advFailed = @() $advPassed = @() # RDP Settings (3 checks) # NOTE: RDP CompleteDisable (fDenyTSConnections=1) is OPTIONAL - depends on user choice # SecurityLayer + UserAuthentication are ALWAYS applied (NLA enforcement) $rdpChecks = @( @{ Path = "HKLM:\System\CurrentControlSet\Control\Terminal Server"; Name = "fDenyTSConnections"; Expected = 1; Desc = "RDP Disabled"; Optional = $true } @{ Path = "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"; Name = "SecurityLayer"; Expected = 2; Desc = "RDP Security Layer (TLS)"; Optional = $false } @{ Path = "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"; Name = "UserAuthentication"; Expected = 1; Desc = "RDP NLA"; Optional = $false } ) # WDigest (1 check) - ALWAYS required $wdigestCheck = @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest"; Name = "UseLogonCredential"; Expected = 0; Desc = "WDigest Disabled"; Optional = $false } # Admin Shares (1 check) - OPTIONAL on domain-joined systems $adminShareCheck = @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters"; Name = "AutoShareWks"; Expected = 0; Desc = "Admin Shares Disabled"; Optional = $true } # Risky Services (3 checks) - UPnP services (SSDPSRV, upnphost) areOPTIONAL (user decides), lmhosts is REQUIRED $riskyServices = @( @{ Name = "SSDPSRV"; Desc = "SSDP Discovery Service"; Optional = $true } @{ Name = "upnphost"; Desc = "UPnP Device Host"; Optional = $true } @{ Name = "lmhosts"; Desc = "TCP/IP NetBIOS Helper"; Optional = $false } ) # TLS Settings (8 checks) - ALWAYS required (all profiles disable legacy TLS) # Check both Server AND Client to match what AdvancedSecurity applies # We validate both Enabled=0 and DisabledByDefault=1 per version/component $tlsChecks = @( # Enabled flags @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server"; Name = "Enabled"; Expected = 0; Desc = "TLS 1.0 Server Disabled"; Optional = $false } @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client"; Name = "Enabled"; Expected = 0; Desc = "TLS 1.0 Client Disabled"; Optional = $false } @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server"; Name = "Enabled"; Expected = 0; Desc = "TLS 1.1 Server Disabled"; Optional = $false } @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client"; Name = "Enabled"; Expected = 0; Desc = "TLS 1.1 Client Disabled"; Optional = $false } # DisabledByDefault flags @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server"; Name = "DisabledByDefault"; Expected = 1; Desc = "TLS 1.0 Server DisabledByDefault"; Optional = $false } @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client"; Name = "DisabledByDefault"; Expected = 1; Desc = "TLS 1.0 Client DisabledByDefault"; Optional = $false } @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server"; Name = "DisabledByDefault"; Expected = 1; Desc = "TLS 1.1 Server DisabledByDefault"; Optional = $false } @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client"; Name = "DisabledByDefault"; Expected = 1; Desc = "TLS 1.1 Client DisabledByDefault"; Optional = $false } ) # WPAD (3 HKLM checks) - ALWAYS required # Official MS key (DisableWpad) + legacy WpadOverride + browser-level AutoDetect # Reference: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/disable-http-proxy-auth-features # NOTE: HKCU AutoDetect is set per-user via HKU in Apply, verified separately below $wpadChecks = @( @{ Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp"; Name = "DisableWpad"; Expected = 1; Desc = "WPAD Disabled (Official MS Key)"; Optional = $false } @{ Path = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Wpad"; Name = "WpadOverride"; Expected = 1; Desc = "WPAD Disabled (WpadOverride)"; Optional = $false } @{ Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings"; Name = "AutoDetect"; Expected = 0; Desc = "WPAD AutoDetect (HKLM)"; Optional = $false } ) # SRP Root Policy (2 checks) - ALWAYS required for CVE-2025-9491 mitigation $srpRootChecks = @( @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers"; Name = "DefaultLevel"; Expected = 262144; Desc = "SRP DefaultLevel (Unrestricted)"; Optional = $false } @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers"; Name = "TransparentEnabled"; Expected = 1; Desc = "SRP TransparentEnabled"; Optional = $false } ) # Firewall Shields Up (1 check) - Maximum profile only, blocks ALL incoming on Public network # Optional = true because it's only applied for Maximum profile (user choice) $shieldsUpCheck = @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\PublicProfile" Name = "DoNotAllowExceptions" Expected = 1 Desc = "Firewall Shields Up (Maximum only)" Optional = $true } # Discovery Protocols (WS-Discovery + mDNS) - Maximum profile only # Optional = true because only applied for Maximum profile (user choice) # Check 1: mDNS disabled via registry $discoveryMdnsCheck = @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters" Name = "EnableMDNS" Expected = 0 Desc = "Discovery Protocols: mDNS Disabled (Maximum only)" Optional = $true } # Discovery Protocols: Firewall block rules (4 checks) - checked separately below # Also need to check services FDResPub and fdPHost are disabled # IPv6 Disable (mitm6 attack mitigation) - Maximum profile only # Optional = true because only applied for Maximum profile (user choice) $ipv6Check = @{ Path = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters" Name = "DisabledComponents" Expected = 255 # 0xFF = completely disabled Desc = "IPv6 Disabled (mitm6 mitigation, Maximum only)" Optional = $true } # PowerShell v2 (1 check) - Feature should be Disabled or Not Present try { $psv2Feature = Get-WindowsOptionalFeature -Online -FeatureName "MicrosoftWindowsPowerShellV2Root" -ErrorAction SilentlyContinue # If feature is not found ($null) or explicitly Disabled, it's secure if (-not $psv2Feature -or $psv2Feature.State -eq "Disabled") { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "PowerShell v2 Feature" Expected = "Disabled/Absent" Actual = if ($psv2Feature) { $psv2Feature.State } else { "Not Present" } } } else { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = "PowerShell v2 Feature"; Expected = "Disabled/Absent"; Actual = $psv2Feature.State } } } catch { # If check fails, assume success/absent to avoid false positives on Home edition $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "PowerShell v2 Feature" Expected = "Disabled/Absent" Actual = "Check passed (assumed absent)" } } # Windows Update (4 Checks) - ALWAYS required - matches AdvancedSecurity module Config/WindowsUpdate.json $wuChecks = @( @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; Name = "AllowOptionalContent"; Expected = 1; Desc = "WU: Get latest updates immediately (Policy)"; Optional = $false } @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; Name = "SetAllowOptionalContent"; Expected = 1; Desc = "WU: AllowOptionalContent Policy Flag"; Optional = $false } @{ Path = "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings"; Name = "AllowMUUpdateService"; Expected = 1; Desc = "WU: Microsoft Update (Office, drivers)"; Optional = $false } @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization"; Name = "DODownloadMode"; Expected = 0; Desc = "WU: P2P Delivery Optimization OFF"; Optional = $false } ) # Finger Protocol (1 check) - verify outbound firewall rule created by AdvancedSecurity try { $fingerRuleName = "NoID Privacy - Block Finger Protocol (Port 79)" $fingerRule = Get-NetFirewallRule -DisplayName $fingerRuleName -ErrorAction SilentlyContinue $fingerOk = $false $actualDesc = "Rule not found" if ($fingerRule) { # Basic rule properties: enabled, outbound, block action if ($fingerRule.Enabled -eq "True" -and $fingerRule.Direction -eq "Outbound" -and $fingerRule.Action -eq "Block") { $portFilter = $fingerRule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue if ($portFilter -and $portFilter.Protocol -eq "TCP" -and $portFilter.RemotePort -eq 79) { $fingerOk = $true } else { $actualDesc = if ($portFilter) { "Protocol=$($portFilter.Protocol), RemotePort=$($portFilter.RemotePort)" } else { "No port filter" } } } else { $actualDesc = "Enabled=$($fingerRule.Enabled), Direction=$($fingerRule.Direction), Action=$($fingerRule.Action)" } } if ($fingerOk) { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "Finger Protocol Firewall Rule" Expected = "Outbound TCP 79 blocked by NoID rule" Actual = "Rule present and configured correctly" } } else { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = "Finger Protocol Firewall Rule" Expected = "Outbound TCP 79 blocked by NoID rule" Actual = $actualDesc } } } catch { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = "Finger Protocol Firewall Rule" Expected = "Outbound TCP 79 blocked by NoID rule" Actual = "Verification failed: $($_.Exception.Message)" } } # Discovery Protocols Firewall Rules + Services (Maximum profile only, Optional) # 4 Firewall Rules: WSD UDP 3702, WSD TCP 5357, WSD TCP 5358, mDNS UDP 5353 # 2 Services: FDResPub, fdPHost should be Disabled try { $discoveryRuleNames = @( "NoID-Block-WSD-UDP-3702", "NoID-Block-WSD-TCP-5357", "NoID-Block-WSD-TCP-5358", "NoID-Block-mDNS-UDP-5353" ) $discoveryRulesFound = 0 foreach ($ruleName in $discoveryRuleNames) { $rule = Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue if ($rule -and $rule.Enabled -eq "True" -and $rule.Action -eq "Block") { $discoveryRulesFound++ } } # Check services $fdResPub = Get-Service -Name "FDResPub" -ErrorAction SilentlyContinue $fdPHost = Get-Service -Name "fdPHost" -ErrorAction SilentlyContinue $servicesDisabled = ( ($fdResPub -and $fdResPub.StartType -eq 'Disabled') -and ($fdPHost -and $fdPHost.StartType -eq 'Disabled') ) # This is Optional (Maximum profile only) - pass regardless of state $results.Verified++ if ($discoveryRulesFound -eq 4 -and $servicesDisabled) { $advPassed += [PSCustomObject]@{ Setting = "Discovery Protocols (WS-Discovery + mDNS, Maximum only)" Expected = "4 block rules + 2 services disabled" Actual = "All configured (Maximum profile applied)" } } else { $advPassed += [PSCustomObject]@{ Setting = "Discovery Protocols (WS-Discovery + mDNS, Maximum only)" Expected = "4 block rules + 2 services disabled" Actual = "Not configured (Optional - rules: $discoveryRulesFound/4, services: $servicesDisabled)" } } } catch { # Optional check - pass anyway $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "Discovery Protocols (WS-Discovery + mDNS, Maximum only)" Expected = "4 block rules + 2 services disabled" Actual = "Check skipped (Optional)" } } # Check all registry settings (respects Optional flag) # NOTE: SRP Pfadregeln werden separat unterhalb geprüft, da random GUID-Namen verwendet werden $allAdvChecks = $rdpChecks + $wdigestCheck + $adminShareCheck + $tlsChecks + $wuChecks + $wpadChecks + $srpRootChecks + $shieldsUpCheck + $discoveryMdnsCheck + $ipv6Check foreach ($check in $allAdvChecks) { $actualValue = $null if (Test-Path $check.Path) { $actualValue = (Get-ItemProperty -Path $check.Path -Name $check.Name -ErrorAction SilentlyContinue).($check.Name) } if ($null -ne $actualValue -and $actualValue -eq $check.Expected) { # Setting exists and matches expected value - SUCCESS $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = $actualValue } } elseif ($check.Optional -eq $true) { # Optional setting - SUCCESS regardless of value (user choice) # This includes: not set, set to expected, or set to opposite value $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = if ($null -eq $actualValue) { "Not set (Optional)" } else { "$actualValue (Optional)" } } } else { # Setting is required but missing or wrong $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = if ($null -eq $actualValue) { "Not set" } else { $actualValue } } } } # WPAD HKCU Check via HKU (1 check) - verify AutoDetect=0 for all user profiles # When running as admin, HKCU points to admin's profile, not the logged-in user # Solution: Check via HKU (HKEY_USERS) like we do in Apply try { if (-not (Test-Path "HKU:")) { New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS -ErrorAction SilentlyContinue | Out-Null } $hkuPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings" $userSIDs = Get-ChildItem -Path "HKU:\" -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match '^S-1-5-21-' -and $_.PSChildName -notmatch '_Classes$' } | Select-Object -ExpandProperty PSChildName $hkuCompliant = $true $hkuActualValue = "All users compliant" if ($userSIDs.Count -eq 0) { # No user profiles found - consider compliant (edge case) $hkuActualValue = "No user profiles (compliant)" } else { foreach ($sid in $userSIDs) { $userKeyPath = "HKU:\$sid\$hkuPath" if (Test-Path $userKeyPath) { $val = (Get-ItemProperty -Path $userKeyPath -Name "AutoDetect" -ErrorAction SilentlyContinue).AutoDetect # Check for non-zero value (1 = WPAD enabled = bad) # null/empty = not set = OK (HKLM keys handle system-wide WPAD disable) # 0 = explicitly disabled = OK if ($val -eq 1) { $hkuCompliant = $false $hkuActualValue = "SID $sid has AutoDetect=1 (WPAD enabled!)" break } # null, empty, or 0 are all acceptable } # Path doesn't exist = user never logged in or offline hive = OK } } if ($hkuCompliant) { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "WPAD AutoDetect (All Users via HKU)" Expected = 0 Actual = $hkuActualValue } } else { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = "WPAD AutoDetect (All Users via HKU)" Expected = 0 Actual = $hkuActualValue } } } catch { # If HKU check fails, count as passed to avoid false negatives $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "WPAD AutoDetect (All Users via HKU)" Expected = 0 Actual = "Check skipped: $($_.Exception.Message)" } } # Check risky services (3 - respects Optional flag) foreach ($svcDef in $riskyServices) { $service = Get-Service -Name $svcDef.Name -ErrorAction SilentlyContinue if ($service -and $service.StartType -eq "Disabled") { # Service is disabled as expected $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "Service: $($svcDef.Desc)" Expected = "Disabled" Actual = $service.StartType } } elseif ($svcDef.Optional -eq $true -and $service -and $service.StartType -ne "Disabled") { # Service is optional and NOT disabled - count as SUCCESS (user chose to keep it) $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "Service: $($svcDef.Desc)" Expected = "Disabled" Actual = "$($service.StartType) (Optional - user choice)" } } elseif (-not $service) { # Service doesn't exist - count as SUCCESS (not installed) $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "Service: $($svcDef.Desc)" Expected = "Disabled" Actual = "Not installed (Success)" } } else { # Service is required but not disabled $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = "Service: $($svcDef.Desc)" Expected = "Disabled" Actual = if ($service) { $service.StartType } else { "Not found" } } } } # SRP Rules (2 checks) - Custom verification logic # NOTE: Set-SRP Rules creates rules with random GUIDs, so we must search by ItemData (path pattern) $srpBasePath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers\0\Paths" $expectedSRPPaths = @( "%LOCALAPPDATA%\Temp\*.lnk" "%USERPROFILE%\Downloads\*.lnk" ) $foundSRPPaths = @() if (Test-Path $srpBasePath) { $srpRules = Get-ChildItem -Path $srpBasePath -ErrorAction SilentlyContinue foreach ($rule in $srpRules) { $itemData = (Get-ItemProperty -Path $rule.PSPath -Name "ItemData" -ErrorAction SilentlyContinue).ItemData if ($itemData) { $foundSRPPaths += $itemData } } } # Check if both expected SRP paths exist foreach ($expectedPath in $expectedSRPPaths) { # SRP ItemData is stored as REG_EXPAND_SZ and Get-ItemProperty returns # the expanded path (e.g., C:\Users\User\AppData\Local\Temp\*.lnk). # To be robust, treat the rule as present if any ItemData equals either # the literal expected string with environment variables, OR the # expanded variant. $expandedExpectedPath = [Environment]::ExpandEnvironmentVariables($expectedPath) $srpMatch = $false foreach ($actualPath in $foundSRPPaths) { if ($null -eq $actualPath) { continue } if ($actualPath -eq $expectedPath -or $actualPath -eq $expandedExpectedPath) { $srpMatch = $true break } } if ($srpMatch) { $results.Verified++ $srpDesc = if ($expectedPath -like '*Temp*') { "SRP: Block LNK from TEMP" } else { "SRP: Block LNK from Downloads" } $advPassed += [PSCustomObject]@{ Setting = $srpDesc Expected = $expectedPath Actual = "Rule present" } } else { $results.Failed++ $srpDesc = if ($expectedPath -like '*Temp*') { "SRP: Block LNK from TEMP" } else { "SRP: Block LNK from Downloads" } $advFailed += [PSCustomObject]@{ Setting = $srpDesc Expected = $expectedPath Actual = "Not set" } } } # Risky Ports checks owned by AdvancedSecurity - individual firewall rule verification # Baseline-owned registry policies (EnableMulticast, NodeType, SMB1, AllowInsecureGuestAuth) # are verified in the SecurityBaseline/Registry section and are intentionally # NOT duplicated here to keep module ownership clean. # 1. Check NetBIOS disabled on all network adapters (aggregated policy check) try { $adapters = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "IPEnabled = TRUE" -ErrorAction SilentlyContinue if ($null -eq $adapters) { $adapters = @() } elseif ($adapters -isnot [array]) { $adapters = @($adapters) } $totalAdapters = $adapters.Count $disabledCount = 0 $nonCompliant = @() foreach ($adapter in $adapters) { $adapterName = if ($adapter.Description.Length -gt 40) { $adapter.Description.Substring(0, 37) + "..." } else { $adapter.Description } if ($adapter.TcpipNetbiosOptions -eq 2) { $disabledCount++ } else { $nonCompliant += "$adapterName (Option=$($adapter.TcpipNetbiosOptions))" } } $settingName = "NetBIOS Adapters (Aggregated)" if ($totalAdapters -eq 0) { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = $settingName Expected = "All adapters Disabled (2)" Actual = "No IPEnabled adapters found" } } elseif ($disabledCount -eq $totalAdapters) { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $settingName Expected = "All adapters Disabled (2)" Actual = "$disabledCount/$totalAdapters disabled" } } else { $results.Failed++ $actualDesc = "$disabledCount/$totalAdapters disabled" if ($nonCompliant.Count -gt 0) { $actualDesc += " | Non-compliant: " + ($nonCompliant -join '; ') } $advFailed += [PSCustomObject]@{ Setting = $settingName Expected = "All adapters Disabled (2)" Actual = $actualDesc } } } catch { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = "NetBIOS Adapters (Aggregated)" Expected = "All adapters Disabled (2)" Actual = "Check failed: $($_.Exception.Message)" } } # 2. Check NoID Privacy Firewall Rules (SSDP block, Admin Shares block) $firewallRulesToCheck = @( @{ Name = "NoID Privacy - Block SSDP (UDP 1900)"; Desc = "FW: Block SSDP (UDP 1900)" } @{ Name = "Block Admin Shares - NoID Privacy"; Desc = "FW: Block Admin Shares (TCP 445)" } ) foreach ($fwRule in $firewallRulesToCheck) { try { $rule = Get-NetFirewallRule -DisplayName $fwRule.Name -ErrorAction SilentlyContinue if ($rule -and $rule.Enabled -eq "True" -and $rule.Action -eq "Block") { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $fwRule.Desc Expected = "Rule enabled and blocking" Actual = "Active" } } elseif ($rule) { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = $fwRule.Desc Expected = "Rule enabled and blocking" Actual = "Enabled=$($rule.Enabled), Action=$($rule.Action)" } } else { # Rule not found - may be optional depending on profile $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $fwRule.Desc Expected = "Rule present or not required" Actual = "Not configured (Profile-dependent)" } } } catch { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $fwRule.Desc Expected = "Rule check" Actual = "Check skipped (optional)" } } } # 3. Check standard Windows Firewall rules for risky ports (LLMNR, NetBIOS, UPnP) $riskyPortRules = @( @{ Pattern = "*LLMNR*"; Port = "5355"; Desc = "FW: LLMNR (UDP 5355)" } @{ Pattern = "*NetBIOS*"; Port = "137-139"; Desc = "FW: NetBIOS (137-139)" } @{ Pattern = "*UPnP*"; Port = "1900,2869"; Desc = "FW: UPnP/SSDP Ports" } ) foreach ($portRule in $riskyPortRules) { # These are informational - Windows has built-in rules that may or may not be disabled $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $portRule.Desc Expected = "Blocked by policy/adapter settings" Actual = "Controlled via NetBIOS/Registry policies" } } # 4. Wireless Display (Miracast) Security - ALWAYS required (2 base checks) # Default hardening: AllowProjectionToPC=0, RequirePinForPairing=2 # Optional full disable: AllowProjectionFromPC=0, AllowMdnsAdvertisement=0, AllowMdnsDiscovery=0 $wirelessDisplayPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Connect" # Base checks (always required) $wirelessBaseChecks = @( @{ Name = "AllowProjectionToPC"; Expected = 0; Desc = "Wireless Display: Block receiving projections" } @{ Name = "RequirePinForPairing"; Expected = 2; Desc = "Wireless Display: Always require PIN" } ) foreach ($check in $wirelessBaseChecks) { try { if (Test-Path $wirelessDisplayPath) { $value = Get-ItemProperty -Path $wirelessDisplayPath -Name $check.Name -ErrorAction SilentlyContinue if ($null -ne $value -and $value.$($check.Name) -eq $check.Expected) { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = $value.$($check.Name) } } elseif ($null -ne $value) { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = $value.$($check.Name) } } else { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = "Not configured" } } } else { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = "Registry key not found" } } } catch { $results.Failed++ $advFailed += [PSCustomObject]@{ Setting = $check.Desc Expected = $check.Expected Actual = "Error: $($_.Exception.Message)" } } } # Optional full disable checks (pass if configured OR not configured - user choice) $wirelessOptionalChecks = @( @{ Name = "AllowProjectionFromPC"; Expected = 0; Desc = "Wireless Display: Block sending projections (Optional)" } @{ Name = "AllowMdnsAdvertisement"; Expected = 0; Desc = "Wireless Display: Block mDNS advertisement (Optional)" } @{ Name = "AllowMdnsDiscovery"; Expected = 0; Desc = "Wireless Display: Block mDNS discovery (Optional)" } ) foreach ($check in $wirelessOptionalChecks) { try { if (Test-Path $wirelessDisplayPath) { $value = Get-ItemProperty -Path $wirelessDisplayPath -Name $check.Name -ErrorAction SilentlyContinue if ($null -ne $value -and $value.$($check.Name) -eq $check.Expected) { # Fully disabled - pass $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $check.Desc Expected = "$($check.Expected) or not configured" Actual = "$($value.$($check.Name)) (Fully disabled)" } } else { # Not configured or different value - still pass (user chose hardened-only) $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $check.Desc Expected = "$($check.Expected) or not configured" Actual = "$(if ($null -ne $value) { $value.$($check.Name) } else { 'Not configured' }) (User choice: hardened-only)" } } } else { # Key doesn't exist - pass (base hardening may not have been run yet) $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $check.Desc Expected = "$($check.Expected) or not configured" Actual = "Not configured (User choice)" } } } catch { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $check.Desc Expected = "$($check.Expected) or not configured" Actual = "Check skipped (optional)" } } } # Optional Miracast firewall rules (pass if present OR not present - user choice) $miracastFirewallRules = @( @{ Name = "NoID Privacy - Block Miracast TCP 7236"; Desc = "FW: Block Miracast TCP 7236 (Optional)" } @{ Name = "NoID Privacy - Block Miracast TCP 7250"; Desc = "FW: Block Miracast TCP 7250 (Optional)" } @{ Name = "NoID Privacy - Block Miracast UDP 7236"; Desc = "FW: Block Miracast UDP 7236 (Optional)" } @{ Name = "NoID Privacy - Block Miracast UDP 7250"; Desc = "FW: Block Miracast UDP 7250 (Optional)" } ) foreach ($fwRule in $miracastFirewallRules) { try { $rule = Get-NetFirewallRule -DisplayName $fwRule.Name -ErrorAction SilentlyContinue if ($rule -and $rule.Enabled -eq "True" -and $rule.Action -eq "Block") { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $fwRule.Desc Expected = "Blocking or not configured" Actual = "Active (Fully disabled mode)" } } else { # Not configured - pass (user chose hardened-only) $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $fwRule.Desc Expected = "Blocking or not configured" Actual = "Not configured (User choice: hardened-only)" } } } catch { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = $fwRule.Desc Expected = "Blocking or not configured" Actual = "Check skipped (optional)" } } } # WiFi Direct Service check (CRITICAL for complete Miracast block - optional based on user choice) try { $wfdService = Get-Service -Name "WFDSConMgrSvc" -ErrorAction SilentlyContinue if ($wfdService -and $wfdService.StartType -eq 'Disabled') { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "WiFi Direct Service (WFDSConMgrSvc)" Expected = "Disabled or running (user choice)" Actual = "Disabled (Fully disabled mode)" } } else { # Service running or not disabled - pass (user chose hardened-only) $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "WiFi Direct Service (WFDSConMgrSvc)" Expected = "Disabled or running (user choice)" Actual = "Running (User choice: hardened-only)" } } } catch { $results.Verified++ $advPassed += [PSCustomObject]@{ Setting = "WiFi Direct Service (WFDSConMgrSvc)" Expected = "Disabled or running (user choice)" Actual = "Check skipped (optional)" } } # Add to AllSettings for HTML report # Use actual count of checks (policy-level, now deterministic) $advTotalChecks = $advPassed.Count + $advFailed.Count $results.AdvancedSecuritySettings = $advTotalChecks $results.AllSettings += [PSCustomObject]@{ Category = "AdvancedSecurity" Total = $advTotalChecks Passed = $advPassed.Count Failed = $advFailed.Count PassedDetails = $advPassed FailedDetails = $advFailed } if ($advFailed.Count -gt 0) { $results.FailedSettings += [PSCustomObject]@{ Category = "AdvancedSecurity" Count = $advFailed.Count Details = $advFailed } } Write-Host " AdvancedSecurity: $($advPassed.Count)/$advTotalChecks verified" -ForegroundColor $(if ($advFailed.Count -eq 0) { "Green" } else { "Yellow" }) } catch { Write-Host " ERROR: $_" -ForegroundColor Red } # Reconcile global Verified count with total/failed settings to avoid # drift from per-category counters in case some success paths didn't # manually increment $results.Verified. $results.TotalSettings = ($results.AllSettings | Measure-Object -Property Total -Sum).Sum $results.Verified = ($results.AllSettings | Measure-Object -Property Passed -Sum).Sum $results.Duration = (Get-Date) - $startTime Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host " Verification Complete" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" Write-Host "Checked Modules (ALL 9 ALWAYS):" -ForegroundColor Cyan Write-Host " - SecurityBaseline: [CHECKED] (335+67+23)" -ForegroundColor Green Write-Host " - ASR: [CHECKED] (19)" -ForegroundColor Green Write-Host " - DNS: [CHECKED] (5)" -ForegroundColor Green Write-Host " - Privacy: [CHECKED] ($($results.PrivacyChecks))" -ForegroundColor Green Write-Host " - AntiAI: [CHECKED] ($($results.AntiAIPolicies))" -ForegroundColor Green Write-Host " - EdgeHardening: [CHECKED] ($($results.EdgeHardeningPolicies))" -ForegroundColor Green Write-Host " - AdvancedSecurity: [CHECKED] ($($results.AdvancedSecuritySettings))" -ForegroundColor Green Write-Host "" Write-Host "Total Settings: $($results.TotalSettings)" -ForegroundColor White Write-Host "Verified: $($results.Verified)" -ForegroundColor Green Write-Host "Failed: $($results.Failed)" -ForegroundColor $(if ($results.Failed -eq 0) { "Green" } else { "Red" }) Write-Host "Success Rate: $([math]::Round(($results.Verified / $results.TotalSettings) * 100, 2))%" -ForegroundColor $(if ($results.Failed -eq 0) { "Green" } else { "Yellow" }) Write-Host "Duration: $($results.Duration.TotalSeconds) seconds" -ForegroundColor White Write-Host "" if ($results.Failed -gt 0) { Write-Host "Failed Settings by Category:" -ForegroundColor Yellow Write-Host "" foreach ($category in $results.FailedSettings) { Write-Host " $($category.Category): $($category.Count) failed" -ForegroundColor Red # Always show first 5 details foreach ($detail in ($category.Details | Select-Object -First 5)) { # Format based on category if ($category.Category -eq "Registry") { Write-Host " - $($detail.Path)\$($detail.Name) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "SecurityTemplate") { Write-Host " - [$($detail.Section)] $($detail.Setting) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "AuditPolicies") { Write-Host " - $($detail.Policy) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "ASR") { Write-Host " - $($detail.Rule) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "DNS") { Write-Host " - $($detail.Check) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "Privacy") { Write-Host " - $($detail.Setting) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "AntiAI") { Write-Host " - $($detail.Policy) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "EdgeHardening") { Write-Host " - $($detail.Policy) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } elseif ($category.Category -eq "AdvancedSecurity") { Write-Host " - $($detail.Setting) | Expected: $($detail.Expected) | Actual: $($detail.Actual)" -ForegroundColor Gray } } # Always show "... and X more" if there are more than 5 items if ($category.Count -gt 5) { Write-Host " ... and $($category.Count - 5) more" -ForegroundColor Gray } } Write-Host "" } if ($ExportPath) { $results | ConvertTo-Json -Depth 10 | Out-File $ExportPath Write-Host "Results exported to: $ExportPath" -ForegroundColor Cyan } # ======================================== # GENERATE HTML COMPLIANCE REPORT # ======================================== Write-Host "" Write-Host "Generating HTML Compliance Report..." -ForegroundColor Cyan try { # Determine project root (one level up from Tools folder) $projectRoot = Split-Path $PSScriptRoot -Parent $reportsFolder = Join-Path $projectRoot "Reports" # Create Reports folder if it doesn't exist if (-not (Test-Path $reportsFolder)) { New-Item -ItemType Directory -Path $reportsFolder -Force | Out-Null } # Generate timestamped filename $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" $htmlFile = Join-Path $reportsFolder "Complete-Hardening_$timestamp.html" # Generate HTML report (inline function for portability) . { param($Results, $OutputFile) # Calculate stats (use correct property names!) $totalSettings = $Results.TotalSettings $passedCount = $Results.Verified $failedCount = $Results.Failed # Safe division with null check if ($totalSettings -gt 0) { $compliancePercent = [math]::Round(($passedCount / $totalSettings) * 100, 1) } else { $compliancePercent = 0 } # Get system info $computerName = $env:COMPUTERNAME $osInfo = Get-CimInstance Win32_OperatingSystem $osVersion = "$($osInfo.Caption) (Build $($osInfo.BuildNumber))" $reportTimestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" # Build HTML $html = @"
Complete Hardening Compliance Report
All $totalSettings Settings Verified| Setting | Path/Policy | Expected | Actual | Status |
|---|---|---|---|---|
| $settingName | $pathInfo | $expected | $actual | $statusBadge |
| $settingName | $pathInfo | $expected | $actual | $statusBadge |
| No settings configured for this module | ||||