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