noid-privacy/Core/Framework.ps1
NexusOne23 d78d941113 v2.2.0: Fix Privacy settings count + DoH connectivity test
Privacy Module:
- Fixed 'Applied X settings' to show only registry settings (60/78/86)
- Bloatware count no longer added to settings total
- Consistent with module prompt (MSRecommended: 60, Strict: 78, Paranoid: 86)

DNS Module:
- Fixed DoH connectivity test for systems with REQUIRE mode active
- Tests HTTPS endpoint (port 443) when classic DNS is blocked
- Proper detection of existing DoH configuration

Verified: Full Apply/Verify/Restore cycle - 633/633 settings (100%)
2025-12-09 10:48:12 +01:00

927 lines
42 KiB
PowerShell

<#
.SYNOPSIS
Main orchestration engine for NoID Privacy Framework
.DESCRIPTION
Core framework that orchestrates module execution, manages configuration,
logging, validation, and rollback functionality.
.NOTES
Author: NexusOne23
Version: 2.2.0
Requires: PowerShell 5.1+
.EXAMPLE
.\Framework.ps1 -DryRun
Run in dry-run mode to preview changes
.EXAMPLE
.\Framework.ps1 -ModulesOnly SecurityBaseline,ASR
Run only specific modules
#>
# Note: This script is dot-sourced as a library, not called directly with parameters.
# All configuration comes from config.json via Initialize-Config.
# Script-level variables
$script:FrameworkVersion = "2.2.0"
$script:FrameworkRoot = Split-Path -Parent $PSScriptRoot
$script:ExecutionStartTime = Get-Date
# Import core and utility modules
$script:ModulesToLoad = @(
[PSCustomObject]@{ Path = "Core\Logger.ps1"; Name = "Logger" },
[PSCustomObject]@{ Path = "Core\Config.ps1"; Name = "Config" },
[PSCustomObject]@{ Path = "Core\Validator.ps1"; Name = "Validator" },
[PSCustomObject]@{ Path = "Core\Rollback.ps1"; Name = "Rollback" },
[PSCustomObject]@{ Path = "Core\NonInteractive.ps1"; Name = "NonInteractive" },
[PSCustomObject]@{ Path = "Utils\Registry.ps1"; Name = "Registry Utils" },
[PSCustomObject]@{ Path = "Utils\Service.ps1"; Name = "Service Utils" },
[PSCustomObject]@{ Path = "Utils\Hardware.ps1"; Name = "Hardware Utils" },
# NOTE: Utils\GPO.ps1 removed - v2.0 SecurityBaseline is self-contained, no LGPO.exe dependency
[PSCustomObject]@{ Path = "Utils\Localization.ps1"; Name = "Localization Utils" },
[PSCustomObject]@{ Path = "Utils\Compatibility.ps1"; Name = "Compatibility Utils" },
[PSCustomObject]@{ Path = "Utils\Dependencies.ps1"; Name = "Dependencies Utils" }
)
Write-Host "NoID Privacy Framework v$script:FrameworkVersion" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
foreach ($moduleInfo in $script:ModulesToLoad) {
$modulePath = Join-Path $script:FrameworkRoot $moduleInfo.Path
if (Test-Path -Path $modulePath) {
try {
. $modulePath
Write-Host "[OK] Loaded: $($moduleInfo.Name)" -ForegroundColor Green
}
catch {
Write-Host "[ERROR] Failed to load: $($moduleInfo.Name)" -ForegroundColor Red
Write-Host "Error: $_" -ForegroundColor Red
exit 1
}
}
else {
Write-Host "[ERROR] Module not found: $($moduleInfo.Name) ($modulePath)" -ForegroundColor Red
exit 1
}
}
Write-Host ""
function Initialize-Framework {
<#
.SYNOPSIS
Initialize the framework and all subsystems
#>
[CmdletBinding()]
param()
Write-Host "Initializing framework..." -ForegroundColor Cyan
# Only initialize logger if not already initialized (NoIDPrivacy.ps1 may have already done it)
if ([string]::IsNullOrEmpty($global:LoggerConfig.LogFilePath)) {
$logLevel = if ($VerboseLogging) { [LogLevel]::DEBUG } else { [LogLevel]::INFO }
$logDirectory = Join-Path $script:FrameworkRoot "Logs"
Initialize-Logger -LogDirectory $logDirectory -MinimumLevel $logLevel
}
Write-Log -Level INFO -Message "NoID Privacy Framework v$script:FrameworkVersion starting" -Module "Framework"
Write-Log -Level INFO -Message "PowerShell version: $($PSVersionTable.PSVersion)" -Module "Framework"
# Load configuration
if ($ConfigPath) {
Initialize-Config -ConfigPath $ConfigPath
}
else {
Initialize-Config
}
# Initialize backup system
if (-not $SkipBackup) {
Initialize-BackupSystem
# Create system restore point
Write-Host "Creating system restore point..." -ForegroundColor Yellow
$restorePointCreated = New-SystemRestorePoint
if (-not $restorePointCreated) {
Write-Log -Level WARNING -Message "System restore point creation failed or unavailable" -Module "Framework"
}
}
else {
Write-Log -Level WARNING -Message "Backup system SKIPPED (not recommended)" -Module "Framework"
}
Write-Host ""
}
function Test-FrameworkPrerequisites {
<#
.SYNOPSIS
Validate all prerequisites before execution
.OUTPUTS
Boolean indicating if prerequisites are met
#>
[CmdletBinding()]
[OutputType([bool])]
param()
Write-Host "Validating system prerequisites..." -ForegroundColor Cyan
Write-Host ""
$prereqsPassed = Test-Prerequisites
if (-not $prereqsPassed.Success) {
Write-Log -Level ERROR -Message "Prerequisite validation failed" -Module "Framework"
Write-Host ""
Write-Host "PREREQUISITE CHECK FAILED" -ForegroundColor Red
Write-Host "Please resolve the issues above before continuing." -ForegroundColor Red
return $false
}
Write-Host ""
Write-Host "All basic prerequisite checks passed" -ForegroundColor Green
Write-Host ""
# Check if system is domain-joined (interactive warning only in CLI mode)
Write-Host "Checking domain status..." -ForegroundColor Cyan
if (Test-NonInteractiveMode) {
# GUI mode - just check, don't prompt
$null = Test-DomainJoined
}
else {
# CLI mode - show interactive warning
$null = Test-DomainJoined -Interactive
}
Write-Host ""
# Confirm system backup exists (interactive prompt only in CLI mode)
Write-Host "Verifying system backup..." -ForegroundColor Cyan
if (Test-NonInteractiveMode) {
# GUI mode - auto-confirm (backup is created by engine)
Write-Host "[GUI] Backup verification: Auto-confirmed" -ForegroundColor Cyan
$backupStatus = [PSCustomObject]@{ UserConfirmed = $true }
}
else {
# CLI mode - interactive prompt
$backupStatus = Confirm-SystemBackup
}
if (-not $backupStatus.UserConfirmed) {
Write-Log -Level ERROR -Message "System backup confirmation failed" -Module "Framework"
return $false
}
Write-Host ""
Write-Host "All prerequisite checks completed successfully" -ForegroundColor Green
Write-Host ""
return $true
}
function Get-ModulesToExecute {
<#
.SYNOPSIS
Determine which modules should be executed
.OUTPUTS
Array of module names to execute
#>
[CmdletBinding()]
[OutputType([string[]])]
param()
if ($ModulesOnly -and $ModulesOnly.Count -gt 0) {
Write-Log -Level INFO -Message "Running specific modules: $($ModulesOnly -join ', ')" -Module "Framework"
return $ModulesOnly
}
else {
$enabledModules = Get-EnabledModules
Write-Log -Level INFO -Message "Running all enabled modules: $($enabledModules -join ', ')" -Module "Framework"
return $enabledModules
}
}
# NOTE: Invoke-HardeningModule has been removed.
# Use Invoke-Hardening instead for module execution.
function Start-HardeningProcess {
<#
.SYNOPSIS
Main execution entry point
#>
[CmdletBinding()]
param()
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "STARTING HARDENING PROCESS" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
if ($DryRun) {
Write-Host "MODE: DRY RUN (Preview only)" -ForegroundColor Yellow
Write-Log -Level WARNING -Message "Running in DRY RUN mode - no changes will be applied" -Module "Framework"
}
else {
Write-Host "MODE: APPLY CHANGES" -ForegroundColor Green
Write-Log -Level INFO -Message "Running in APPLY mode - changes will be applied" -Module "Framework"
}
Write-Host ""
# Get modules to execute
$modulesToRun = Get-ModulesToExecute
if ($modulesToRun.Count -eq 0) {
Write-Log -Level WARNING -Message "No modules enabled for execution" -Module "Framework"
Write-Host "No modules to execute. Check your configuration." -ForegroundColor Yellow
return
}
Write-Host "Modules to execute: $($modulesToRun.Count)" -ForegroundColor Cyan
foreach ($mod in $modulesToRun) {
Write-Host " - $mod" -ForegroundColor White
}
Write-Host ""
# Confirmation prompt (unless in dry run or NonInteractive mode)
if (-not $DryRun -and -not (Test-NonInteractiveMode)) {
Write-Host "WARNING: This will modify your system settings." -ForegroundColor Yellow
Write-Host "A backup and restore point have been created." -ForegroundColor Yellow
Write-Host ""
do {
$confirmation = Read-Host "Do you want to continue? [Y/N] (default: Y)"
if ([string]::IsNullOrWhiteSpace($confirmation)) { $confirmation = "Y" }
$confirmation = $confirmation.ToUpper()
if ($confirmation -notin @('Y', 'N')) {
Write-Host ""
Write-Host "Invalid input. Please enter Y or N." -ForegroundColor Red
Write-Host ""
}
} while ($confirmation -notin @('Y', 'N'))
if ($confirmation -ne "Y") {
Write-Log -Level INFO -Message "Execution cancelled by user" -Module "Framework"
Write-Host ""
Write-Host "Execution cancelled." -ForegroundColor Yellow
Write-Host ""
return
}
Write-Host ""
}
elseif (Test-NonInteractiveMode) {
Write-NonInteractiveDecision -Module "Framework" -Decision "Auto-confirming execution (GUI mode)"
}
# Execute modules using Invoke-Hardening
Write-Log -Level INFO -Message "Starting module execution: $($modulesToRun -join ', ')" -Module "Framework"
# Determine correct module parameter based on what user selected
if ($modulesToRun.Count -eq 1) {
$hardeningResult = Invoke-Hardening -Module $modulesToRun[0] -DryRun:$DryRun
}
else {
$hardeningResult = Invoke-Hardening -Module "All" -DryRun:$DryRun
}
# Summary (correctly use hardeningResult.ModuleResults)
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "EXECUTION SUMMARY" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
$totalDuration = (Get-Date) - $script:ExecutionStartTime
# Correct calculation from ModuleResults
$totalModules = $hardeningResult.ModulesExecuted
$successCount = ($hardeningResult.ModuleResults | Where-Object { $_.Success }).Count
$failureCount = $totalModules - $successCount
Write-Host "Total modules executed: $totalModules" -ForegroundColor White
Write-Host "Successful: $successCount" -ForegroundColor Green
Write-Host "Failed: $failureCount" -ForegroundColor $(if ($failureCount -gt 0) { "Red" } else { "White" })
Write-Host "Total duration: $([math]::Round($totalDuration.TotalMinutes, 2)) minutes" -ForegroundColor White
if ($hardeningResult.Warnings.Count -gt 0) {
Write-Host "Warnings: $($hardeningResult.Warnings.Count)" -ForegroundColor Yellow
}
if ($hardeningResult.Errors.Count -gt 0) {
Write-Host "Errors: $($hardeningResult.Errors.Count)" -ForegroundColor Red
}
Write-Host ""
Write-Log -Level INFO -Message "Hardening process completed" -Module "Framework"
Write-Log -Level INFO -Message "Log file: $(Get-LogFilePath)" -Module "Framework"
Write-Host "Log file: $(Get-LogFilePath)" -ForegroundColor Cyan
Write-Host ""
# Reboot recommendation
if (-not $DryRun -and $successCount -gt 0) {
Write-Host "RECOMMENDATION: Restart your computer for all changes to take effect." -ForegroundColor Yellow
Write-Host ""
}
}
function Invoke-Hardening {
<#
.SYNOPSIS
Execute hardening module(s)
.PARAMETER Module
Module name to execute (SecurityBaseline, ASR, DNS, etc.) or "All"
.PARAMETER DryRun
Preview changes without applying them
.OUTPUTS
PSCustomObject with execution results
#>
[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
[Parameter(Mandatory = $true)]
[ValidateSet("SecurityBaseline", "ASR", "DNS", "Privacy", "AntiAI", "EdgeHardening", "AdvancedSecurity", "All")]
[string]$Module,
[Parameter(Mandatory = $false)]
[switch]$DryRun
)
$startTime = Get-Date
$results = [PSCustomObject]@{
Success = $true
ModulesExecuted = 0
Duration = $null
Errors = @()
Warnings = @()
ModuleResults = @()
}
# Get module list to execute
$modulesToExecute = @()
if ($Module -eq "All") {
# Get all enabled modules from config (sorted by priority)
$modulesToExecute = Get-EnabledModules
}
else {
# Single module - check if it's enabled in config
$moduleConfig = $script:Config.modules.$Module
if ($null -eq $moduleConfig) {
Write-Log -Level WARNING -Message "Module '$Module' not found in configuration" -Module "Framework"
$results.Warnings += "Module '$Module' not configured"
return $results
}
if ($moduleConfig.enabled -eq $false) {
Write-Log -Level INFO -Message "Module '$Module' is disabled in configuration - skipping" -Module "Framework"
Write-Host "Module '$Module' is disabled - skipping execution" -ForegroundColor Gray
return $results
}
$modulesToExecute = @($Module)
}
Write-Log -Level INFO -Message "Executing modules: $($modulesToExecute -join ', ')" -Module "Framework"
# Initialize backup system ONCE before all modules
if (-not $DryRun) {
try {
Initialize-BackupSystem
Write-Log -Level INFO -Message "Backup session initialized for all modules" -Module "Framework"
# Set session type from GUI config (for backup identification)
# SessionType is in options block when sent from GUI
if ($script:Config.options -and $script:Config.options.PSObject.Properties.Name -contains 'sessionType' -and $script:Config.options.sessionType) {
Set-SessionType -SessionType $script:Config.options.sessionType
Write-Log -Level DEBUG -Message "Session type from GUI config: $($script:Config.options.sessionType)" -Module "Framework"
}
else {
# CLI mode: Auto-detect session type based on module count
$autoSessionType = if ($modulesToExecute.Count -ge 7) { "wizard" }
elseif ($modulesToExecute.Count -eq 1) { "advanced" }
else { "manual" }
Set-SessionType -SessionType $autoSessionType
Write-Log -Level DEBUG -Message "Session type auto-detected: $autoSessionType (based on $($modulesToExecute.Count) modules)" -Module "Framework"
}
# Create Pre-Framework Snapshot for ASR Rules (shared resource conflict prevention)
# Only when: Multi-module apply AND ASR module is in the list
# Why: SecurityBaseline sets 15 ASR rules, then ASR sets 19 rules
# We need to capture the ORIGINAL state before ANY module touches ASR
if ($modulesToExecute.Count -gt 1 -and $modulesToExecute -contains "ASR") {
Write-Log -Level INFO -Message "Creating Pre-Framework ASR snapshot (multi-module apply with ASR detected)" -Module "Framework"
try {
$sessionPath = $global:BackupBasePath
$mpPref = Get-MpPreference -ErrorAction SilentlyContinue
if (-not $mpPref) {
Write-Log -Level INFO -Message "Pre-Framework ASR snapshot skipped: Get-MpPreference returned no data (Defender/ASR not available)." -Module "Framework"
}
else {
$hasIdsProp = $mpPref.PSObject.Properties.Match('AttackSurfaceReductionRules_Ids').Count -gt 0
$hasActionsProp = $mpPref.PSObject.Properties.Match('AttackSurfaceReductionRules_Actions').Count -gt 0
if (-not $hasIdsProp -and -not $hasActionsProp) {
Write-Log -Level INFO -Message "Pre-Framework ASR snapshot skipped: ASR rule properties not present (third-party AV or Defender ASR disabled)." -Module "Framework"
}
else {
$ruleIds = @()
$ruleActions = @()
if ($hasIdsProp -and $mpPref.AttackSurfaceReductionRules_Ids) {
$ruleIds = @($mpPref.AttackSurfaceReductionRules_Ids)
}
if ($hasActionsProp -and $mpPref.AttackSurfaceReductionRules_Actions) {
$ruleActions = @($mpPref.AttackSurfaceReductionRules_Actions)
}
$ruleCount = $ruleIds.Count
$preFrameworkSnapshot = @{
ASR = @{
RuleIds = $ruleIds
RuleActions = $ruleActions
SnapshotDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
RuleCount = $ruleCount
}
AppliesTo = @("ASR") # Only apply this snapshot if ASR module is being restored
}
$snapshotPath = Join-Path $sessionPath "PreFramework_Snapshot.json"
$preFrameworkSnapshot | ConvertTo-Json -Depth 10 | Out-File $snapshotPath -Encoding UTF8
Write-Log -Level SUCCESS -Message "Pre-Framework snapshot saved: $ruleCount ASR rules captured (original system state)" -Module "Framework"
}
}
}
catch {
Write-Log -Level WARNING -Message "Failed to create Pre-Framework snapshot (non-critical): $_" -Module "Framework"
}
}
}
catch {
Write-ErrorLog -Message "Failed to initialize backup system" -Module "Framework" -ErrorRecord $_
$warnMsg = "Backup system initialization failed - proceeding without automatic backup"
$results.Warnings += $warnMsg
}
}
# Execute each module
foreach ($moduleName in $modulesToExecute) {
try {
Write-Log -Level INFO -Message "========================================" -Module "Framework"
Write-Log -Level INFO -Message "Module: $moduleName" -Module "Framework"
Write-Log -Level INFO -Message "========================================" -Module "Framework"
# Module Confirmation Prompt for interactive CLI runs.
# Skipped in DryRun and in NonInteractive/GUI mode.
if (-not $DryRun -and -not (Test-NonInteractiveMode)) {
Write-Host ""
Write-Host "========================================" -ForegroundColor Yellow
Write-Host " MODULE: $moduleName" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Yellow
Write-Host ""
# Module-specific description
switch ($moduleName) {
"SecurityBaseline" {
Write-Host "Microsoft Security Baseline for Windows 11 25H2" -ForegroundColor White
Write-Host ""
Write-Host " > Applies 425 hardening settings:" -ForegroundColor Gray
Write-Host " - 335 Registry policies (password, firewall, BitLocker)" -ForegroundColor Gray
Write-Host " - 67 Security template settings (user rights, audit)" -ForegroundColor Gray
Write-Host " - 23 Advanced audit policies" -ForegroundColor Gray
Write-Host " - VBS + Credential Guard + Memory Integrity" -ForegroundColor Gray
Write-Host ""
Write-Host " Impact: Enterprise-grade security, may break legacy software" -ForegroundColor Yellow
}
"ASR" {
Write-Host "Attack Surface Reduction Rules" -ForegroundColor White
Write-Host ""
Write-Host " > Applies 19 Microsoft Defender ASR rules:" -ForegroundColor Gray
Write-Host " - Block ransomware, exploits, malicious scripts" -ForegroundColor Gray
Write-Host " - Block credential theft (lsass.exe protection)" -ForegroundColor Gray
Write-Host " - Block Office macros, email executables" -ForegroundColor Gray
Write-Host " - Block untrusted USB execution, Safe Mode reboot" -ForegroundColor Gray
Write-Host ""
Write-Host " Note: You'll be asked about SCCM/Intune usage" -ForegroundColor Yellow
}
"DNS" {
Write-Host "Secure DNS with DNS-over-HTTPS" -ForegroundColor White
Write-Host ""
Write-Host " > Configures encrypted DNS:" -ForegroundColor Gray
Write-Host " - Choose provider: Quad9 (default), Cloudflare, or AdGuard" -ForegroundColor Gray
Write-Host " - Enable DoH encryption (HTTPS)" -ForegroundColor Gray
Write-Host " - Blocks DNS hijacking and snooping" -ForegroundColor Gray
Write-Host " - IPv4 + IPv6 configuration" -ForegroundColor Gray
Write-Host ""
Write-Host " Note: You'll choose provider and DoH mode interactively" -ForegroundColor Yellow
}
"Privacy" {
Write-Host "Telemetry & Privacy Hardening" -ForegroundColor White
Write-Host ""
Write-Host " > Applies privacy settings based on selected mode:" -ForegroundColor Gray
Write-Host " - Telemetry control (3 modes: MSRecommended/Strict/Paranoid)" -ForegroundColor Gray
Write-Host " - MSRecommended: 60 settings (default, max compatibility)" -ForegroundColor DarkGray
Write-Host " - Strict: 78 settings, Paranoid: 86 settings" -ForegroundColor DarkGray
Write-Host " - Disable ads, tips, personalization" -ForegroundColor Gray
Write-Host " - Remove bloatware (up to 24 apps, if present)" -ForegroundColor Gray
Write-Host " - OneDrive hardening (keeps sync functional)" -ForegroundColor Gray
Write-Host ""
Write-Host " Note: You'll choose privacy mode interactively" -ForegroundColor Yellow
}
"AntiAI" {
Write-Host "Disable Windows 11 AI Features" -ForegroundColor White
Write-Host ""
Write-Host " > Disables 15 features via 32 policies:" -ForegroundColor Gray
Write-Host " - Windows Recall + Export Block" -ForegroundColor Gray
Write-Host " - Windows Copilot (app + URI handlers + Edge sidebar)" -ForegroundColor Gray
Write-Host " - Click to Do, Explorer AI Actions" -ForegroundColor Gray
Write-Host " - Paint AI (3), Notepad AI, Settings Agent" -ForegroundColor Gray
Write-Host ""
Write-Host " Impact: All AI features disabled, reboot required" -ForegroundColor Yellow
}
"EdgeHardening" {
Write-Host "Microsoft Edge v139 Security Baseline" -ForegroundColor White
Write-Host ""
Write-Host " > Applies Edge security policies:" -ForegroundColor Gray
Write-Host " - Enhanced Security Mode, SmartScreen + PUA" -ForegroundColor Gray
Write-Host " - Site Isolation + SSL/TLS hardening" -ForegroundColor Gray
Write-Host " - Tracking Prevention + Privacy settings" -ForegroundColor Gray
Write-Host " - Extension blocklist (blocks all by default)" -ForegroundColor Gray
Write-Host ""
Write-Host " Impact: Maximum Edge security, extensions blocked by default" -ForegroundColor Yellow
}
"AdvancedSecurity" {
Write-Host "Advanced Security Hardening (Beyond MS Baseline)" -ForegroundColor White
Write-Host ""
Write-Host " > Applies 15 security features (50 individual settings):" -ForegroundColor Gray
Write-Host " - RDP hardening + optional complete disable" -ForegroundColor Gray
Write-Host " - WDigest credential protection" -ForegroundColor Gray
Write-Host " - Admin Shares disable (domain-aware)" -ForegroundColor Gray
Write-Host " - Risky ports/services block (LLMNR, NetBIOS, UPnP)" -ForegroundColor Gray
Write-Host " - Legacy TLS 1.0/1.1 disable, WPAD disable, PSv2 removal" -ForegroundColor Gray
Write-Host " - SRP .lnk protection (CVE-2025-9491)" -ForegroundColor Gray
Write-Host " - Windows Update (3 simple GUI settings)" -ForegroundColor Gray
Write-Host " - Wireless Display (Miracast) security" -ForegroundColor Gray
Write-Host ""
Write-Host " Note: You'll choose profile (Balanced/Enterprise/Maximum)" -ForegroundColor Yellow
}
default {
Write-Host "This module will apply changes to your system." -ForegroundColor White
Write-Host ""
}
}
Write-Host ""
Write-Host "Options:" -ForegroundColor White
Write-Host " [Y] Yes - Apply this module" -ForegroundColor Green
Write-Host " [N] No - Skip this module" -ForegroundColor Yellow
Write-Host " [A] Abort - Stop entire process" -ForegroundColor Red
Write-Host ""
do {
$response = Read-Host "Continue with ${moduleName}? [Y/N/A] (default: Y)"
if ([string]::IsNullOrWhiteSpace($response)) { $response = "Y" }
$response = $response.ToUpper()
} while ($response -notin @('Y', 'N', 'A', 'YES', 'NO', 'ABORT'))
if ($response -in @('A', 'ABORT')) {
Write-Log -Level WARNING -Message "User aborted execution at module: $moduleName" -Module "Framework"
Write-Host ""
Write-Host "Execution aborted by user" -ForegroundColor Red
Write-Host ""
$results.Warnings += "Execution aborted by user at module: $moduleName"
break
}
elseif ($response -in @('N', 'NO')) {
Write-Log -Level INFO -Message "User skipped module: $moduleName" -Module "Framework"
Write-Host ""
Write-Host "Skipping module: $moduleName" -ForegroundColor Yellow
Write-Host ""
continue
}
Write-Host ""
Write-Host "Proceeding with $moduleName..." -ForegroundColor Green
Write-Host ""
}
$moduleResult = $null
$modulePath = Join-Path $script:FrameworkRoot "Modules\$moduleName"
# Check if module exists
if (-not (Test-Path $modulePath)) {
$errMsg = "Module not found: $moduleName (Path: $modulePath)"
Write-Log -Level WARNING -Message $errMsg -Module "Framework"
$results.Warnings += $errMsg
continue
}
# Check module implementation status (FIX #2)
$moduleConfig = $script:Config.modules.$moduleName
if ($moduleConfig.PSObject.Properties.Name -contains 'status') {
if ($moduleConfig.status -ne 'IMPLEMENTED') {
Write-Log -Level WARNING -Message "Skipping module '$moduleName' - Status: $($moduleConfig.status) (not IMPLEMENTED)" -Module "Framework"
Write-Host " [SKIP] $moduleName - Not yet implemented" -ForegroundColor Yellow
continue
}
}
# Load and execute module based on name
switch ($moduleName) {
"SecurityBaseline" {
$manifestPath = Join-Path $modulePath "SecurityBaseline.psd1"
if (Test-Path $manifestPath) {
if (-not (Get-Module -Name $moduleName)) {
Import-Module $manifestPath -ErrorAction Stop
}
$moduleResult = Invoke-SecurityBaseline -DryRun:$DryRun
}
else {
throw "Module manifest not found: $manifestPath"
}
}
"ASR" {
$manifestPath = Join-Path $modulePath "ASR.psd1"
if (Test-Path $manifestPath) {
if (-not (Get-Module -Name $moduleName)) {
Import-Module $manifestPath -ErrorAction Stop
}
$moduleResult = Invoke-ASRRules -DryRun:$DryRun
}
else {
throw "Module manifest not found: $manifestPath"
}
}
"DNS" {
$manifestPath = Join-Path $modulePath "DNS.psd1"
if (Test-Path $manifestPath) {
if (-not (Get-Module -Name $moduleName)) {
Import-Module $manifestPath -ErrorAction Stop
}
# DNS module handles provider selection
# ONLY pass config values in NonInteractive mode (GUI)
# In interactive mode, let the module prompt the user!
if (Test-NonInteractiveMode) {
# GUI mode - use config values
$moduleResult = Invoke-DNSConfiguration -Provider $script:Config.modules.DNS.provider -DryRun:$DryRun
}
else {
# Interactive CLI mode - module will ask for provider and DoH mode
$moduleResult = Invoke-DNSConfiguration -DryRun:$DryRun
}
}
else {
throw "Module manifest not found: $manifestPath"
}
}
"Privacy" {
$manifestPath = Join-Path $modulePath "Privacy.psd1"
if (Test-Path $manifestPath) {
if (-not (Get-Module -Name $moduleName)) {
Import-Module $manifestPath -ErrorAction Stop
}
# Privacy module handles mode selection
# ONLY pass config values in NonInteractive mode (GUI)
if (Test-NonInteractiveMode) {
# GUI mode - use config values
$privacyArgs = @{ DryRun = $DryRun }
if ($script:Config.modules.Privacy.PSObject.Properties.Name -contains 'mode' -and $script:Config.modules.Privacy.mode) {
Write-Log -Level INFO -Message "Privacy mode: $($script:Config.modules.Privacy.mode)" -Module "Framework"
$privacyArgs["Mode"] = $script:Config.modules.Privacy.mode
}
if ($script:Config.modules.Privacy.PSObject.Properties.Name -contains 'removeBloatware') {
$rb = $script:Config.modules.Privacy.removeBloatware
if ($rb -is [string]) {
$rb = ($rb -eq "Y" -or $rb -eq "yes" -or $rb -eq "true" -or $rb -eq "1")
}
Write-Log -Level INFO -Message "Privacy removeBloatware: $rb" -Module "Framework"
$privacyArgs["RemoveBloatware"] = $rb
}
$moduleResult = Invoke-PrivacyHardening @privacyArgs
}
else {
# Interactive CLI mode - module will ask for mode selection
$moduleResult = Invoke-PrivacyHardening -DryRun:$DryRun
}
}
else {
throw "Module manifest not found: $manifestPath"
}
}
"AntiAI" {
$manifestPath = Join-Path $modulePath "AntiAI.psd1"
if (Test-Path $manifestPath) {
if (-not (Get-Module -Name $moduleName)) {
Import-Module $manifestPath -ErrorAction Stop
}
# AntiAI module applies maximum AI deactivation (no modes)
Write-Log -Level INFO -Message "Disabling all Windows 11 AI features (15 features, 32 policies)" -Module "Framework"
$moduleResult = Invoke-AntiAI -DryRun:$DryRun
}
else {
throw "Module manifest not found: $manifestPath"
}
}
"EdgeHardening" {
$manifestPath = Join-Path $modulePath "EdgeHardening.psd1"
if (Test-Path $manifestPath) {
if (-not (Get-Module -Name $moduleName)) {
Import-Module $manifestPath -ErrorAction Stop
}
# EdgeHardening applies Microsoft Edge security baseline
Write-Log -Level INFO -Message "Applying Microsoft Edge v139 Security Baseline (24 policies)" -Module "Framework"
$moduleResult = Invoke-EdgeHardening -DryRun:$DryRun
}
else {
throw "Module manifest not found: $manifestPath"
}
}
"AdvancedSecurity" {
$manifestPath = Join-Path $modulePath "AdvancedSecurity.psd1"
if (Test-Path $manifestPath) {
if (-not (Get-Module -Name $moduleName)) {
Import-Module $manifestPath -ErrorAction Stop
}
# AdvancedSecurity handles profile selection
# ONLY pass config values in NonInteractive mode (GUI)
if (Test-NonInteractiveMode) {
# GUI mode - use config values (securityProfile)
$secProfile = $script:Config.modules.AdvancedSecurity.securityProfile
if ($secProfile) {
Write-Log -Level INFO -Message "AdvancedSecurity profile: $secProfile" -Module "Framework"
$moduleResult = Invoke-AdvancedSecurity -SecurityProfile $secProfile -DryRun:$DryRun
}
else {
$moduleResult = Invoke-AdvancedSecurity -DryRun:$DryRun
}
}
else {
# Interactive CLI mode - module will ask for profile
$moduleResult = Invoke-AdvancedSecurity -DryRun:$DryRun
}
}
else {
throw "Module manifest not found: $manifestPath"
}
}
default {
$warnMsg = "Module '$moduleName' is not yet implemented"
Write-Log -Level WARNING -Message $warnMsg -Module "Framework"
$results.Warnings += $warnMsg
continue
}
}
# Store module result
if ($moduleResult) {
# If module returned an array, use the last element as the actual result
# (handles cases where helper functions inadvertently output to pipeline)
if ($moduleResult -is [array]) {
Write-Log -Level DEBUG -Message "Module '$moduleName' returned array ($($moduleResult.Count) items), using last element as result object" -Module "Framework"
$moduleResult = $moduleResult[-1]
}
$results.ModuleResults += $moduleResult
$results.ModulesExecuted++
# Handle different return types: Boolean or PSCustomObject
$success = $false
if ($moduleResult -is [bool]) {
# Module returned simple boolean (e.g., Privacy module)
$success = $moduleResult
}
elseif ($moduleResult -is [PSCustomObject]) {
# Module returned object with Success property (e.g., ASR, DNS modules)
$hasSuccess = $null -ne ($moduleResult.PSObject.Properties | Where-Object { $_.Name -eq 'Success' })
$success = if ($hasSuccess) { $moduleResult.Success } else { $false }
}
else {
# Unknown type - assume failure
Write-Log -Level WARNING -Message "Module '$moduleName' returned unexpected type: $($moduleResult.GetType().Name)" -Module "Framework"
}
if ($success) {
Write-Log -Level SUCCESS -Message "Module '$moduleName' completed successfully" -Module "Framework"
}
else {
Write-Log -Level WARNING -Message "Module '$moduleName' completed with errors" -Module "Framework"
# Only add errors if moduleResult is an object with Errors property
if ($moduleResult -is [PSCustomObject]) {
$hasErrors = $null -ne ($moduleResult.PSObject.Properties | Where-Object { $_.Name -eq 'Errors' })
if ($hasErrors -and $moduleResult.Errors.Count -gt 0) {
$results.Errors += $moduleResult.Errors
}
}
}
# Always collect warnings from modules (regardless of success)
# Warnings are informational (e.g., "rule set to AUDIT mode") - not errors
if ($moduleResult -is [PSCustomObject]) {
$hasWarnings = $null -ne ($moduleResult.PSObject.Properties | Where-Object { $_.Name -eq 'Warnings' })
if ($hasWarnings -and $moduleResult.Warnings.Count -gt 0) {
$results.Warnings += $moduleResult.Warnings
}
}
}
}
catch {
Write-ErrorLog -Message "Failed to execute module '$moduleName'" -Module "Framework" -ErrorRecord $_
$errMsg = "Module '$moduleName' execution failed: $($_.Exception.Message)"
$results.Errors += $errMsg
$results.Success = $false
}
}
# Calculate duration
$results.Duration = (Get-Date) - $startTime
# Final success status
if ($results.Errors.Count -gt 0) {
$results.Success = $false
}
# Update session display name for backup identification (after all modules complete)
if (-not $DryRun) {
try {
Update-SessionDisplayName
}
catch {
Write-Log -Level WARNING -Message "Failed to update session display name: $_" -Module "Framework"
}
}
Write-Log -Level INFO -Message "Hardening execution completed - Modules: $($results.ModulesExecuted), Errors: $($results.Errors.Count), Warnings: $($results.Warnings.Count)" -Module "Framework"
return $results
}
# Note: Export-ModuleMember not used - this script is dot-sourced, not imported as module
# Main execution (only if script is run directly, not dot-sourced)
# This allows Framework.ps1 to be used standalone OR dot-sourced by NoIDPrivacy.ps1
if ($MyInvocation.InvocationName -ne '.' -and $MyInvocation.Line -notmatch '^\s*\.\s+') {
try {
Initialize-Framework
$prereqsPassed = Test-FrameworkPrerequisites
if ($prereqsPassed) {
Start-HardeningProcess
}
else {
Write-Host "Execution aborted due to failed prerequisites." -ForegroundColor Red
exit 1
}
}
catch {
Write-Host ""
Write-Host "CRITICAL ERROR" -ForegroundColor Red
Write-Host "An unexpected error occurred:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
Write-Host "Stack trace:" -ForegroundColor Gray
Write-Host $_.ScriptStackTrace -ForegroundColor Gray
if ($null -ne (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
Write-Log -Level ERROR -Message "Critical framework error" -Module "Framework" -Exception $_.Exception
}
exit 1
}
}