2025-12-08 10:32:49 +01:00
<#
. 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 .
2025-12-09 10:48:12 +01:00
Total : 633 settings ( Paranoid mode )
2025-12-08 10:32:49 +01:00
SecurityBaseline : 425 ( 335 Registry + 67 SecTemplate + 23 Audit )
ASR : 19
DNS : 5
Privacy : 81 ( 57 registry Paranoid + 24 bloatware )
2025-12-09 07:03:39 +01:00
AntiAI : 32 compliance checks ( 15 features )
2025-12-08 10:32:49 +01:00
EdgeHardening : 24 ( 22 - 23 applied depending on extensions )
AdvancedSecurity : 50 ( 15 features incl . Discovery Protocols + IPv6 )
. NOTES
Author : NexusOne23
2026-01-07 18:46:14 +01:00
Version : 2.2 . 3
2025-12-08 10:32:49 +01:00
#>
# 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
2025-12-09 10:48:12 +01:00
$EXPECTED_PRIVACY_COUNT = 78 # 54 registry from Privacy-MSRecommended.json + 24 bloatware apps
2025-12-09 07:03:39 +01:00
$EXPECTED_ANTIAI_COUNT = 32 # 32 AntiAI registry policy checks (15 features)
2025-12-08 10:32:49 +01:00
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' ,
2026-01-07 18:46:14 +01:00
'Summary' , 'AutomaticallyBlockedByMasterSwitch' , 'ModuleName' , 'Version' ,
'TotalFeatures' , 'TotalPolicies' , 'URIHandlers' , 'Note' , 'FilePath' ,
'HostsEntries' , 'CloudBased' , 'RequiresReboot' ,
'RequiresADMX' , 'Impact' , 'Name' ) ) {
2025-12-08 10:32:49 +01:00
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
2026-01-07 18:46:14 +01:00
# Load expected ASR rules - JSON is array directly
$asrRules = Get-Content ( Join-Path $asrConfigPath " ASR-Rules.json " ) -Raw | ConvertFrom-Json
2025-12-08 10:32:49 +01:00
2026-01-07 18:46:14 +01:00
$asrFailed = @ ( )
$asrPassed = @ ( )
2025-12-08 10:32:49 +01:00
2026-01-07 18:46:14 +01:00
# 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 "
}
2025-12-08 10:32:49 +01:00
}
}
2026-01-07 18:46:14 +01:00
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
}
2025-12-08 10:32:49 +01:00
}
2026-01-07 18:46:14 +01:00
if ( $index -ge 0 ) {
$actualAction = $currentASRActions [ $index ]
$expectedAction = $rule . Action
2025-12-08 10:32:49 +01:00
2026-01-07 18:46:14 +01:00
# 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
2025-12-08 10:32:49 +01:00
2026-01-07 18:46:14 +01:00
# For flexible rules: Pass if Block OR Audit
# For other rules: Pass only if exact match
$rulePassed = if ( $isFlexibleRule ) { $isActiveMode } else { $actualAction -eq $expectedAction }
2025-12-08 10:32:49 +01:00
2026-01-07 18:46:14 +01:00
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
}
2025-12-08 10:32:49 +01:00
}
}
else {
$results . Failed + +
2026-01-07 18:46:14 +01:00
$expectedActionText = if ( $rule . Action -eq 1 ) { " Block " } elseif ( $rule . Action -eq 2 ) { " Audit " } else { " Disabled " }
2025-12-08 10:32:49 +01:00
$asrFailed + = [ PSCustomObject ] @ {
Rule = $rule . Name
GUID = $rule . GUID
Expected = $expectedActionText
2026-01-07 18:46:14 +01:00
Actual = " Not configured "
2025-12-08 10:32:49 +01:00
}
}
}
}
2026-01-07 18:46:14 +01:00
# 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
}
2025-12-08 10:32:49 +01:00
2026-01-07 18:46:14 +01:00
if ( $asrFailed . Count -gt 0 ) {
$results . FailedSettings + = [ PSCustomObject ] @ {
Category = " ASR "
Count = $asrFailed . Count
Details = $asrFailed
}
2025-12-08 10:32:49 +01:00
}
2026-01-07 18:46:14 +01:00
Write-Host " ASR Rules: $( $results . ASRRules - $asrFailed . Count ) / $( $results . ASRRules ) verified " -ForegroundColor $ ( if ( $asrFailed . Count -eq 0 ) { " Green " } else { " Yellow " } )
2025-12-08 10:32:49 +01:00
} # 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
2026-01-07 18:46:14 +01:00
( $entry . ServerAddresses -contains '127.0.0.1' ) -or
( $entry . AddressOrigin -eq 'DHCP' )
2025-12-08 10:32:49 +01:00
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
2026-01-07 18:46:14 +01:00
@ { 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 }
2025-12-08 10:32:49 +01:00
# 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 = @ (
2026-01-07 18:46:14 +01:00
@ { 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 }
2025-12-08 10:32:49 +01:00
)
# SRP Root Policy (2 checks) - ALWAYS required for CVE-2025-9491 mitigation
$srpRootChecks = @ (
2026-01-07 18:46:14 +01:00
@ { 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 }
2025-12-08 10:32:49 +01:00
)
# 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 = @ {
2026-01-07 18:46:14 +01:00
Path = " HKLM:\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\PublicProfile "
Name = " DoNotAllowExceptions "
2025-12-08 10:32:49 +01:00
Expected = 1
2026-01-07 18:46:14 +01:00
Desc = " Firewall Shields Up (Maximum only) "
2025-12-08 10:32:49 +01:00
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 = @ {
2026-01-07 18:46:14 +01:00
Path = " HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters "
Name = " EnableMDNS "
2025-12-08 10:32:49 +01:00
Expected = 0
2026-01-07 18:46:14 +01:00
Desc = " Discovery Protocols: mDNS Disabled (Maximum only) "
2025-12-08 10:32:49 +01:00
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 = @ {
2026-01-07 18:46:14 +01:00
Path = " HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters "
Name = " DisabledComponents "
2025-12-08 10:32:49 +01:00
Expected = 255 # 0xFF = completely disabled
2026-01-07 18:46:14 +01:00
Desc = " IPv6 Disabled (mitm6 mitigation, Maximum only) "
2025-12-08 10:32:49 +01:00
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 = @ (
2026-01-07 18:46:14 +01:00
@ { 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 }
2025-12-08 10:32:49 +01:00
)
# 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 ) "
2026-01-07 18:46:14 +01:00
}
else {
2025-12-08 10:32:49 +01:00
" 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 |
2026-01-07 18:46:14 +01:00
Where-Object { $_ . PSChildName -match '^S-1-5-21-' -and $_ . PSChildName -notmatch '_Classes$' } |
Select-Object -ExpandProperty PSChildName
2025-12-08 10:32:49 +01:00
$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
2026-01-07 18:46:14 +01:00
$nonCompliant = @ ( )
2025-12-08 10:32:49 +01:00
foreach ( $adapter in $adapters ) {
2026-01-07 18:46:14 +01:00
$adapterName = if ( $adapter . Description . Length -gt 40 ) { $adapter . Description . Substring ( 0 , 37 ) + " ... " } else { $adapter . Description }
2025-12-08 10:32:49 +01:00
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
2026-01-07 18:46:14 +01:00
$results . Verified = ( $results . AllSettings | Measure-Object -Property Passed -Sum ) . Sum
2025-12-08 10:32:49 +01:00
$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 = @"
< ! DOCTYPE html >
< html lang = " en " >
< head >
< meta charset = " UTF-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1.0 " >
< title > NoID Privacy - Complete Hardening Compliance Report < / title >
< style >
: root {
- -color -primary: #2563eb;
- -color -success: #10b981;
- -color -danger: #ef4444;
- -color -warning: #f59e0b;
- -color -bg -dark: #0f172a;
- -color -bg -light: #f8fafc;
- -color -text: #1e293b;
- -color -border: #e2e8f0;
}
* {
margin : 0 ;
padding : 0 ;
box-sizing : border-box ;
}
body {
font-family : 'Segoe UI' , system-ui , -apple -system , sans-serif ;
background : linear-gradient ( 135deg , #667eea 0%, #764ba2 100%);
padding : 2rem ;
min-height : 100vh ;
}
. container {
max-width : 1400px ;
margin : 0 auto ;
background : white ;
border-radius : 20px ;
box-shadow : 0 25px 50px - 12px rgba ( 0 , 0 , 0 , 0.25 ) ;
overflow : hidden ;
}
. header {
background : linear-gradient ( 135deg , var ( - -color -bg -dark ) 0 % , #1e3a8a 100%);
color : white ;
padding : 3rem ;
text-align : center ;
position : relative ;
overflow : hidden ;
}
. header :: before {
content : '' ;
position : absolute ;
top : -50 % ;
right : -50 % ;
width : 200 % ;
height : 200 % ;
background : radial-gradient ( circle , rgba ( 255 , 255 , 255 , 0.1 ) 0 % , transparent 70 % ) ;
animation : pulse 15s ease-in -out infinite ;
}
@keyframes pulse {
0 % , 100 % { transform : scale ( 1 ) ; }
50 % { transform : scale ( 1.1 ) ; }
}
. header h1 {
font-size : 2 . 5rem ;
font-weight : 700 ;
margin-bottom : 0 . 5rem ;
position : relative ;
z-index : 1 ;
}
. header . subtitle {
font-size : 1 . 1rem ;
opacity : 0.9 ;
position : relative ;
z-index : 1 ;
}
. header . badge {
display : inline-block ;
margin-top : 1rem ;
padding : 0 . 5rem 1 . 5rem ;
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
border-radius : 50px ;
font-size : 0 . 9rem ;
font-weight : 600 ;
backdrop-filter : blur ( 10px ) ;
}
. meta-info {
background : var ( - -color -bg -light ) ;
padding : 2rem ;
border-bottom : 3px solid var ( - -color -border ) ;
display : grid ;
grid-template -columns: repeat ( auto-fit , minmax ( 250px , 1fr ) ) ;
gap : 1 . 5rem ;
}
. meta-item {
display : flex ;
flex-direction : column ;
}
. meta-label {
font-size : 0 . 75rem ;
text-transform : uppercase ;
letter-spacing : 0 . 5px ;
color : #64748b;
margin-bottom : 0 . 25rem ;
}
. meta-value {
font-size : 1 . 1rem ;
font-weight : 600 ;
color : var ( - -color -text ) ;
}
. dashboard {
padding : 2rem ;
background : white ;
}
. stats-grid {
display : grid ;
grid-template -columns: repeat ( auto-fit , minmax ( 200px , 1fr ) ) ;
gap : 1 . 5rem ;
margin-bottom : 2rem ;
}
. stat-card {
background : linear-gradient ( 135deg , #f8fafc 0%, #e2e8f0 100%);
padding : 1 . 5rem ;
border-radius : 12px ;
border-left : 4px solid var ( - -color -primary ) ;
transition : transform 0 . 2s , box-shadow 0 . 2s ;
}
. stat-card : hover {
transform : translateY ( - 2px ) ;
box-shadow : 0 4px 12px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. stat-card . success { border-left -color: var ( - -color -success ) ; }
. stat-card . danger { border-left -color: var ( - -color -danger ) ; }
. stat-value {
font-size : 2 . 5rem ;
font-weight : 700 ;
line-height : 1 ;
margin-bottom : 0 . 5rem ;
}
. stat-value . success { color : var ( - -color -success ) ; }
. stat-value . danger { color : var ( - -color -danger ) ; }
. stat-label {
font-size : 0 . 875rem ;
color : #64748b;
font-weight : 500 ;
}
. progress-section {
margin : 2rem 0 ;
}
. progress-bar -container {
background : #e2e8f0;
height : 50px ;
border-radius : 25px ;
overflow : hidden ;
position : relative ;
box-shadow : inset 0 2px 4px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. progress-bar -fill {
height : 100 % ;
background : linear-gradient ( 90deg , var ( - -color -success ) 0 % , #34d399 100%);
display : flex ;
align-items : center ;
justify-content : center ;
color : white ;
font-weight : 700 ;
font-size : 1 . 1rem ;
transition : width 2s ease ;
position : relative ;
overflow : hidden ;
}
. progress-bar -fill:: after {
content : '' ;
position : absolute ;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
background : linear-gradient ( 90deg ,
transparent 0 % ,
rgba ( 255 , 255 , 255 , 0.3 ) 50 % ,
transparent 100 % ) ;
animation : shimmer 2s infinite ;
}
@keyframes shimmer {
0 % { transform : translateX ( -100 % ) ; }
100 % { transform : translateX ( 100 % ) ; }
}
. controls {
display : flex ;
gap : 1rem ;
margin-bottom : 2rem ;
flex-wrap : wrap ;
}
. search-box {
flex : 1 ;
min-width : 300px ;
padding : 0 . 75rem 1rem ;
border : 2px solid var ( - -color -border ) ;
border-radius : 8px ;
font-size : 1rem ;
transition : border-color 0 . 2s ;
}
. search-box : focus {
outline : none ;
border-color : var ( - -color -primary ) ;
}
. filter-buttons {
display : flex ;
gap : 0 . 5rem ;
}
. btn {
padding : 0 . 75rem 1 . 5rem ;
border : none ;
border-radius : 8px ;
font-weight : 600 ;
cursor : pointer ;
transition : all 0 . 2s ;
font-size : 0 . 875rem ;
}
. btn-primary {
background : var ( - -color -primary ) ;
color : white ;
}
. btn-success {
background : var ( - -color -success ) ;
color : white ;
}
. btn-danger {
background : var ( - -color -danger ) ;
color : white ;
}
. btn : hover {
transform : translateY ( - 1px ) ;
box-shadow : 0 4px 8px rgba ( 0 , 0 , 0 , 0.2 ) ;
}
. btn . active {
box-shadow : inset 0 2px 4px rgba ( 0 , 0 , 0 , 0.2 ) ;
}
. module-section {
margin : 2rem 0 ;
border : 2px solid var ( - -color -border ) ;
border-radius : 12px ;
overflow : hidden ;
background : white ;
}
. module-header {
background : linear-gradient ( 135deg , var ( - -color -bg -dark ) 0 % , #334155 100%);
color : white ;
padding : 1 . 5rem ;
cursor : pointer ;
display : flex ;
justify-content : space-between ;
align-items : center ;
transition : background 0 . 2s ;
}
. module-header : hover {
background : linear-gradient ( 135deg , #1e3a8a 0%, #1e40af 100%);
}
. module-title {
font-size : 1 . 25rem ;
font-weight : 700 ;
display : flex ;
align-items : center ;
gap : 1rem ;
}
. module-stats {
display : flex ;
gap : 1 . 5rem ;
font-size : 0 . 875rem ;
}
. module-stat {
display : flex ;
align-items : center ;
gap : 0 . 5rem ;
}
. expand-icon {
transition : transform 0 . 3s ;
}
. module-section . collapsed . expand-icon {
transform : rotate ( - 90deg ) ;
}
. module-content {
2025-12-22 22:38:14 +01:00
max-height : none ;
overflow : visible ;
2025-12-08 10:32:49 +01:00
transition : max-height 0 . 3s ease ;
}
. module-section . collapsed . module-content {
max-height : 0 ;
}
. settings-table {
width : 100 % ;
border-collapse : collapse ;
table-layout : fixed ;
}
. settings-table thead {
background : var ( - -color -bg -light ) ;
position : sticky ;
top : 0 ;
z-index : 10 ;
}
. settings-table th {
padding : 1rem ;
text-align : left ;
font-weight : 600 ;
color : var ( - -color -text ) ;
border-bottom : 2px solid var ( - -color -border ) ;
font-size : 0 . 875rem ;
text-transform : uppercase ;
letter-spacing : 0 . 5px ;
}
. settings-table th : nth-child ( 1 ) { width : 25 % ; }
. settings-table th : nth-child ( 2 ) { width : 30 % ; }
. settings-table th : nth-child ( 3 ) { width : 15 % ; }
. settings-table th : nth-child ( 4 ) { width : 15 % ; }
. settings-table th : nth-child ( 5 ) { width : 15 % ; }
. settings-table td {
padding : 1rem ;
border-bottom : 1px solid var ( - -color -border ) ;
word-wrap : break-word ;
word-break : break-word ;
overflow-wrap : break-word ;
}
. settings-table td : nth-child ( 2 ) ,
. settings-table td : nth-child ( 3 ) ,
. settings-table td : nth-child ( 4 ) {
font-size : 0 . 8rem ;
}
. settings-table tbody tr {
transition : background 0 . 2s ;
}
. settings-table tbody tr : hover {
background : #f1f5f9;
}
. settings-table tbody tr . passed {
background : rgba ( 16 , 185 , 129 , 0.05 ) ;
}
. settings-table tbody tr . failed {
background : rgba ( 239 , 68 , 68 , 0.05 ) ;
}
. status-badge {
display : inline-flex ;
align-items : center ;
gap : 0 . 5rem ;
padding : 0 . 375rem 0 . 75rem ;
border-radius : 6px ;
font-size : 0 . 75rem ;
font-weight : 600 ;
text-transform : uppercase ;
letter-spacing : 0 . 5px ;
}
. status-badge . passed {
background : #d1fae5;
color : #065f46;
}
. status-badge . failed {
background : #fee2e2;
color : #991b1b;
}
. status-icon {
font-size : 1rem ;
}
. value-cell {
font-family : 'Consolas' , 'Monaco' , monospace ;
font-size : 0 . 875rem ;
}
. value-match {
color : var ( - -color -success ) ;
font-weight : 600 ;
}
. value-mismatch {
color : var ( - -color -danger ) ;
font-weight : 600 ;
}
. export-section {
padding : 2rem ;
background : var ( - -color -bg -light ) ;
border-top : 2px solid var ( - -color -border ) ;
display : flex ;
gap : 1rem ;
justify-content : center ;
flex-wrap : wrap ;
}
. footer {
background : var ( - -color -bg -dark ) ;
color : rgba ( 255 , 255 , 255 , 0.7 ) ;
padding : 2rem ;
text-align : center ;
}
. footer a {
color : var ( - -color -primary ) ;
text-decoration : none ;
}
@page {
size : landscape ;
margin : 1cm ;
}
@media print {
body {
background : white ;
padding : 0 ;
}
. container {
box-shadow : none ;
border-radius : 0 ;
}
. controls , . export-section {
display : none ;
}
2025-12-22 22:38:14 +01:00
/ * Balanced header for print * /
. header {
padding : 1 . 5rem 2rem ;
page-break -inside: avoid ;
}
. header h1 {
font-size : 1 . 8rem ;
margin-bottom : 0 . 3rem ;
}
. header . subtitle {
font-size : 1rem ;
}
. header . badge {
margin-top : 0 . 5rem ;
padding : 0 . 4rem 1 . 2rem ;
font-size : 0 . 85rem ;
}
/ * Balanced meta-info for print * /
. meta-info {
padding : 1rem 1 . 5rem ;
gap : 1rem ;
page-break -inside: avoid ;
}
. meta-label {
font-size : 0 . 65rem ;
}
. meta-value {
font-size : 0 . 95rem ;
}
/ * Balanced dashboard for print * /
. dashboard {
padding : 1rem 1 . 5rem ;
page-break -inside: avoid ;
}
. stats-grid {
page-break -inside: avoid ;
display : flex ;
flex-wrap : nowrap ;
gap : 1rem ;
margin-bottom : 1rem ;
}
. stat-card {
flex : 1 ;
min-width : 0 ;
padding : 1rem ;
}
. stat-value {
font-size : 2rem ;
}
. stat-label {
font-size : 0 . 75rem ;
}
. progress-section {
margin : 0 . 75rem 0 ;
page-break -inside: avoid ;
}
. progress-bar -container {
height : 40px ;
}
. progress-bar -fill {
background : #10b981 !important;
-webkit -print -color -adjust: exact ! important ;
print-color -adjust: exact ! important ;
}
2025-12-08 10:32:49 +01:00
. module-section {
page-break -inside: avoid ;
}
. module-content {
max-height : none ! important ;
}
. settings-table {
font-size : 0 . 7rem ;
}
. settings-table td {
padding : 0 . 5rem ;
}
. settings-table td : nth-child ( 2 ) ,
. settings-table td : nth-child ( 3 ) ,
. settings-table td : nth-child ( 4 ) {
font-size : 0 . 65rem ;
}
. settings-table th : nth-child ( 1 ) { width : 22 % ; }
. settings-table th : nth-child ( 2 ) { width : 33 % ; }
. settings-table th : nth-child ( 3 ) { width : 15 % ; }
. settings-table th : nth-child ( 4 ) { width : 15 % ; }
. settings-table th : nth-child ( 5 ) { width : 15 % ; }
}
@media ( max-width : 768px ) {
. stats-grid {
grid-template -columns: 1fr ;
}
. controls {
flex-direction : column ;
}
. search-box {
width : 100 % ;
}
}
< / style >
< / head >
< body >
< div class = " container " >
< div class = " header " >
2026-01-07 18:46:14 +01:00
< h1 > NoID Privacy v2 . 2 . 3 < / h1 >
2025-12-08 10:32:49 +01:00
< p class = " subtitle " > Complete Hardening Compliance Report < / p >
< span class = " badge " > All $totalSettings Settings Verified < / span >
< / div >
< div class = " meta-info " >
< div class = " meta-item " >
< span class = " meta-label " > Report Generated < / span >
< span class = " meta-value " > $reportTimestamp < / span >
< / div >
< div class = " meta-item " >
< span class = " meta-label " > Computer Name < / span >
< span class = " meta-value " > $computerName < / span >
< / div >
< div class = " meta-item " >
< span class = " meta-label " > Operating System < / span >
< span class = " meta-value " > $osVersion < / span >
< / div >
< div class = " meta-item " >
< span class = " meta-label " > Framework Version < / span >
2026-01-07 18:46:14 +01:00
< span class = " meta-value " > NoID Privacy v2 . 2 . 3 < / span >
2025-12-08 10:32:49 +01:00
< / div >
< / div >
< div class = " dashboard " >
< div class = " stats-grid " >
< div class = " stat-card " >
< div class = " stat-value " > $totalSettings < / div >
< div class = " stat-label " > Total Settings Checked < / div >
< / div >
< div class = " stat-card success " >
< div class = " stat-value success " > $passedCount < / div >
< div class = " stat-label " > Settings Passed < / div >
< / div >
< div class = " stat-card danger " >
< div class = " stat-value danger " > $failedCount < / div >
< div class = " stat-label " > Settings Failed < / div >
< / div >
< div class = " stat-card " >
< div class = " stat-value " > $compliancePercent % < / div >
< div class = " stat-label " > Compliance Rate < / div >
< / div >
< / div >
< div class = " progress-section " >
< div class = " progress-bar-container " >
< div class = " progress-bar-fill " style = " width: $compliancePercent %; " >
$compliancePercent % Compliant
< / div >
< / div >
< / div >
< div class = " controls " >
< input type = " text " class = " search-box " id = " searchBox " placeholder = " Search settings, modules, or values... " >
< div class = " filter-buttons " >
< button class = " btn btn-primary active " onclick = " filterSettings('all') " > All Settings < / button >
< button class = " btn btn-success " onclick = " filterSettings('passed') " > Passed Only < / button >
< button class = " btn btn-danger " onclick = " filterSettings('failed') " > Failed Only < / button >
< / div >
< / div >
< / div >
< div class = " modules-container " id = " modulesContainer " >
" @
# Build module sections with details (iterate over ALL modules)
foreach ( $category in $Results . AllSettings ) {
$categoryName = $category . Category
$catTotal = $category . Total
$catPassed = $category . Passed
$catFailed = $category . Failed
$html + = @"
< div class = " module-section " id = " module- $categoryName " >
< div class = " module-header " onclick = " toggleModule('module- $categoryName ') " >
< div class = " module-title " >
< span class = " expand-icon " > & #9660;</span>
< span > $categoryName < / span >
< / div >
< div class = " module-stats " >
< span class = " module-stat " >
< span > Total : < / span >
< strong > $catTotal < / strong >
< / span >
< span class = " module-stat " style = " color: #10b981; " >
< span > Passed : < / span >
< strong > $catPassed < / strong >
< / span >
< span class = " module-stat " style = " color: #ef4444; " >
< span > Failed : < / span >
< strong > $catFailed < / strong >
< / span >
< / div >
< / div >
< div class = " module-content " >
< table class = " settings-table " >
< thead >
< tr >
< th > Setting < / th >
< th > Path / Policy < / th >
< th > Expected < / th >
< th > Actual < / th >
< th > Status < / th >
< / tr >
< / thead >
< tbody >
" @
# Add rows for PASSED settings (detailed view)
foreach ( $detail in $category . PassedDetails ) {
$rowClass = 'passed'
$statusBadge = '<span class="status-badge passed"><span class="status-icon">✓</span>Passed</span>'
# Extract setting info based on category
if ( $categoryName -eq " Registry " ) {
$settingName = if ( $detail . Name ) { $detail . Name } else { $detail . ValueName }
$pathInfo = if ( $detail . Path ) { $detail . Path } else { $detail . KeyName }
$expected = $detail . Expected
$actual = $detail . Actual
# Improve cryptic setting names
if ( $settingName -like " **del* " ) {
$settingName = " [GPO Cleanup] Remove obsolete values from: $( $pathInfo -replace '.*\\' , '' ) "
}
elseif ( $settingName -eq " (Reserved) " ) {
$settingName = " [IE Security] Reserved Entry (System-level protection) "
}
elseif ( $settingName -eq " 1 " -and $pathInfo -like " *DeviceClasses* " ) {
$settingName = " USB Storage Devices Block (GUID {d48179be-ec20-11d1-b6b8-00c04fa372a7}) "
}
elseif ( $settingName -eq " 1 " -and $pathInfo -like " *ExtensionInstallBlocklist* " ) {
$settingName = " [Edge] Block all extensions by default (wildcard) "
}
elseif ( $settingName -match " ^[0-9A-F]{4} $ " -and $pathInfo -like " *Internet Settings*Zones* " ) {
# Internet Explorer Zone Settings - Hex to readable
$zoneSettingNames = @ {
" 1C00 " = " ActiveX Controls Auto-Prompting "
" 270C " = " Software Channel Permissions "
" 1201 " = " ActiveX Download Signed Controls "
" 2001 " = " ActiveX Run Unsigned Controls "
" 2102 " = " Binary & Script Behaviors "
" 1802 " = " Script ActiveX Marked Safe "
" 160A " = " Override Per-Site ActiveX "
" 1406 " = " Font Downloads "
" 1804 " = " Script Java Applets "
" 2200 " = " Automatic Prompt File Downloads "
" 1209 " = " Run ActiveX in Office Documents "
" 1206 " = " ScriptActiveX Persist Stream Init "
" 1809 " = " Use Phishing Filter "
" 2500 " = " Protected Mode "
" 2103 " = " Allow Script Initiated Windows "
" 1606 " = " Logon Options "
" 2402 " = " Cross Domain Drag/Drop "
" 2004 " = " Cross Domain Data Access "
" 1001 " = " Download Signed ActiveX Controls "
" 1A00 " = " User Data Persistence "
" 2708 " = " Websites in Less Privileged Zones "
" 1004 " = " Download Unsigned ActiveX Controls "
" 120b " = " Run Components Not Signed Authenticode "
" 1407 " = " Run Java "
" 1409 " = " Enable .NET Scripting "
" 1607 " = " Submit Non-Encrypted Form Data "
" 2709 " = " Drag/Drop Across Domains "
" 2101 " = " Script ActiveX Marked Safe Init "
" 2301 " = " Allow META REFRESH "
" 1806 " = " Userdata Across Domains "
" 120c " = " Run Components Signed Authenticode "
" 140C " = " Active Scripting "
" 1608 " = " File Downloads "
" 1200 " = " Run ActiveX Controls & Plugins "
" 1400 " = " ActiveX Run Unsigned "
" 1402 " = " Script Java Applets "
" 1803 " = " Reserved "
" 2000 " = " Binary Behaviors "
" 1405 " = " Script ActiveX Controls "
}
$friendlyName = $zoneSettingNames [ $settingName ]
if ( $friendlyName ) {
$zoneName = if ( $pathInfo -like " *Zones\\0* " ) { " My Computer " }
2026-01-07 18:46:14 +01:00
elseif ( $pathInfo -like " *Zones\\1* " ) { " Local Intranet " }
elseif ( $pathInfo -like " *Zones\\2* " ) { " Trusted Sites " }
elseif ( $pathInfo -like " *Zones\\3* " ) { " Internet " }
elseif ( $pathInfo -like " *Zones\\4* " ) { " Restricted Sites " }
else { " Zone " }
2025-12-08 10:32:49 +01:00
$settingName = " [ $zoneName ] $friendlyName "
}
}
elseif ( $settingName -eq " DCSettingIndex " ) {
$settingName = " Power Setting (On Battery/DC) "
}
elseif ( $settingName -eq " ACSettingIndex " ) {
$settingName = " Power Setting (Plugged In/AC) "
}
elseif ( ( $settingName -eq " iexplore.exe " -or $settingName -eq " explorer.exe " ) -and $pathInfo -like " *FeatureControl* " ) {
# IE FeatureControl settings
$featureNames = @ {
2026-01-07 18:46:14 +01:00
" FEATURE_DISABLE_MK_PROTOCOL " = " Disable MK Protocol (Security) "
" FEATURE_MIME_HANDLING " = " MIME Handling Security "
" FEATURE_MIME_SNIFFING " = " MIME Sniffing Protection "
2025-12-08 10:32:49 +01:00
" FEATURE_RESTRICT_ACTIVEXINSTALL " = " Restrict ActiveX Install "
2026-01-07 18:46:14 +01:00
" FEATURE_RESTRICT_FILEDOWNLOAD " = " Restrict File Download "
" FEATURE_SECURITYBAND " = " Security Band (Info Bar) "
" FEATURE_WINDOW_RESTRICTIONS " = " Window Restrictions (Pop-up Block) "
" FEATURE_ZONE_ELEVATION " = " Zone Elevation Block "
2025-12-08 10:32:49 +01:00
}
$processName = if ( $settingName -eq " iexplore.exe " ) { " IE " } else { " Explorer " }
foreach ( $feature in $featureNames . Keys ) {
if ( $pathInfo -like " * $feature * " ) {
$settingName = " [ $processName ] $( $featureNames [ $feature ] ) "
break
}
}
}
}
elseif ( $categoryName -eq " SecurityTemplate " ) {
$settingName = $detail . Setting
$pathInfo = " Security Template "
$expected = $detail . Expected
$actual = $detail . Actual
}
elseif ( $categoryName -eq " AuditPolicies " ) {
$settingName = $detail . Policy
$pathInfo = " Audit Policy "
$expected = $detail . Expected
$actual = $detail . Actual
}
elseif ( $categoryName -eq " ASR " ) {
$settingName = $detail . Rule
$pathInfo = " ASR Rule "
$expected = $detail . Expected
$actual = $detail . Actual
}
else {
# Generic handling for other categories
$settingName = if ( $detail . Setting ) { $detail . Setting } elseif ( $detail . Check ) { $detail . Check } elseif ( $detail . Policy ) { $detail . Policy } else { " Unknown " }
$pathInfo = if ( $detail . Path ) { $detail . Path } else { $categoryName }
$expected = $detail . Expected
$actual = $detail . Actual
# EdgeHardening specific improvements
if ( $categoryName -eq " EdgeHardening " ) {
if ( $settingName -like " **delvals* " ) {
$settingName = " [Edge] GPO Cleanup - Remove obsolete policy values "
}
elseif ( $settingName -eq " 1 " ) {
# Check if path contains ExtensionInstallBlocklist
if ( $detail . Path -like " *ExtensionInstallBlocklist* " ) {
$settingName = " [Edge] Block all extensions by default (wildcard *) "
}
}
}
}
# Encode HTML special characters
$settingName = [ System.Web.HttpUtility ] :: HtmlEncode ( $settingName )
$pathInfo = [ System.Web.HttpUtility ] :: HtmlEncode ( $pathInfo )
$expected = [ System.Web.HttpUtility ] :: HtmlEncode ( $expected )
$actual = [ System.Web.HttpUtility ] :: HtmlEncode ( $actual )
$html + = @"
< tr class = " $rowClass " >
< td title = " $settingName " > $settingName < / td >
< td class = " value-cell " title = " $pathInfo " > $pathInfo < / td >
< td class = " value-cell " title = " $expected " > $expected < / td >
< td class = " value-cell " title = " $actual " > $actual < / td >
< td > $statusBadge < / td >
< / tr >
" @
}
# Add rows for FAILED settings (detailed view)
foreach ( $detail in $category . FailedDetails ) {
$rowClass = 'failed'
$statusBadge = '<span class="status-badge failed"><span class="status-icon">✕</span>Failed</span>'
# Extract setting info based on category
if ( $categoryName -eq " RegistryPolicies " -or $categoryName -eq " Registry " ) {
$settingName = if ( $detail . ValueName ) { $detail . ValueName } elseif ( $detail . Name ) { $detail . Name } else { " Unknown " }
$pathInfo = if ( $detail . KeyName ) { $detail . KeyName } elseif ( $detail . Path ) { $detail . Path } else { " Unknown " }
$expected = $detail . Expected
$actual = $detail . Actual
# Improve cryptic setting names (same logic as passed details)
if ( $settingName -like " **del* " ) {
$settingName = " [GPO Cleanup] Remove obsolete values from: $( $pathInfo -replace '.*\\' , '' ) "
}
elseif ( $settingName -eq " (Reserved) " ) {
$settingName = " [IE Security] Reserved Entry (System-level protection) "
}
elseif ( $settingName -eq " 1 " -and $pathInfo -like " *DeviceClasses* " ) {
$settingName = " USB Storage Devices Block (GUID {d48179be-ec20-11d1-b6b8-00c04fa372a7}) "
}
elseif ( $settingName -eq " 1 " -and $pathInfo -like " *ExtensionInstallBlocklist* " ) {
$settingName = " [Edge] Block all extensions by default (wildcard) "
}
elseif ( $settingName -match " ^[0-9A-F]{4} $ " -and $pathInfo -like " *Internet Settings*Zones* " ) {
# Internet Explorer Zone Settings - Hex to readable
$zoneSettingNames = @ {
" 1C00 " = " ActiveX Controls Auto-Prompting "
" 270C " = " Software Channel Permissions "
" 1201 " = " ActiveX Download Signed Controls "
" 2001 " = " ActiveX Run Unsigned Controls "
" 2102 " = " Binary & Script Behaviors "
" 1802 " = " Script ActiveX Marked Safe "
" 160A " = " Override Per-Site ActiveX "
" 1406 " = " Font Downloads "
" 1804 " = " Script Java Applets "
" 2200 " = " Automatic Prompt File Downloads "
" 1209 " = " Run ActiveX in Office Documents "
" 1206 " = " ScriptActiveX Persist Stream Init "
" 1809 " = " Use Phishing Filter "
" 2500 " = " Protected Mode "
" 2103 " = " Allow Script Initiated Windows "
" 1606 " = " Logon Options "
" 2402 " = " Cross Domain Drag/Drop "
" 2004 " = " Cross Domain Data Access "
" 1001 " = " Download Signed ActiveX Controls "
" 1A00 " = " User Data Persistence "
" 2708 " = " Websites in Less Privileged Zones "
" 1004 " = " Download Unsigned ActiveX Controls "
" 120b " = " Run Components Not Signed Authenticode "
" 1407 " = " Run Java "
" 1409 " = " Enable .NET Scripting "
" 1607 " = " Submit Non-Encrypted Form Data "
" 2709 " = " Drag/Drop Across Domains "
" 2101 " = " Script ActiveX Marked Safe Init "
" 2301 " = " Allow META REFRESH "
" 1806 " = " Userdata Across Domains "
" 120c " = " Run Components Signed Authenticode "
" 140C " = " Active Scripting "
" 1608 " = " File Downloads "
" 1200 " = " Run ActiveX Controls & Plugins "
" 1400 " = " ActiveX Run Unsigned "
" 1402 " = " Script Java Applets "
" 1803 " = " Reserved "
" 2000 " = " Binary Behaviors "
" 1405 " = " Script ActiveX Controls "
}
$friendlyName = $zoneSettingNames [ $settingName ]
if ( $friendlyName ) {
$zoneName = if ( $pathInfo -like " *Zones\\0* " -or $pathInfo -like " *Zones\0* " ) { " My Computer " }
2026-01-07 18:46:14 +01:00
elseif ( $pathInfo -like " *Zones\\1* " -or $pathInfo -like " *Zones\1* " ) { " Local Intranet " }
elseif ( $pathInfo -like " *Zones\\2* " -or $pathInfo -like " *Zones\2* " ) { " Trusted Sites " }
elseif ( $pathInfo -like " *Zones\\3* " -or $pathInfo -like " *Zones\3* " ) { " Internet " }
elseif ( $pathInfo -like " *Zones\\4* " -or $pathInfo -like " *Zones\4* " ) { " Restricted Sites " }
else { " Zone " }
2025-12-08 10:32:49 +01:00
$settingName = " [ $zoneName ] $friendlyName "
}
}
elseif ( $settingName -eq " DCSettingIndex " ) {
$settingName = " Power Setting (On Battery/DC) "
}
elseif ( $settingName -eq " ACSettingIndex " ) {
$settingName = " Power Setting (Plugged In/AC) "
}
elseif ( ( $settingName -eq " iexplore.exe " -or $settingName -eq " explorer.exe " ) -and $pathInfo -like " *FeatureControl* " ) {
# IE FeatureControl settings
$featureNames = @ {
2026-01-07 18:46:14 +01:00
" FEATURE_DISABLE_MK_PROTOCOL " = " Disable MK Protocol (Security) "
" FEATURE_MIME_HANDLING " = " MIME Handling Security "
" FEATURE_MIME_SNIFFING " = " MIME Sniffing Protection "
2025-12-08 10:32:49 +01:00
" FEATURE_RESTRICT_ACTIVEXINSTALL " = " Restrict ActiveX Install "
2026-01-07 18:46:14 +01:00
" FEATURE_RESTRICT_FILEDOWNLOAD " = " Restrict File Download "
" FEATURE_SECURITYBAND " = " Security Band (Info Bar) "
" FEATURE_WINDOW_RESTRICTIONS " = " Window Restrictions (Pop-up Block) "
" FEATURE_ZONE_ELEVATION " = " Zone Elevation Block "
2025-12-08 10:32:49 +01:00
}
$processName = if ( $settingName -eq " iexplore.exe " ) { " IE " } else { " Explorer " }
foreach ( $feature in $featureNames . Keys ) {
if ( $pathInfo -like " * $feature * " ) {
$settingName = " [ $processName ] $( $featureNames [ $feature ] ) "
break
}
}
}
}
elseif ( $categoryName -eq " SecurityTemplate " ) {
$settingName = $detail . Setting
$pathInfo = " Security Template "
$expected = $detail . Expected
$actual = $detail . Actual
}
elseif ( $categoryName -eq " AuditPolicies " ) {
$settingName = $detail . Policy
$pathInfo = " Audit Policy "
$expected = $detail . Expected
$actual = $detail . Actual
}
elseif ( $categoryName -eq " ASR " ) {
$settingName = $detail . Rule
$pathInfo = " ASR Rule "
$expected = $detail . Expected
$actual = $detail . Actual
}
else {
# Generic handling for other categories
$settingName = if ( $detail . Setting ) { $detail . Setting } elseif ( $detail . Check ) { $detail . Check } elseif ( $detail . Policy ) { $detail . Policy } else { " Unknown " }
$pathInfo = if ( $detail . Path ) { $detail . Path } else { $categoryName }
$expected = $detail . Expected
$actual = $detail . Actual
# EdgeHardening specific improvements
if ( $categoryName -eq " EdgeHardening " ) {
if ( $settingName -like " **delvals* " ) {
$settingName = " [Edge] GPO Cleanup - Remove obsolete policy values "
}
elseif ( $settingName -eq " 1 " ) {
# Check if path contains ExtensionInstallBlocklist
if ( $detail . Path -like " *ExtensionInstallBlocklist* " ) {
$settingName = " [Edge] Block all extensions by default (wildcard *) "
}
}
}
}
# Encode HTML special characters
$settingName = [ System.Web.HttpUtility ] :: HtmlEncode ( $settingName )
$pathInfo = [ System.Web.HttpUtility ] :: HtmlEncode ( $pathInfo )
$expected = [ System.Web.HttpUtility ] :: HtmlEncode ( $expected )
$actual = [ System.Web.HttpUtility ] :: HtmlEncode ( $actual )
$valueClass = if ( $detail . Status -eq 'Pass' ) { 'value-match' } else { 'value-mismatch' }
$html + = @"
< tr class = " $rowClass " >
< td title = " $settingName " > $settingName < / td >
< td class = " value-cell " title = " $pathInfo " > $pathInfo < / td >
< td class = " value-cell " title = " $expected " > $expected < / td >
< td class = " value-cell $valueClass " title = " $actual " > $actual < / td >
< td > $statusBadge < / td >
< / tr >
" @
}
# If no failed settings, show success message
if ( $catFailed -eq 0 -and $catPassed -eq 0 ) {
$html + = @"
< tr >
< td colspan = " 5 " style = " padding: 2rem; text-align: center; color: #64748b; " >
No settings configured for this module
< / td >
< / tr >
" @
}
$html + = @"
< / tbody >
< / table >
< / div >
< / div >
" @
}
# Close HTML
$html + = @"
< / div >
< div class = " export-section " >
< button class = " btn btn-primary " onclick = " window.print() " > Print Report < / button >
< / div >
< div class = " footer " >
2026-01-07 18:46:14 +01:00
< p > Generated by NoID Privacy v2 . 2 . 3 < / p >
2025-12-08 10:32:49 +01:00
< p > Professional Windows 11 Security & Privacy Hardening Framework < / p >
< / div >
< / div >
< script >
document . getElementById ( 'searchBox' ) . addEventListener ( 'input' , function ( e ) {
const searchTerm = e . target . value . toLowerCase ( ) ;
const rows = document . querySelectorAll ( '.settings-table tbody tr' ) ;
rows . forEach ( row = > {
const text = row . textContent . toLowerCase ( ) ;
row . style . display = text . includes ( searchTerm ) ? '' : 'none' ;
} ) ;
} ) ;
function filterSettings(filter ) {
const buttons = document . querySelectorAll ( '.filter-buttons .btn' ) ;
buttons . forEach ( btn = > btn . classList . remove ( 'active' ) ) ;
event . target . classList . add ( 'active' ) ;
const rows = document . querySelectorAll ( '.settings-table tbody tr' ) ;
rows . forEach ( row = > {
if ( filter = = = 'all' ) {
row . style . display = '' ;
} else if ( filter = = = 'passed' ) {
row . style . display = row . classList . contains ( 'passed' ) ? '' : 'none' ;
} else if ( filter = = = 'failed' ) {
row . style . display = row . classList . contains ( 'failed' ) ? '' : 'none' ;
}
} ) ;
}
function toggleModule(moduleId ) {
const section = document . getElementById ( moduleId ) ;
section . classList . toggle ( 'collapsed' ) ;
}
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
const modules = document . querySelectorAll ( '.module-section' ) ;
modules . forEach ( ( module , index ) = > {
if ( index > 0 ) {
module . classList . add ( 'collapsed' ) ;
}
} ) ;
} ) ;
< / script >
< / body >
< / html >
" @
# Save HTML file
$html | Out-File -FilePath $OutputFile -Encoding UTF8
} -Results $results -OutputFile $htmlFile
Write-Host " "
Write-Host " ======================================== " -ForegroundColor Green
Write-Host " HTML COMPLIANCE REPORT GENERATED " -ForegroundColor Green
Write-Host " ======================================== " -ForegroundColor Green
Write-Host " "
Write-Host " Report Location: " -ForegroundColor Cyan
Write-Host " $htmlFile " -ForegroundColor White
Write-Host " "
Write-Host " Open this file in your browser to view the detailed compliance report " -ForegroundColor Gray
Write-Host " with all $( $results . TotalSettings ) settings verified! " -ForegroundColor Gray
Write-Host " "
}
catch {
Write-Host " Warning: Failed to generate HTML report: $_ " -ForegroundColor Yellow
}
# Final status message
if ( $results . Failed -eq 0 ) {
Write-Host " [+] ALL SETTINGS VERIFIED SUCCESSFULLY! " -ForegroundColor Green
}
else {
Write-Host " [-] SOME SETTINGS FAILED VERIFICATION " -ForegroundColor Red
}
# Return result (don't use exit - causes output buffer issues when called from interactive shell)
return $results . Failed -eq 0