#Requires -Version 5.1 <# .SYNOPSIS Network Security Audit Checklist v4.0 - Professional GUI Tool .DESCRIPTION Comprehensive WPF-based security audit checklist for SMB environments. Features: auto system theme detection, 8 themes, categorized checks, context hints for junior auditors, compliance mapping (NIST/CIS/HIPAA), per-item findings/notes/evidence/remediation tracking, severity ratings, weighted risk scoring, auto-discovery scripts, executive summary, HTML/PDF export, save/load, audit diff comparison, scan profiles, headless/silent mode for RMM deployment, three-tier reporting, and risk-tier safety classification. .PARAMETER Silent Run in headless mode (no GUI). Auto-scans, exports, and exits. Designed for RMM deployment via ConnectWise, Datto, NinjaRMM, etc. .PARAMETER ScanProfile Scan profile to use: Quick, Standard, Full, ADOnly, LocalOnly, HIPAA, PCI, CMMC, SOC2, ISO27001. Default: Full (all 67 checks). Quick runs ~20 critical checks. Framework profiles run checks mapped to that compliance framework. .PARAMETER OutputPath Path for report output. Default: Desktop\SecurityAudit__.html .PARAMETER ReportTier Report detail level: Executive, Management, Technical, All. Default: All (generates all three tiers in one report). .PARAMETER ReadOnly Safety mode: skip any checks that could modify system state. Default: $true. Set -ReadOnly:$false to allow WinRM/audit policy setup. .PARAMETER Client Client name for the report header. Default: domain or computer name. .PARAMETER Auditor Auditor name for the report header. Default: current username. .EXAMPLE .\NetworkSecurityAudit_v3.ps1 # Normal GUI mode .EXAMPLE .\NetworkSecurityAudit_v3.ps1 -Silent -ScanProfile Standard -OutputPath "C:\Reports\audit.html" # Headless mode for RMM: scan, export, exit .EXAMPLE .\NetworkSecurityAudit_v3.ps1 -Silent -ScanProfile Quick -ReportTier Executive # Quick assessment with executive summary only .AUTHOR SysAdminDoc .VERSION 4.0.0 #> param( [switch]$Silent, [ValidateSet('Quick','Standard','Full','ADOnly','LocalOnly','HIPAA','PCI','CMMC','SOC2','ISO27001')] [string]$ScanProfile = 'Full', [string]$OutputPath = '', [ValidateSet('Executive','Management','Technical','All')] [string]$ReportTier = 'All', [bool]$ReadOnly = $true, [string]$Client = '', [string]$Auditor = '', [switch]$ExportJSON, [switch]$ExportCSV, [switch]$ExportJSONL ) # ── Auto-Elevate to Administrator ──────────────────────────────────────────── $script:IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $script:IsAdmin) { try { $argList = @('-NoProfile','-ExecutionPolicy','Bypass','-File',"`"$PSCommandPath`"") # Pass through all CLI parameters on re-launch if ($Silent) { $argList += '-Silent' } if ($ScanProfile -ne 'Full') { $argList += '-ScanProfile'; $argList += $ScanProfile } if ($OutputPath) { $argList += '-OutputPath'; $argList += "`"$OutputPath`"" } if ($ReportTier -ne 'All') { $argList += '-ReportTier'; $argList += $ReportTier } if (-not $ReadOnly) { $argList += '-ReadOnly:$false' } if ($Client) { $argList += '-Client'; $argList += "`"$Client`"" } if ($Auditor) { $argList += '-Auditor'; $argList += "`"$Auditor`"" } if ($ExportJSON) { $argList += '-ExportJSON' } if ($ExportCSV) { $argList += '-ExportCSV' } if ($ExportJSONL) { $argList += '-ExportJSONL' } Start-Process -FilePath 'powershell.exe' -ArgumentList $argList -Verb RunAs -WindowStyle Hidden exit } catch { # User declined UAC or elevation failed - continue without admin } } # ── Store CLI config in script scope ───────────────────────────────────────── $script:SilentMode = $Silent.IsPresent $script:CliProfile = $ScanProfile $script:CliOutput = $OutputPath $script:CliReport = $ReportTier $script:ReadOnlyMode = $ReadOnly $script:CliClient = $Client $script:CliAuditor = $Auditor $script:CliExportJSON = $ExportJSON.IsPresent $script:CliExportCSV = $ExportCSV.IsPresent $script:CliExportJSONL = $ExportJSONL.IsPresent Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase # ── DPI Awareness + Console Hide ───────────────────────────────────────────── try { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public class DpiHelper { [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [DllImport("shcore.dll")] public static extern int SetProcessDpiAwareness(int awareness); } public class ConsoleHelper { [DllImport("kernel32.dll")] static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); public static void Hide() { IntPtr h = GetConsoleWindow(); if (h != IntPtr.Zero) ShowWindow(h, 0); } public static void Show() { IntPtr h = GetConsoleWindow(); if (h != IntPtr.Zero) ShowWindow(h, 5); } } '@ -ErrorAction SilentlyContinue try { [DpiHelper]::SetProcessDpiAwareness(2) } catch { [DpiHelper]::SetProcessDPIAware() } [ConsoleHelper]::Hide() } catch { } # ── Environment Detection ──────────────────────────────────────────────────── $script:Env = @{ ComputerName = $env:COMPUTERNAME IsAdmin = $script:IsAdmin PSVersion = $PSVersionTable.PSVersion.ToString() OSCaption = '' IsServer = $false IsDomainJoined = $false DomainName = '' HasAD = $false HasDNS = $false HasGPO = $false HasDefender = $false HasSMB = $false HasBitLocker = $false HasAppLocker = $false WinRMRunning = $false MissingModules = [System.Collections.ArrayList]@() InstalledModules = [System.Collections.ArrayList]@() } try { $os = Get-CimInstance Win32_OperatingSystem -EA SilentlyContinue if ($os) { $script:Env.OSCaption = $os.Caption $script:Env.IsServer = $os.Caption -match 'Server' } } catch {} try { $cs = Get-CimInstance Win32_ComputerSystem -EA SilentlyContinue if ($cs) { $script:Env.IsDomainJoined = $cs.PartOfDomain if ($cs.PartOfDomain) { $script:Env.DomainName = $cs.Domain } } } catch {} # Check available modules and capabilities $moduleChecks = @( @{ Name='ActiveDirectory'; EnvKey='HasAD'; RSATName='Rsat.ActiveDirectory.DS-LDS.Tools'; Feature='RSAT-AD-PowerShell' } @{ Name='DnsServer'; EnvKey='HasDNS'; RSATName='Rsat.Dns.Tools'; Feature='RSAT-DNS-Server' } @{ Name='GroupPolicy'; EnvKey='HasGPO'; RSATName='Rsat.GroupPolicy.Management.Tools'; Feature='GPMC' } ) foreach ($mc in $moduleChecks) { if (Get-Module $mc.Name -ListAvailable -EA SilentlyContinue) { $script:Env[$mc.EnvKey] = $true $script:Env.InstalledModules.Add($mc.Name) | Out-Null } else { $script:Env.MissingModules.Add($mc) | Out-Null } } # Check features/services try { if (Get-Command Get-MpComputerStatus -EA SilentlyContinue) { $script:Env.HasDefender = $true } } catch {} try { if (Get-Command Get-SmbServerConfiguration -EA SilentlyContinue) { $script:Env.HasSMB = $true } } catch {} try { if (Get-Command Get-BitLockerVolume -EA SilentlyContinue) { $script:Env.HasBitLocker = $true } } catch {} try { if (Get-Command Get-AppLockerPolicy -EA SilentlyContinue) { $script:Env.HasAppLocker = $true } } catch {} # WinRM service status try { $winrm = Get-Service WinRM -EA SilentlyContinue $script:Env.WinRMRunning = ($winrm -and $winrm.Status -eq 'Running') } catch {} # Additional capabilities for turnkey auto-checks try { if (Get-Command Get-Tpm -EA SilentlyContinue) { $script:Env['HasTPM'] = $true } } catch {} try { if (Get-Command Get-VpnConnection -EA SilentlyContinue) { $script:Env['HasVPN'] = $true } } catch {} try { if (Get-Command Get-NetFirewallProfile -EA SilentlyContinue) { $script:Env['HasFW'] = $true } } catch {} try { if (Get-Command Get-LocalGroupMember -EA SilentlyContinue) { $script:Env['HasLocalUser'] = $true } } catch {} try { if (Get-Command Get-DnsServerDiagnostics -EA SilentlyContinue) { $script:Env['HasDNSServer'] = $true } } catch {} try { if (Get-Command Confirm-SecureBootUEFI -EA SilentlyContinue) { $script:Env['HasSecureBoot'] = $true } } catch {} # ── Azure AD / Entra / Hybrid Join Detection ───────────────────────────────── $script:Env['JoinType'] = 'Workgroup' $script:Env['AzureADJoined'] = $false $script:Env['IntuneManaged'] = $false $script:Env['TenantName'] = '' try { $dsreg = dsregcmd /status 2>&1 | Out-String $azJoined = $dsreg -match 'AzureAdJoined\s*:\s*YES' $domJoined = $dsreg -match 'DomainJoined\s*:\s*YES' $wpJoined = $dsreg -match 'WorkplaceJoined\s*:\s*YES' if ($azJoined -and $domJoined) { $script:Env['JoinType'] = 'Hybrid Azure AD' } elseif ($azJoined) { $script:Env['JoinType'] = 'Azure AD Joined' } elseif ($domJoined) { $script:Env['JoinType'] = 'Domain Joined' } elseif ($wpJoined) { $script:Env['JoinType'] = 'Workplace Joined' } $script:Env['AzureADJoined'] = $azJoined if ($dsreg -match 'TenantName\s*:\s*(.+)') { $script:Env['TenantName'] = $Matches[1].Trim() } } catch {} # Intune / MDM enrollment detection try { $enrollKeys = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue foreach ($ek in $enrollKeys) { $prov = (Get-ItemProperty $ek.PSPath -EA SilentlyContinue).ProviderID if ($prov -eq 'MS DM Server') { $script:Env['IntuneManaged'] = $true; break } } } catch {} # ── OS Build / Version for Feature Gating ───────────────────────────────────── try { $ntCur = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -EA SilentlyContinue $script:Env['OSBuild'] = [int]($ntCur.CurrentBuildNumber) $script:Env['OSVersion'] = $ntCur.DisplayVersion # e.g. 22H2, 23H2, 24H2 } catch { $script:Env['OSBuild'] = 0; $script:Env['OSVersion'] = '' } # ── LAPS Module Detection ───────────────────────────────────────────────────── $script:Env['HasWindowsLAPS'] = $false $script:Env['HasLegacyLAPS'] = $false try { if (Get-Command Get-LapsADPassword -EA SilentlyContinue) { $script:Env['HasWindowsLAPS'] = $true } } catch {} try { $lapsGPO = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\LAPS' -EA SilentlyContinue $lapsCSE = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions\{D76B9641-3288-4f75-942D-087DE603E3EA}' -EA SilentlyContinue if ($lapsGPO -or $lapsCSE) { $script:Env['HasLegacyLAPS'] = $true } } catch {} # ── Module Installer Functions ─────────────────────────────────────────────── function Install-AuditPrereqs { [CmdletBinding()]param() $results = [System.Collections.ArrayList]@() foreach ($mod in $script:Env.MissingModules) { $installed = $false $msg = '' if ($script:Env.IsServer) { # Server: Use Install-WindowsFeature try { $feat = Install-WindowsFeature -Name $mod.Feature -EA Stop if ($feat.Success) { $installed = $true; $msg = "Installed server feature: $($mod.Feature)" } else { $msg = "Failed to install $($mod.Feature): Feature install returned failure" } } catch { $msg = "Failed to install $($mod.Feature): $($_.Exception.Message)" } } else { # Workstation: Use Add-WindowsCapability (Win10/11) try { $cap = Get-WindowsCapability -Online -Name "$($mod.RSATName)~~~~*" -EA Stop | Where-Object { $_.State -ne 'Installed' } if ($cap) { foreach ($c in $cap) { Add-WindowsCapability -Online -Name $c.Name -EA Stop | Out-Null } $installed = $true; $msg = "Installed RSAT capability: $($mod.RSATName)" } else { $installed = $true; $msg = "Already installed: $($mod.RSATName)" } } catch { $msg = "Failed to install $($mod.RSATName): $($_.Exception.Message)" } } $results.Add(@{ Module=$mod.Name; Installed=$installed; Message=$msg }) | Out-Null } # Ensure NuGet provider try { $nuget = Get-PackageProvider -Name NuGet -ListAvailable -EA SilentlyContinue if (-not $nuget -or $nuget.Version -lt [Version]'2.8.5.201') { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -EA Stop | Out-Null $results.Add(@{ Module='NuGet'; Installed=$true; Message='NuGet provider installed' }) | Out-Null } } catch { $results.Add(@{ Module='NuGet'; Installed=$false; Message="NuGet: $($_.Exception.Message)" }) | Out-Null } return $results } function Enable-AuditWinRM { [CmdletBinding()]param([string]$Target, [PSCredential]$Credential) $isLocal = ($Target -eq 'localhost' -or $Target -eq '127.0.0.1' -or $Target -eq $env:COMPUTERNAME) if ($isLocal) { try { if (-not $script:Env.WinRMRunning) { Enable-PSRemoting -Force -SkipNetworkProfileCheck -EA Stop | Out-Null Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force -EA SilentlyContinue $script:Env.WinRMRunning = $true return @{ Success=$true; Message='WinRM enabled on local machine' } } return @{ Success=$true; Message='WinRM already running locally' } } catch { return @{ Success=$false; Message="WinRM local config failed: $($_.Exception.Message)" } } } else { # Remote WinRM setup - attempt via PsExec-style or WMI try { $params = @{ ComputerName=$Target; ErrorAction='Stop' } if ($Credential) { $params.Credential = $Credential } # First check if already reachable $test = Test-WSMan @params -EA SilentlyContinue if ($test) { return @{ Success=$true; Message="WinRM already configured on $Target" } } # Try enabling via WMI (works if WMI is accessible) $wmiParams = @{ ComputerName=$Target; Class='Win32_Process'; Name='Create'; ErrorAction='Stop' } if ($Credential) { $wmiParams.Credential = $Credential } $wmiParams.ArgumentList = @('powershell.exe -Command "Enable-PSRemoting -Force -SkipNetworkProfileCheck"') Invoke-WmiMethod @wmiParams | Out-Null # Wait and verify Start-Sleep -Seconds 5 $verify = Test-WSMan @params -EA SilentlyContinue if ($verify) { return @{ Success=$true; Message="WinRM enabled on $Target via WMI" } } else { return @{ Success=$false; Message="WinRM command sent to $Target but verification failed - may need reboot" } } } catch { return @{ Success=$false; Message="Remote WinRM setup failed on $Target`: $($_.Exception.Message)" } } } } # ── Additional Turnkey Functions ───────────────────────────────────────────── function Find-DomainControllers { [CmdletBinding()]param() $dcs = [System.Collections.ArrayList]@() $primary = $null # Method 1: DNS SRV records (fastest, works without AD module) try { $domain = if ($script:Env.IsDomainJoined) { $script:Env.DomainName } else { $null } if ($domain) { $srv = Resolve-DnsName "_ldap._tcp.dc._msdcs.$domain" -Type SRV -EA Stop foreach ($r in ($srv | Where-Object { $_.Type -eq 'SRV' } | Sort-Object Priority, Weight)) { $name = $r.NameTarget -replace '\.$','' if ($name -and $dcs -notcontains $name) { $dcs.Add($name) | Out-Null } } } } catch { } # Method 2: nltest (works without AD module, uses cached info) if ($dcs.Count -eq 0 -and $script:Env.IsDomainJoined) { try { $nl = nltest /dsgetdc:$($script:Env.DomainName) 2>&1 $dcLine = ($nl | Select-String 'DC: \\\\(.+)' | Select-Object -First 1) if ($dcLine -and $dcLine.Matches) { $name = $dcLine.Matches[0].Groups[1].Value.Trim() if ($name -and $dcs -notcontains $name) { $dcs.Add($name) | Out-Null } } } catch { } } # Method 3: [System.DirectoryServices] (backup) if ($dcs.Count -eq 0 -and $script:Env.IsDomainJoined) { try { $ctx = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new('Domain', $script:Env.DomainName) $dom = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($ctx) foreach ($dc in $dom.DomainControllers) { $name = $dc.Name if ($name -and $dcs -notcontains $name) { $dcs.Add($name) | Out-Null } } } catch { } } # Determine primary DC (PDC emulator if possible) if ($dcs.Count -gt 0) { try { $nl2 = nltest /dsgetdc:$($script:Env.DomainName) /pdc 2>&1 $pdcLine = ($nl2 | Select-String 'DC: \\\\(.+)' | Select-Object -First 1) if ($pdcLine -and $pdcLine.Matches) { $primary = $pdcLine.Matches[0].Groups[1].Value.Trim() } } catch { } if (-not $primary) { $primary = $dcs[0] } } return @{ DCs=$dcs; Primary=$primary; Count=$dcs.Count } } function Enable-RemoteRegistry { [CmdletBinding()]param([string]$Target, [PSCredential]$Credential) $isLocal = ($Target -eq 'localhost' -or $Target -eq '127.0.0.1' -or $Target -eq $env:COMPUTERNAME) try { if ($isLocal) { $svc = Get-Service RemoteRegistry -EA Stop if ($svc.Status -ne 'Running') { Set-Service RemoteRegistry -StartupType Manual -EA Stop Start-Service RemoteRegistry -EA Stop return @{ Success=$true; Message='Remote Registry started locally' } } return @{ Success=$true; Message='Remote Registry already running' } } else { $params = @{ ComputerName=$Target; ErrorAction='Stop' } if ($Credential) { $params.Credential = $Credential } Invoke-Command @params -ScriptBlock { Set-Service RemoteRegistry -StartupType Manual -EA Stop Start-Service RemoteRegistry -EA Stop } return @{ Success=$true; Message="Remote Registry started on $Target" } } } catch { return @{ Success=$false; Message="Remote Registry: $($_.Exception.Message)" } } } function Open-WinRMFirewallRules { [CmdletBinding()]param() try { $rules = Get-NetFirewallRule -DisplayGroup 'Windows Remote Management' -EA SilentlyContinue $disabled = $rules | Where-Object { $_.Enabled -ne 'True' } if ($disabled) { Enable-NetFirewallRule -DisplayGroup 'Windows Remote Management' -EA Stop return @{ Success=$true; Message="Enabled $($disabled.Count) WinRM firewall rules" } } return @{ Success=$true; Message='WinRM firewall rules already enabled' } } catch { return @{ Success=$false; Message="Firewall: $($_.Exception.Message)" } } } function Set-PSGalleryTrust { [CmdletBinding()]param() try { $repo = Get-PSRepository -Name PSGallery -EA SilentlyContinue if ($repo -and $repo.InstallationPolicy -ne 'Trusted') { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -EA Stop return @{ Success=$true; Message='PSGallery set to Trusted' } } return @{ Success=$true; Message='PSGallery already trusted' } } catch { return @{ Success=$false; Message="PSGallery: $($_.Exception.Message)" } } } function Enable-RemoteWMI { [CmdletBinding()]param() try { $rules = Get-NetFirewallRule -DisplayGroup 'Windows Management Instrumentation (WMI)' -EA SilentlyContinue $disabled = $rules | Where-Object { $_.Enabled -ne 'True' } if ($disabled) { Enable-NetFirewallRule -DisplayGroup 'Windows Management Instrumentation (WMI)' -EA Stop return @{ Success=$true; Message="Enabled $($disabled.Count) WMI firewall rules" } } return @{ Success=$true; Message='WMI firewall rules already enabled' } } catch { return @{ Success=$false; Message="WMI Firewall: $($_.Exception.Message)" } } } function Enable-EventLogRemote { [CmdletBinding()]param() try { $rules = Get-NetFirewallRule -DisplayGroup 'Remote Event Log Management' -EA SilentlyContinue $disabled = $rules | Where-Object { $_.Enabled -ne 'True' } if ($disabled) { Enable-NetFirewallRule -DisplayGroup 'Remote Event Log Management' -EA Stop return @{ Success=$true; Message="Enabled $($disabled.Count) Event Log firewall rules" } } return @{ Success=$true; Message='Event Log firewall rules already enabled' } } catch { return @{ Success=$false; Message="Event Log Firewall: $($_.Exception.Message)" } } } # ── Turnkey Configuration State ────────────────────────────────────────────── $script:TurnkeyAutoExport = $true $script:TurnkeyAutoScan = $false # set to $true after user confirms $script:TurnkeyLaunched = $false $script:FullAuditMode = $false # one-click: preflight + scan + export (no prompts) $script:TurnkeyPS = $null $script:TurnkeyAsync = $null $script:TurnkeyStatus = [hashtable]::Synchronized(@{ Status = '' Phase = '' Log = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new()) Done = $false }) $script:DiscoveredDCs = @() # ── Theme Definitions ──────────────────────────────────────────────────────── $script:Themes = @{ 'Midnight' = @{ WindowBg='#1a1a2e';PanelBg='#16213e';CardBg='#16213e';SurfaceBg='#0f3460' InputBg='#1e293b';BorderDim='#334155';TextPrimary='#e2e8f0';TextSecondary='#94a3b8' Accent='#0ea5e9';AccentHover='#38bdf8';AccentPress='#0284c7';BarBg='#1e293b' ProgressGood='#22c55e';ProgressMid='#eab308';ThumbBg='#475569' HoverBg='#0f3460';SelectedBg='#0ea5e9';CheckedBorder='#22c55e';CheckedBg='#1a2e1a' HeaderGrad1='#0f3460';HeaderGrad2='#16213e';HintBg='#0c2d4a';HintBorder='#1e4976' } 'Slate' = @{ WindowBg='#0f172a';PanelBg='#1e293b';CardBg='#1e293b';SurfaceBg='#334155' InputBg='#0f172a';BorderDim='#475569';TextPrimary='#f1f5f9';TextSecondary='#94a3b8' Accent='#6366f1';AccentHover='#818cf8';AccentPress='#4f46e5';BarBg='#0f172a' ProgressGood='#22c55e';ProgressMid='#eab308';ThumbBg='#64748b' HoverBg='#334155';SelectedBg='#6366f1';CheckedBorder='#22c55e';CheckedBg='#1a2e1a' HeaderGrad1='#1e293b';HeaderGrad2='#0f172a';HintBg='#1e1b4b';HintBorder='#3730a3' } 'Nord' = @{ WindowBg='#2e3440';PanelBg='#3b4252';CardBg='#3b4252';SurfaceBg='#434c5e' InputBg='#2e3440';BorderDim='#4c566a';TextPrimary='#eceff4';TextSecondary='#d8dee9' Accent='#88c0d0';AccentHover='#8fbcbb';AccentPress='#5e81ac';BarBg='#2e3440' ProgressGood='#a3be8c';ProgressMid='#ebcb8b';ThumbBg='#4c566a' HoverBg='#434c5e';SelectedBg='#5e81ac';CheckedBorder='#a3be8c';CheckedBg='#2e3b2e' HeaderGrad1='#434c5e';HeaderGrad2='#3b4252';HintBg='#2e3440';HintBorder='#4c566a' } 'Dracula' = @{ WindowBg='#282a36';PanelBg='#44475a';CardBg='#44475a';SurfaceBg='#6272a4' InputBg='#282a36';BorderDim='#6272a4';TextPrimary='#f8f8f2';TextSecondary='#bd93f9' Accent='#ff79c6';AccentHover='#ff92d0';AccentPress='#ff55b8';BarBg='#282a36' ProgressGood='#50fa7b';ProgressMid='#f1fa8c';ThumbBg='#6272a4' HoverBg='#6272a4';SelectedBg='#ff79c6';CheckedBorder='#50fa7b';CheckedBg='#2a3a2a' HeaderGrad1='#44475a';HeaderGrad2='#282a36';HintBg='#1e1f29';HintBorder='#44475a' } 'Monokai' = @{ WindowBg='#272822';PanelBg='#3e3d32';CardBg='#3e3d32';SurfaceBg='#49483e' InputBg='#272822';BorderDim='#75715e';TextPrimary='#f8f8f2';TextSecondary='#a6a68a' Accent='#a6e22e';AccentHover='#b8f340';AccentPress='#8cc41a';BarBg='#272822' ProgressGood='#a6e22e';ProgressMid='#e6db74';ThumbBg='#75715e' HoverBg='#49483e';SelectedBg='#a6e22e';CheckedBorder='#a6e22e';CheckedBg='#2e3a22' HeaderGrad1='#3e3d32';HeaderGrad2='#272822';HintBg='#272822';HintBorder='#49483e' } 'Light' = @{ WindowBg='#f8fafc';PanelBg='#ffffff';CardBg='#ffffff';SurfaceBg='#e2e8f0' InputBg='#f1f5f9';BorderDim='#cbd5e1';TextPrimary='#1e293b';TextSecondary='#64748b' Accent='#2563eb';AccentHover='#3b82f6';AccentPress='#1d4ed8';BarBg='#e2e8f0' ProgressGood='#16a34a';ProgressMid='#ca8a04';ThumbBg='#94a3b8' HoverBg='#e2e8f0';SelectedBg='#2563eb';CheckedBorder='#16a34a';CheckedBg='#dcfce7' HeaderGrad1='#e2e8f0';HeaderGrad2='#f1f5f9';HintBg='#eff6ff';HintBorder='#bfdbfe' } 'Solarized Dark' = @{ WindowBg='#002b36';PanelBg='#073642';CardBg='#073642';SurfaceBg='#586e75' InputBg='#002b36';BorderDim='#586e75';TextPrimary='#fdf6e3';TextSecondary='#93a1a1' Accent='#268bd2';AccentHover='#2aa0f0';AccentPress='#1a6fb5';BarBg='#002b36' ProgressGood='#859900';ProgressMid='#b58900';ThumbBg='#586e75' HoverBg='#586e75';SelectedBg='#268bd2';CheckedBorder='#859900';CheckedBg='#0a3a1a' HeaderGrad1='#073642';HeaderGrad2='#002b36';HintBg='#002b36';HintBorder='#586e75' } 'Catppuccin Mocha' = @{ WindowBg='#1e1e2e';PanelBg='#313244';CardBg='#313244';SurfaceBg='#45475a' InputBg='#1e1e2e';BorderDim='#585b70';TextPrimary='#cdd6f4';TextSecondary='#a6adc8' Accent='#cba6f7';AccentHover='#d4b8fa';AccentPress='#b48bf0';BarBg='#1e1e2e' ProgressGood='#a6e3a1';ProgressMid='#f9e2af';ThumbBg='#585b70' HoverBg='#45475a';SelectedBg='#cba6f7';CheckedBorder='#a6e3a1';CheckedBg='#2a3a2e' HeaderGrad1='#313244';HeaderGrad2='#1e1e2e';HintBg='#1e1e2e';HintBorder='#45475a' } } # ── System Theme Detection ─────────────────────────────────────────────────── function Get-SystemTheme { try { $v = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -Name 'AppsUseLightTheme' -EA Stop).AppsUseLightTheme if ($v -eq 0) { 'Dark' } else { 'Light' } } catch { 'Dark' } } $script:CurrentThemeName = if ((Get-SystemTheme) -eq 'Light') { 'Light' } else { 'Midnight' } function Get-T { $script:Themes[$script:CurrentThemeName] } function New-Brush([string]$hex) { New-Object System.Windows.Media.SolidColorBrush ([System.Windows.Media.ColorConverter]::ConvertFromString($hex)) } # ── Severity Colors ────────────────────────────────────────────────────────── $script:SeverityColors = @{ Critical='#ef4444'; High='#f97316'; Medium='#eab308'; Low='#22c55e' } $script:CategoryAccents = @{ 'Network Perimeter'='#0ea5e9'; 'Identity & Access'='#a855f7'; 'Endpoint Security'='#22c55e' 'Backup & Recovery'='#eab308'; 'Logging & Monitoring'='#f97316'; 'Network Architecture'='#06b6d4' 'Physical Security'='#ec4899'; 'Common Findings'='#ef4444' } # ── Audit Data with Hints + Compliance Mapping ─────────────────────────────── $script:AuditCategories = [ordered]@{ 'Network Perimeter' = @{ Desc = 'Where most breaches start - external attack surface assessment' Items = @( @{ ID='NP01'; Severity='Critical'; Weight=10 Text='Firewall rules review - check for any/any rules, unused rules, rules older than 2 years' Hint='Log into the firewall admin console (pfSense: Firewall > Rules, FortiGate: Policy & Objects > Firewall Policy, SonicWall: Policies > Rules and Policies). Export the full rule list. Search for rules where Source=Any AND Destination=Any AND Service=Any - these are effectively open doors. Flag any rule with no hit count in 90+ days and any rule with a creation date older than 2 years. Document the rule ID, description, creator if available, and last hit date. Ask the client: "Who requested this rule and is it still needed?"' Compliance='NIST CSF PR.AC-5, PR.PT-4 | CIS Control 4.4, 4.5, 9.2 | HIPAA 164.312(e)(1)' } @{ ID='NP02'; Severity='Critical'; Weight=10 Text='Open ports audit - every open port must have documented business justification' Hint='Run an external port scan using nmap from OUTSIDE the network: "nmap -sS -sV -p- " or use an online scanner like Shodan, Censys, or SecurityTrails. Compare results against a documented port justification list. Common dangerous findings: RDP (3389), SMB (445), Telnet (23), FTP (21), or database ports (1433, 3306, 5432) open to the internet. Every open port needs an owner and business reason documented. If they cannot justify it, it gets closed.' Compliance='NIST CSF PR.AC-5, DE.CM-7 | CIS Control 4.1, 4.4, 9.2 | HIPAA 164.312(e)(1)' } @{ ID='NP03'; Severity='High'; Weight=7 Text='VPN configuration - verify split tunneling policy and MFA requirement' Hint='Check the VPN concentrator/server config. For split tunneling: look at the client routing table when connected - if only specific subnets route through VPN, split tunneling is ON. This means internet traffic bypasses corporate security controls. Ask if this is intentional and documented. For MFA: verify the VPN authenticates against RADIUS/LDAP with a second factor (Duo, Azure MFA, etc.), not just username/password. Test by attempting a VPN login - you should be prompted for MFA. Check: FortiGate VPN settings, GlobalProtect portal config, or OpenVPN server.conf.' Compliance='NIST CSF PR.AC-3, PR.AC-7 | CIS Control 6.3, 6.4 | HIPAA 164.312(d), 164.312(e)(1)' } @{ ID='NP04'; Severity='High'; Weight=7 Text='DNS filtering enabled and configured (e.g., Umbrella, NextDNS, pfBlockerNG)' Hint='Check what DNS servers the DHCP scope hands out to clients. Run "nslookup" from a workstation and note the DNS server. If it is the ISP default (like 8.8.8.8 with no filtering layer), there is no DNS filtering. Solutions to look for: Cisco Umbrella, Cloudflare Gateway, NextDNS, pfBlockerNG (on pfSense), Pi-hole, or firewall-based DNS filtering. Verify that the filter blocks malware, phishing, and C2 domains at minimum. Test by visiting a known test block page (e.g., examplemalwaredomain.com from Umbrella). Also verify DNS cannot be bypassed - check if outbound port 53 is forced through the filter.' Compliance='NIST CSF PR.DS-5, DE.CM-1 | CIS Control 9.2, 9.3 | HIPAA 164.312(e)(1)' } @{ ID='NP05'; Severity='High'; Weight=8 Text='Egress filtering configured - not just ingress rules' Hint='This is the #1 thing people miss. Most firewalls only filter INBOUND traffic and allow ALL outbound. Log into the firewall and look at outbound/egress rules. If you see a default "Allow All" outbound rule with no restrictions, flag it. Proper egress filtering blocks: unusual outbound ports, direct IP connections (bypassing DNS), known bad destinations, and limits which internal hosts can reach the internet. At minimum, outbound should restrict ports to 80, 443, and business-required services. Check for outbound rules on: pfSense (LAN rules), FortiGate (outbound policies), SonicWall (LAN to WAN rules).' Compliance='NIST CSF PR.AC-5, PR.DS-5, DE.CM-1 | CIS Control 4.4, 4.5, 9.3 | HIPAA 164.312(e)(1)' } @{ ID='NP06'; Severity='Medium'; Weight=5 Text='Temporary firewall rules audit - identify and remove stale rules' Hint='Search the firewall rule list for anything with "temp", "test", "old", or a person name in the description. Also look at rule comments and creation dates. Temporary rules created for vendor access, troubleshooting, or one-time projects are the #1 source of forgotten attack surface. Ask: "Was there a specific ticket or change request for this rule? Does it have an expiration date?" Common finding: a rule from 3-5 years ago created to let a vendor in "just for today" that is still active. Document rule name, date created, last hit count, and whether anyone can justify it.' Compliance='NIST CSF PR.AC-5 | CIS Control 4.5 | HIPAA 164.312(e)(1)' } @{ ID='NP07'; Severity='Medium'; Weight=5 Text='IDS/IPS signatures are current and actively monitored' Hint='If the firewall has IDS/IPS (Snort, Suricata, FortiGate IPS, SonicWall GAV/IPS), check: 1) Is it actually ENABLED (not just licensed)? 2) When were signatures last updated? (should be daily or at least weekly) 3) Is it in DETECT mode or PREVENT/BLOCK mode? Detect-only means it logs but does not stop attacks. 4) Who reviews the alerts? If nobody looks at the logs, IDS/IPS is useless. Check the IPS dashboard for last update timestamp and top triggered rules. On pfSense: Services > Snort/Suricata. On FortiGate: Security Profiles > Intrusion Prevention.' Compliance='NIST CSF DE.CM-1, DE.DP-2 | CIS Control 13.3, 13.6 | HIPAA 164.312(e)(1)' } @{ ID='NP08'; Severity='Low'; Weight=3 Text='SSL/TLS inspection for outbound traffic' Hint='SSL/TLS inspection (also called HTTPS inspection, SSL decryption, or deep packet inspection) lets the firewall see inside encrypted traffic to detect malware and data exfiltration. Check if the firewall is configured to decrypt and inspect HTTPS traffic. This requires a CA certificate deployed to endpoints. On FortiGate: Security Profiles > SSL/SSH Inspection. On pfSense: this requires Squid proxy with SSL bump. Important: this has privacy implications - document any exclusions (banking, healthcare, personal sites) and ensure there is a policy that employees are aware of inspection. Many SMBs will not have this - note it as a recommendation rather than a critical finding.' Compliance='NIST CSF DE.CM-1 | CIS Control 9.3, 13.3 | HIPAA 164.312(e)(1)' } @{ ID='NP09'; Severity='High'; Weight=7 Text='NAT/PAT rules review - verify no unnecessary port forwarding to internal hosts' Hint='Check the firewall NAT/port forwarding rules. Every port forward maps an external port to an internal host. Run: on pfSense go to Firewall > NAT > Port Forward. On FortiGate check Policy & Objects > Virtual IPs. On SonicWall check Network > NAT Policies. Document each forward: external port, internal IP, internal port, and purpose. Dangerous findings: RDP (3389) forwarded directly, any port forward to a workstation (not a server), forwards to end-of-life systems. Compare against the open port scan from NP02 to verify consistency.' Compliance='NIST CSF PR.AC-5 | CIS Control 4.1, 4.4 | HIPAA 164.312(e)(1)' } @{ ID='NP10'; Severity='Medium'; Weight=5 Text='Firmware/software version on perimeter devices is current and supported' Hint='Check the firmware version of all perimeter devices: firewall, VPN concentrator, edge switches, WAPs. Compare against the vendor current/recommended version. Look for known CVEs affecting the running version - check CVE databases or the vendor security advisories page. Devices running end-of-life firmware are a critical finding because they no longer receive security patches. Check: pfSense System > Update, FortiGate System > Firmware, SonicWall System > Settings > Firmware. Document: device name, current version, latest available version, and any known CVEs.' Compliance='NIST CSF PR.IP-12, ID.RA-1 | CIS Control 2.1, 7.1 | HIPAA 164.312(a)(1)' } ) } 'Identity & Access' = @{ Desc = 'Authentication, authorization, and account lifecycle management' Items = @( @{ ID='IA01'; Severity='Critical'; Weight=10 Text='Domain Admin account audit - document every account with DA privileges and justification' Hint='Run in PowerShell: Get-ADGroupMember "Domain Admins" -Recursive | Select Name,SamAccountName,Enabled | Format-Table. Also check: Enterprise Admins, Schema Admins, and Administrators groups. Every account in these groups needs a documented business justification. Common bad findings: IT staff daily accounts in DA (should use separate admin accounts), service accounts in DA (almost never needed), former employees, generic accounts like "admin" or "sysadmin". The gold standard is: nobody daily-drives a Domain Admin account. They should use a separate privileged account only when needed. Also run: Get-ADUser -Filter {AdminCount -eq 1} to find all accounts with elevated privileges.' Compliance='NIST CSF PR.AC-1, PR.AC-4, PR.AC-6 | CIS Control 5.1, 5.4, 5.5, 6.8 | HIPAA 164.312(a)(1), 164.312(a)(2)(i)' } @{ ID='IA02'; Severity='Critical'; Weight=10 Text='Service accounts audit - verify password age, rotation policy, and least privilege' Hint='Run: Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties PasswordLastSet,PasswordNeverExpires,ServicePrincipalName,MemberOf | Select Name,PasswordLastSet,PasswordNeverExpires. Also search for accounts with "svc", "service", "sql", "backup" in the name. Key checks: 1) Password age - if PasswordLastSet is more than 1 year old, flag it. If it is the account creation date, the password has NEVER been changed. 2) Is it in Domain Admins? Service accounts almost never need DA. 3) Is PasswordNeverExpires set to True? 4) Can you find the password? Check documentation, scripts, batch files, and scheduled tasks for hardcoded passwords. The classic finding: service account with DA + password = CompanyName2019.' Compliance='NIST CSF PR.AC-1, PR.AC-4 | CIS Control 5.2, 5.4, 5.5 | HIPAA 164.312(a)(1), 164.312(d)' } @{ ID='IA03'; Severity='Critical'; Weight=10 Text='MFA coverage - email, VPN, RDP, cloud admin portals, all remote access' Hint='Make a matrix of all remote access points and verify MFA on each: 1) Email (Exchange Online/M365 - check Entra ID > Security > MFA registration, or Conditional Access policies), 2) VPN (check VPN config for RADIUS integration with MFA provider like Duo, Azure MFA), 3) RDP (if exposed externally, must have NLA + MFA via Duo RDP or Azure MFA NPS extension), 4) Cloud admin portals (Azure, AWS, Google Workspace admin - check for MFA enforcement on admin roles), 5) Remote desktop gateways, 6) Any web apps with SSO. Run in Entra ID PowerShell: Get-MgUser -All | Where { $_.StrongAuthenticationMethods.Count -eq 0 } to find users without MFA registered. If any access point is username/password only, flag it as critical.' Compliance='NIST CSF PR.AC-7 | CIS Control 6.3, 6.4, 6.5 | HIPAA 164.312(d)' } @{ ID='IA04'; Severity='Critical'; Weight=10 Text='Terminated employee account review - cross-reference against HR separation list' Hint='Request a list of all employees who left the organization in the past 12-24 months from HR. Then run: Get-ADUser -Filter {Enabled -eq $true} -Properties WhenCreated,LastLogonDate | Select Name,SamAccountName,LastLogonDate,WhenCreated | Export-Csv. Cross-reference the HR list against active AD accounts. Also check: Entra ID/Azure AD, M365 licenses still assigned, VPN accounts, cloud service accounts (Salesforce, QuickBooks, etc.), building access badges, shared mailbox access. Common finding: 30-40% of terminated employees still have active accounts because there is no formal offboarding process. Ask to see the offboarding checklist - if there is not one, that is a finding too.' Compliance='NIST CSF PR.AC-1, PR.AC-6 | CIS Control 5.1, 5.3 | HIPAA 164.312(a)(2)(ii), 164.308(a)(3)(ii)(C)' } @{ ID='IA05'; Severity='High'; Weight=7 Text='Password policy review - length, complexity, expiration, history requirements' Hint='Run: Get-ADDefaultDomainPasswordPolicy. Check: MinPasswordLength (should be 12+ per current NIST guidance, 14+ is better), ComplexityEnabled (should be True), PasswordHistoryCount (should be 24 to prevent reuse), LockoutThreshold (3-5 attempts), LockoutDuration (15-30 min). Also check for Fine-Grained Password Policies: Get-ADFineGrainedPasswordPolicy -Filter *. Note: current NIST 800-63B guidance says forced password expiration is no longer recommended IF you have MFA and breach monitoring. However, many compliance frameworks still require rotation. Document the policy and compare against the applicable standard. If passwords are shorter than 12 characters, flag it.' Compliance='NIST CSF PR.AC-1, PR.AC-7 | CIS Control 5.2 | HIPAA 164.312(d), 164.308(a)(5)(ii)(D)' } @{ ID='IA06'; Severity='High'; Weight=6 Text='Privileged Access Management (PAM) - just-in-time access, session recording' Hint='Check if the organization uses any PAM solution (CyberArk, BeyondTrust, Thycotic/Delinea, ManageEngine PAM360, or even a basic password vault like KeePass for shared admin credentials). Key questions: 1) Are admin passwords stored in a vault or do people just remember them? 2) Is there a checkout/checkin process for privileged credentials? 3) Are privileged sessions recorded/logged? 4) Is there just-in-time (JIT) elevation where admin rights are granted temporarily? For Azure: check Entra ID PIM (Privileged Identity Management) - are admin roles permanently assigned or do users activate them on demand? Most SMBs will not have formal PAM - note as a recommendation with risk context.' Compliance='NIST CSF PR.AC-4, PR.AC-6 | CIS Control 5.4, 5.5, 6.8 | HIPAA 164.312(a)(1)' } @{ ID='IA07'; Severity='Medium'; Weight=5 Text='Shared/generic account inventory and remediation plan' Hint='Search AD for accounts that multiple people use: look for names like "reception", "front desk", "scanner", "warehouse", "shared", "generic", "admin" (without a person name). Run: Get-ADUser -Filter * -Properties Description | Where { $_.Description -match "shared|generic|multiple" }. Also ask: "Does anyone share login credentials?" and "Are there any accounts where multiple people know the password?" Shared accounts destroy accountability - you cannot determine WHO did something if 5 people use the same login. Document each shared account, how many people use it, and what it is used for. Recommend individual accounts with appropriate group permissions instead.' Compliance='NIST CSF PR.AC-1, PR.AC-6 | CIS Control 5.1, 5.4 | HIPAA 164.312(a)(2)(i)' } @{ ID='IA08'; Severity='Medium'; Weight=5 Text='Guest/vendor account lifecycle management' Hint='Ask: "How do you handle vendor/contractor access?" Look for: vendor accounts in AD (names like "vendor_companyname", "contractor_name"), VPN accounts for third parties, shared credentials given to vendors. Check if vendor accounts have: expiration dates set (Get-ADUser -Filter * -Properties AccountExpirationDate | Where { $_.AccountExpirationDate -ne $null }), limited group memberships, been used recently (check LastLogonDate). The finding is almost always: vendor accounts with no expiration, no review process, and too many permissions. Ask when the last vendor access review was performed. If the answer is "never", that is the finding.' Compliance='NIST CSF PR.AC-1, PR.AC-3 | CIS Control 5.1, 5.3, 6.1 | HIPAA 164.308(a)(4)(ii)(B), 164.312(a)(1)' } @{ ID='IA09'; Severity='Medium'; Weight=5 Text='Azure AD / Entra ID conditional access policies reviewed' Hint='If the org uses M365/Azure, log into Entra ID admin center > Protection > Conditional Access. Document all active policies. Key policies that SHOULD exist: 1) Require MFA for all users, 2) Block legacy authentication (Basic auth in IMAP, POP3, SMTP - these bypass MFA), 3) Require compliant device for access, 4) Block access from risky locations/countries, 5) Require MFA for admin roles. If there are NO conditional access policies, that is a significant finding. Also check: Entra ID > Security > Authentication methods to see which MFA methods are enabled (avoid SMS-only, prefer Authenticator app). Note: Conditional Access requires at minimum Entra ID P1 licensing.' Compliance='NIST CSF PR.AC-3, PR.AC-7 | CIS Control 6.3, 6.4, 6.5 | HIPAA 164.312(d), 164.312(e)(1)' } @{ ID='IA10'; Severity='High'; Weight=7 Text='Stale/inactive account cleanup - accounts with no login in 90+ days' Hint='Run: Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonTimestamp | Where { [DateTime]::FromFileTime($_.LastLogonTimestamp) -lt (Get-Date).AddDays(-90) } | Select Name,SamAccountName,@{N="LastLogon";E={[DateTime]::FromFileTime($_.LastLogonTimestamp)}}. This finds enabled accounts that have not logged in for 90+ days. These are prime targets for attackers because nobody notices unauthorized use of a forgotten account. Cross-reference against the terminated employee list (IA04) and service accounts. Some will be legitimate (seasonal workers, leave of absence), but most are just forgotten. Disable them immediately and delete after 30 days if unclaimed.' Compliance='NIST CSF PR.AC-1, PR.AC-6 | CIS Control 5.3 | HIPAA 164.312(a)(2)(ii)' } ) } 'Endpoint Security' = @{ Desc = 'Device-level protection, patching, and hardening' Items = @( @{ ID='EP01'; Severity='Critical'; Weight=10 Text='EDR/AV deployment coverage verified at 100% - identify any gaps' Hint='Pull a report from the EDR/AV management console (CrowdStrike Falcon, SentinelOne, Sophos Central, Defender for Endpoint, Bitdefender GravityZone, etc.). Compare the agent count against total known endpoints from AD: (Get-ADComputer -Filter {Enabled -eq $true}).Count. The numbers should match. Common gaps: Linux servers, developer workstations with "exceptions", IoT devices, personal devices on the network, newly imaged machines, and Macs. Also check: is the agent active and updating? Look for agents that have not checked in for 7+ days - they may be offline, uninstalled, or malfunctioning. AV definitions should be no more than 24 hours old. If using Defender, run: Get-MpComputerStatus on endpoints to verify.' Compliance='NIST CSF DE.CM-4, PR.DS-5 | CIS Control 10.1, 10.2 | HIPAA 164.308(a)(5)(ii)(B)' } @{ ID='EP02'; Severity='Critical'; Weight=10 Text='Patch compliance - internet-facing systems and critical CVEs prioritized' Hint='Check the patch management tool (WSUS, Intune, SCCM/MECM, ConnectWise Automate, NinjaRMM, etc.) for compliance reports. Focus on: 1) Internet-facing systems (web servers, email gateways, VPN appliances) - these must be patched within 48-72 hours for critical CVEs, 2) Critical/High severity CVEs across all systems - check CISA KEV (Known Exploited Vulnerabilities) catalog for actively exploited vulns, 3) Servers first, then workstations. Run: Get-HotFix | Sort InstalledOn -Desc | Select -First 10 on sample systems to check recency. If the last patch was installed more than 30 days ago, flag it. Also check third-party patching (Adobe, Java, Chrome, Firefox, 7-Zip, etc.) - these are frequently exploited and often ignored.' Compliance='NIST CSF PR.IP-12, ID.RA-1 | CIS Control 7.1, 7.2, 7.3, 7.4 | HIPAA 164.308(a)(5)(ii)(B)' } @{ ID='EP03'; Severity='High'; Weight=7 Text='SMB/Protocol hardening - signing, encryption, NTLM level, LLMNR, NetBIOS' Hint='SMB and protocol hardening prevents lateral movement and credential relay attacks. Check: 1) SMBv1 disabled (Get-SmbServerConfiguration | Select EnableSMB1Protocol), 2) SMB signing required (RequireSecuritySignature=True on both server and client - prevents relay attacks), 3) SMB encryption enabled (EncryptData=True for SMB 3.0+), 4) NTLM level set to 5 (LmCompatibilityLevel=5 in HKLM:\SYSTEM\CurrentControlSet\Control\Lsa - NTLMv2 only, refuse LM and NTLM), 5) LLMNR disabled (EnableMulticast=0 in DNS Client GPO - prevents LLMNR poisoning/Responder attacks), 6) NetBIOS over TCP/IP disabled on all adapters (prevents NBT-NS poisoning). These are the most commonly exploited protocols in internal network penetration tests.' Compliance='NIST CSF PR.AC-5, PR.DS-2 | CIS Control 4.1, 4.8 | HIPAA 164.312(e)(1), 164.312(a)(1)' } @{ ID='EP04'; Severity='High'; Weight=6 Text='USB/removable media policy enforced via GPO or endpoint management' Hint='Check Group Policy: Computer Config > Admin Templates > System > Removable Storage Access. Policies should restrict or audit USB mass storage device access. Also check if the EDR platform has USB control (CrowdStrike Device Control, SentinelOne Device Control). This prevents data exfiltration and malware introduction via USB drives. Run: gpresult /h report.html on a sample workstation and search for "Removable" to see applied policies. If there is no USB restriction at all, flag it. For environments that need USB access (labs, manufacturing), recommend whitelisting specific approved devices by hardware ID rather than allowing all. BitLocker To Go can be required for removable media in the policy.' Compliance='NIST CSF PR.AC-3, PR.DS-5, PR.PT-2 | CIS Control 10.3 | HIPAA 164.310(d)(1), 164.312(a)(1)' } @{ ID='EP05'; Severity='High'; Weight=7 Text='BitLocker/disk encryption enabled on all endpoints' Hint='Run on endpoints: manage-bde -status or Get-BitLockerVolume. Check that the C: drive is fully encrypted with XTS-AES 256. For domain-wide view, check AD for BitLocker recovery keys: Get-ADObject -Filter {ObjectClass -eq "msFVE-RecoveryInformation"} -SearchBase "DC=domain,DC=com" | Group {$_.DistinguishedName.Split(",")[1]} | Select Count,Name. Compare against total computer count. For Intune-managed devices, check the Encryption Report in Endpoint Manager. Common findings: desktops are often skipped because "they do not leave the office" (but drives can be stolen from disposed machines), and servers rarely have BitLocker. If laptops are not encrypted and one is lost/stolen, that is a reportable data breach under HIPAA and most state laws.' Compliance='NIST CSF PR.DS-1, PR.DS-5 | CIS Control 3.6 | HIPAA 164.312(a)(2)(iv), 164.312(e)(2)(ii)' } @{ ID='EP06'; Severity='Medium'; Weight=5 Text='Host-based firewall enabled and configured on all endpoints' Hint='Check Windows Firewall status via GPO or locally: Get-NetFirewallProfile | Select Name,Enabled. All three profiles (Domain, Private, Public) should be Enabled. Then check: Get-NetFirewallRule | Where {$_.Enabled -eq "True" -and $_.Direction -eq "Inbound" -and $_.Action -eq "Allow"} | Select DisplayName,Profile. Look for overly permissive inbound rules. Common finding: Windows Firewall was disabled years ago because "it was causing problems" and nobody turned it back on. Also verify GPO enforces the firewall ON so users/techs cannot disable it: Computer Config > Windows Settings > Security Settings > Windows Defender Firewall. If the org has an EDR, check if its host firewall module is active instead.' Compliance='NIST CSF PR.AC-5, PR.PT-4 | CIS Control 4.4, 4.5 | HIPAA 164.312(e)(1)' } @{ ID='EP07'; Severity='Medium'; Weight=5 Text='Application whitelisting / AppLocker policies in place' Hint='Check if AppLocker or Windows Defender Application Control (WDAC) is configured: Get-AppLockerPolicy -Effective (will error if not configured). In Group Policy: Computer Config > Windows Settings > Security Settings > Application Control Policies > AppLocker. Application whitelisting prevents unauthorized executables from running - it is one of the most effective controls against malware and ransomware. Most SMBs will NOT have this. Note it as a strong recommendation. At minimum, recommend blocking execution from user-writable locations (%TEMP%, %APPDATA%, Downloads). Also check if SRP (Software Restriction Policies) are in use as a simpler alternative. Document current state and provide a recommendation for phased rollout.' Compliance='NIST CSF PR.DS-5, PR.IP-1 | CIS Control 2.5, 2.6, 2.7 | HIPAA 164.312(a)(1)' } @{ ID='EP08'; Severity='High'; Weight=7 Text='Hardware security features: VBS, Credential Guard, LSA Protection, TPM 2.0, Secure Boot' Hint='This check validates critical hardware-backed security features. VBS (Virtualization-Based Security) and Credential Guard protect credentials from theft even if the OS is compromised. Check via WMI: Get-CimInstance Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard. VBS Status=2 means Running. SecurityServicesRunning should include 1 (Credential Guard) and 2 (HVCI). LSA Protection (RunAsPPL) prevents credential dumping tools like Mimikatz. Check: HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\RunAsPPL should be 1. WDigest caching should be disabled: HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest\UseLogonCredential should be 0. Secure Boot and TPM 2.0 are prerequisites for VBS. These controls are the #1 defense against credential theft attacks.' Compliance='NIST CSF PR.AC-2, PR.PT-1 | CIS Control 1.1, 4.1, 10.5 | HIPAA 164.310(a)(1) | CMMC L2 SC.L2-3.13.11' } @{ ID='EP09'; Severity='Low'; Weight=3 Text='Auto-run / auto-play disabled across the environment' Hint='Check Group Policy: Computer Config > Admin Templates > Windows Components > AutoPlay Policies. "Turn off AutoPlay" should be Enabled for "All drives". Also check: User Config > Admin Templates > Windows Components > AutoPlay Policies. Run on a sample endpoint: Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" NoDriveTypeAutoRun - a value of 255 disables autorun for all drive types. AutoPlay/AutoRun was a massive malware vector (Conficker worm spread this way). While modern Windows has improved defaults, older systems and USB drives with autorun.inf can still be exploited if this is not explicitly disabled via policy.' Compliance='NIST CSF PR.PT-2 | CIS Control 10.3 | HIPAA 164.308(a)(5)(ii)(B)' } @{ ID='EP10'; Severity='High'; Weight=7 Text='End-of-life operating systems identified and documented with migration plan' Hint='Run: Get-ADComputer -Filter {Enabled -eq $true} -Properties OperatingSystem,OperatingSystemVersion | Group OperatingSystem | Select Count,Name. Flag any: Windows 7, Windows 8/8.1, Windows Server 2008/R2, Server 2012/R2, any "Windows XP" or "Windows Vista". These no longer receive security patches and are actively targeted. Even Server 2012 R2 reached end of extended support in October 2023. For each EOL system, document: hostname, purpose, why it has not been upgraded (legacy app?), and compensating controls in place (network isolation, restricted access). If a system is EOL because of a legacy application, recommend virtualizing and isolating it on its own VLAN with strict firewall rules.' Compliance='NIST CSF PR.IP-12, ID.AM-2 | CIS Control 2.1, 2.2 | HIPAA 164.308(a)(5)(ii)(B)' } ) } 'Backup & Recovery' = @{ Desc = 'Data protection, disaster recovery, and business continuity' Items = @( @{ ID='BR01'; Severity='Critical'; Weight=10 Text='3-2-1 backup rule compliance - 3 copies, 2 media types, 1 offsite' Hint='Interview the IT contact and document: 1) How many copies of data exist? (production + backup + offsite = 3 minimum), 2) What media types? (e.g., local NAS + cloud = 2 media types), 3) Is there an offsite copy? (cloud backup, tape rotation, replicated to another site). Check the backup software (Veeam, Datto, Acronis, Barracuda, BackupExec, Windows Server Backup) and verify all three are configured. Common failure: backups go to a NAS in the same server room. If the building floods or catches fire, both production AND backup are lost. Verify the offsite copy is in a different physical location or a cloud target. Also check: does the 3-2-1 rule apply to ALL critical data or just the file server?' Compliance='NIST CSF PR.IP-4 | CIS Control 11.1, 11.2, 11.3 | HIPAA 164.308(a)(7)(ii)(A), 164.310(d)(2)(iv)' } @{ ID='BR02'; Severity='Critical'; Weight=10 Text='Backup restore TEST completed and documented (not just backup verification)' Hint='This is the single most important backup check. Ask: "When was the last time you actually restored data from a backup?" Not "when was the last successful backup job" - when did you RESTORE something and verify it works? Ask to see documentation of the test: what was restored, from what date, how long did it take, was the data intact? If the answer is "we have never tested a restore" (extremely common), flag it as critical. A backup that has never been tested is not a backup - it is a hope. Recommend: quarterly restore tests at minimum, with full documentation. Test different scenarios: individual file, full server, entire VM, bare-metal recovery, and application-level (e.g., can you restore and bring up Exchange/SQL?).' Compliance='NIST CSF PR.IP-4, PR.IP-9 | CIS Control 11.4, 11.5 | HIPAA 164.308(a)(7)(ii)(D)' } @{ ID='BR03'; Severity='Critical'; Weight=10 Text='Air-gapped or immutable backups in place for ransomware protection' Hint='Modern ransomware specifically targets backup systems - it will encrypt or delete backups before encrypting production data. Check: 1) Are any backup copies air-gapped (physically disconnected from the network)? Examples: tape with offsite rotation, removable USB drives rotated offsite. 2) Are cloud backups immutable (cannot be deleted or modified for a retention period)? Check Veeam immutability settings, Datto cloud retention, Wasabi object lock, AWS S3 Object Lock, Azure immutable blob storage. 3) Can an admin with full access delete ALL backup copies? If yes, ransomware with stolen admin credentials can too. The test: if a ransomware operator gets Domain Admin, can they destroy every backup? If the answer is yes, this is critical.' Compliance='NIST CSF PR.IP-4 | CIS Control 11.3, 11.4 | HIPAA 164.308(a)(7)(ii)(A)' } @{ ID='BR04'; Severity='High'; Weight=7 Text='RTO/RPO defined and documented - business stakeholders are aware of targets' Hint='RTO = Recovery Time Objective (how long can the business be down?). RPO = Recovery Point Objective (how much data can we afford to lose?). Ask the IT lead AND a business stakeholder separately: "If your main server goes down, how long is acceptable before it is back up?" and "If we have to restore from backup, how many hours/days of lost data is acceptable?" If IT says "24 hours" and the business says "2 hours", there is a misalignment that needs to be resolved. Then check if the backup system can actually MEET the stated RTO/RPO. If RPO is 4 hours but backups only run nightly, the backup configuration does not meet the requirement. Document: stated RTO, stated RPO, actual backup frequency, actual tested restore time.' Compliance='NIST CSF ID.BE-5, PR.IP-9, RC.RP-1 | CIS Control 11.1 | HIPAA 164.308(a)(7)(ii)(B)' } @{ ID='BR05'; Severity='High'; Weight=6 Text='Backup encryption enabled for data at rest and in transit' Hint='Check backup software encryption settings. In Veeam: check job settings > Storage > Encryption. In Datto: encryption should be on by default - verify in device settings. Backup data contains everything - credentials, personal data, financial records. If backup media is unencrypted and a tape/drive is lost or stolen, it is a data breach. Check: 1) Are backup files encrypted at rest? (AES-256 preferred) 2) Are backup transfers encrypted in transit? (SSL/TLS to cloud targets) 3) Where are the encryption keys stored? (should NOT be only on the backup server itself - if the server is lost, you cannot decrypt the backups). 4) For cloud backups: is the data encrypted with a customer-managed key or only provider-managed?' Compliance='NIST CSF PR.DS-1, PR.DS-2 | CIS Control 3.6, 3.10 | HIPAA 164.312(a)(2)(iv), 164.312(e)(2)(ii)' } @{ ID='BR06'; Severity='High'; Weight=7 Text='Backup monitoring and alerting for failures is active' Hint='Ask: "When was the last backup failure and how did you find out about it?" Check the backup console for recent job history. Look for: failed jobs in the last 30 days, jobs with warnings (partial failures), jobs that have not run at all. Then check: is there automated alerting? Who receives the alerts? Is someone REVIEWING the alerts daily? Common finding: backup alerts go to an email distribution list that nobody reads, or alerts are configured but the email address is a former employee. The worst scenario: backups have been silently failing for months and nobody noticed. Check backup log/history going back 90 days. Also verify: are ALL critical systems included in backup jobs? Compare the backup job list against the server inventory.' Compliance='NIST CSF DE.CM-3, DE.DP-4 | CIS Control 11.2 | HIPAA 164.308(a)(7)(ii)(A)' } @{ ID='BR07'; Severity='Medium'; Weight=5 Text='DR plan documented and tabletop exercise completed within past 12 months' Hint='Ask to see the Disaster Recovery Plan document. Key elements it should contain: 1) Contact list/call tree, 2) System priority list (what gets recovered first), 3) Step-by-step recovery procedures for each critical system, 4) RTO/RPO targets per system, 5) Alternate site/work location plan, 6) Communication plan for employees/customers. Then ask: "When was the last time you walked through this plan as a team?" A tabletop exercise is a meeting where you simulate a disaster scenario and walk through the response steps. If there is no DR plan, that is a finding. If there is a plan but it has never been tested, that is also a finding. Plans that exist only on paper are often outdated and full of incorrect assumptions.' Compliance='NIST CSF PR.IP-9, RC.RP-1, RC.IM-1 | CIS Control 11.5 | HIPAA 164.308(a)(7)(i), 164.308(a)(7)(ii)(B-D)' } @{ ID='BR08'; Severity='Medium'; Weight=5 Text='Cloud/SaaS backup coverage (M365, Google Workspace, etc.)' Hint='A very common misconception: "Microsoft backs up our data in M365." Microsoft provides infrastructure resilience, NOT data backup. If a user deletes files or email, or ransomware encrypts SharePoint, the data can be permanently lost after retention periods expire. Check: 1) Is there a third-party backup of M365 data? (Veeam for M365, Datto SaaS Protection, Barracuda Cloud-to-Cloud, Spanning, AvePoint). 2) What is backed up? (Exchange mailboxes, OneDrive, SharePoint, Teams). 3) What is the retention period? 4) Has a restore test been performed? Also check for other SaaS apps that hold business data: Salesforce, QuickBooks Online, HubSpot, etc. If the business relies on it and it is not backed up, it is a finding.' Compliance='NIST CSF PR.IP-4 | CIS Control 11.1, 11.2 | HIPAA 164.308(a)(7)(ii)(A), 164.310(d)(2)(iv)' } ) } 'Logging & Monitoring' = @{ Desc = 'Visibility into security events and incident detection capability' Items = @( @{ ID='LM01'; Severity='High'; Weight=7 Text='DNS query logging enabled and retained for incident response' Hint='DNS logs are a goldmine during incident response - they show every domain every device queried. Check: 1) On Windows DNS Server: DNS Manager > Server Properties > Debug Logging (legacy) or better: DNS Analytical logging via Event Viewer or PowerShell: Set-DnsServerDiagnostics -All $true. 2) On pfSense with pfBlockerNG/Unbound: check if query logging is enabled in Unbound settings. 3) If using a DNS filter (Umbrella, NextDNS): check their query log retention. Key: logs must be RETAINED for at least 90 days (preferably 1 year) for incident investigation. If an incident occurs and you need to know "what domains did the compromised host contact?" you need these logs. If DNS logging is not enabled, that is a significant visibility gap.' Compliance='NIST CSF DE.CM-1, DE.AE-3 | CIS Control 8.2, 8.9 | HIPAA 164.312(b), 164.308(a)(1)(ii)(D)' } @{ ID='LM02'; Severity='High'; Weight=8 Text='Centralized log collection (SIEM or log aggregator) deployed' Hint='Ask: "Where do your logs go?" If the answer is "they stay on each server" that is a finding - attackers delete local logs to cover tracks. Check for: SIEM solutions (Splunk, Microsoft Sentinel, Elastic SIEM, Wazuh, AlienVault/AT&T USM, Graylog, LogRhythm) or log aggregators (syslog server, Windows Event Collector). At minimum, these sources should feed centrally: Domain Controllers (authentication events), firewall (traffic logs), VPN (connection logs), DNS (query logs), file servers (access logs), and EDR/AV (detection logs). If there is no central logging, the org is essentially blind to security events and cannot perform effective incident response. For SMBs, Wazuh (free/open-source) or Microsoft Sentinel (if already on Azure) are cost-effective options to recommend.' Compliance='NIST CSF DE.CM-1, DE.CM-3, DE.AE-3 | CIS Control 8.2, 8.5, 8.9 | HIPAA 164.312(b), 164.308(a)(1)(ii)(D)' } @{ ID='LM03'; Severity='High'; Weight=7 Text='Windows Event Log forwarding configured for security events' Hint='Check if Windows Event Forwarding (WEF) or an agent-based collection is active. Key events to collect from DCs and servers: Event ID 4624/4625 (logon success/failure), 4672 (special privilege logon), 4720/4726 (user created/deleted), 4732/4733 (user added/removed from security group), 4740 (account lockout), 1102 (audit log cleared - VERY suspicious), 4688 (process creation with command line logging). Run on a DC: wevtutil qe Security /c:5 /rd:true /f:text to see recent security events. Check GPO: Computer Config > Windows Settings > Security Settings > Advanced Audit Policy Configuration. If "Audit Logon Events" is "No Auditing", critical visibility is missing. Increase audit policy and forward events to a central collector.' Compliance='NIST CSF DE.CM-1, DE.CM-3, DE.AE-3 | CIS Control 8.2, 8.5, 8.8 | HIPAA 164.312(b)' } @{ ID='LM04'; Severity='Medium'; Weight=5 Text='Firewall logging enabled with adequate retention period' Hint='Check the firewall logging configuration. On pfSense: Status > System Logs > Settings. On FortiGate: Log & Report > Log Settings. On SonicWall: Log > Settings. Verify: 1) Traffic logging is enabled for both allowed and denied traffic, 2) Logs are stored remotely (syslog to a log server, not just local disk that can fill up), 3) Retention period is at least 90 days. Check: how much disk space is allocated for logs? If the firewall has a 256MB log partition and generates 50MB/day, logs only go back 5 days. Remote syslog solves this. Also verify: are the logs being REVIEWED? Look at the firewall dashboard for top blocked connections, top talkers, and any suspicious patterns. If nobody looks at the logs, they serve limited purpose beyond post-incident forensics.' Compliance='NIST CSF DE.CM-1 | CIS Control 8.2, 8.5, 8.9 | HIPAA 164.312(b)' } @{ ID='LM05'; Severity='Medium'; Weight=5 Text='Failed login attempt monitoring and alerting' Hint='Check: 1) Account lockout policy is configured (Net accounts on any domain machine shows lockout threshold/duration), 2) Someone is alerted when accounts get locked out or when there are brute-force patterns. In AD: check Event ID 4740 (account lockout) on the PDC emulator DC. Run: Get-EventLog Security -InstanceId 4740 -Newest 20 | Format-Table TimeGenerated,Message. If there are frequent lockouts, investigate why - it could be a brute-force attack, a misconfigured service, or a user with a saved wrong password. For alerting: does the SIEM/monitoring tool have a rule for "more than X failed logins in Y minutes"? If there is no lockout policy AND no monitoring, an attacker can brute-force passwords indefinitely with no detection.' Compliance='NIST CSF DE.CM-1, DE.AE-2 | CIS Control 8.5 | HIPAA 164.312(b), 164.308(a)(1)(ii)(D)' } @{ ID='LM06'; Severity='Medium'; Weight=5 Text='File integrity monitoring on critical systems' Hint='FIM detects unauthorized changes to critical system files, configurations, and binaries. Check if the EDR or a dedicated FIM tool (Tripwire, OSSEC/Wazuh, CrowdStrike Falcon FIM) monitors changes to: system32 executables, registry keys (Run/RunOnce), scheduled tasks, services, startup items, critical application configs, and web server directories. On Windows: Sysmon (Event ID 11 - file create, Event ID 13 - registry modification) is a lightweight option. If there is no FIM, an attacker who modifies system files or installs a backdoor may go undetected. For SMBs, this is often a stretch goal - note as a recommendation. If they have Wazuh or an EDR with FIM capabilities, verify it is actually enabled and configured for key paths.' Compliance='NIST CSF DE.CM-1, DE.CM-5 | CIS Control 3.14 | HIPAA 164.312(b), 164.312(c)(2)' } @{ ID='LM07'; Severity='Medium'; Weight=5 Text='Log retention meets compliance and investigation requirements' Hint='Ask what the log retention policy is (if one exists) and verify actual retention. Compliance minimums: HIPAA requires 6 years for audit logs related to PHI, PCI DSS requires 1 year with 3 months immediately available, most cyber insurance policies require 90 days minimum. Check actual retention by looking at the oldest available log entry in: the SIEM, the firewall, the DC event logs (wevtutil el then check Security log size/retention), and the backup system. Common finding: logs are configured to "overwrite as needed" with a small max size, so actual retention is only days or weeks. Increase log file sizes and ensure remote/central logging has adequate storage for the required retention period.' Compliance='NIST CSF DE.CM-1, PR.PT-1 | CIS Control 8.1, 8.9, 8.10 | HIPAA 164.312(b), 164.530(j)(2)' } @{ ID='LM08'; Severity='High'; Weight=7 Text='Security alerting and incident response notification process defined' Hint='Ask: "If a critical security alert fires at 2am on Saturday, what happens?" There should be a documented process: who gets notified first, what is the escalation path, what are the first response steps, is there an IR retainer with a security firm? Check: 1) Does the EDR/SIEM have email/SMS/phone alerting configured? 2) Who receives the alerts - is it a person, or an unmonitored mailbox? 3) Is there 24/7 coverage or only business hours? 4) Is there an incident response plan/playbook? For SMBs, an MDR (Managed Detection and Response) service provides 24/7 monitoring. If the org has no alerting, no IR plan, and no MDR, they will only discover a breach when damage is visible (ransomware note, customer complaint, bank fraud alert).' Compliance='NIST CSF DE.DP-4, RS.CO-2, RS.CO-3 | CIS Control 17.1, 17.2, 17.4 | HIPAA 164.308(a)(6)(i), 164.308(a)(6)(ii)' } ) } 'Network Architecture' = @{ Desc = 'Network design, segmentation, and traffic control' Items = @( @{ ID='NA01'; Severity='Critical'; Weight=10 Text='Network segmentation implemented - no flat network topology' Hint='A flat network means every device can talk to every other device - servers, workstations, printers, IoT, guests all on the same subnet. This is an attacker paradise because compromising one device gives immediate access to everything. Check: run "ipconfig /all" on a workstation and on a server - if they are on the same subnet (e.g., both 192.168.1.x/24), the network is flat. Proper segmentation puts: servers on their own VLAN/subnet, workstations on another, IoT/cameras/printers on another, guest WiFi completely isolated. Check the switch configuration for VLAN assignments and the firewall for inter-VLAN routing rules. If everything is on one subnet with no VLANs, this is a critical finding. Draw or request a network diagram to visualize the topology.' Compliance='NIST CSF PR.AC-5 | CIS Control 12.2, 12.8 | HIPAA 164.312(e)(1)' } @{ ID='NA02'; Severity='High'; Weight=7 Text='VLAN separation between user, server, IoT, and guest networks' Hint='Log into the core switch (managed switch) and review VLAN configuration. Common VLANs that should exist: Management (switch/AP management interfaces), Servers, Workstations/Users, VoIP phones, Security cameras/IoT, Guest WiFi, Printers. On Cisco: show vlan brief. On HP/Aruba: show vlans. On UniFi: check Networks in the controller. Then check the firewall/router inter-VLAN rules: the guest VLAN should have NO access to internal VLANs, IoT should only reach what it needs, workstations should access servers on specific ports only. If VLANs exist but there are no ACLs/firewall rules between them, the segmentation is cosmetic only. Common finding: VLANs were created but the firewall allows all inter-VLAN traffic, defeating the purpose.' Compliance='NIST CSF PR.AC-5 | CIS Control 12.2 | HIPAA 164.312(e)(1)' } @{ ID='NA03'; Severity='High'; Weight=7 Text='Wireless security - WPA3/WPA2-Enterprise, rogue AP detection' Hint='Check WiFi configuration on the wireless controller or APs. Key checks: 1) What encryption? WPA2-Personal (PSK) with a shared password is acceptable for guest but not for corporate. Corporate WiFi should use WPA2-Enterprise or WPA3-Enterprise with RADIUS authentication (users log in with their own AD credentials). 2) Is the PSK strong and rotated? If corporate WiFi uses a shared password that has not changed in years and every employee knows it (including former employees), flag it. 3) Rogue AP detection: does the wireless controller scan for unauthorized access points? (UniFi, Meraki, Aruba all have this). 4) Is guest WiFi truly isolated from the corporate network? Connect to guest and try to ping internal IPs. 5) Check for open/hidden SSIDs broadcasting that should not be.' Compliance='NIST CSF PR.AC-3, PR.AC-5 | CIS Control 12.6 | HIPAA 164.312(e)(1)' } @{ ID='NA04'; Severity='Medium'; Weight=5 Text='Network diagram is current and accurately documents infrastructure' Hint='Ask for the network diagram. Check: 1) Does one exist? (if not, that is a finding), 2) When was it last updated? 3) Does it show: all VLANs/subnets with IP ranges, firewall placement, switch interconnections, WAN/ISP connections, VPN tunnels, cloud connections, server locations, wireless AP placement? Walk the server room/closets and compare the physical setup against the diagram. Common finding: the diagram is 3 years old, shows equipment that has been replaced, and is missing new additions. An inaccurate diagram leads to security blind spots and slows incident response. Tools to recommend for maintaining diagrams: draw.io (free), Lucidchart, Visio, or even Netbox for automated documentation.' Compliance='NIST CSF ID.AM-1, ID.AM-2, ID.AM-4 | CIS Control 1.1, 1.2, 12.1 | HIPAA 164.310(d)(2)(iii)' } @{ ID='NA05'; Severity='Medium'; Weight=5 Text='802.1X / NAC deployed for network access control' Hint='802.1X prevents unauthorized devices from connecting to the network. When a device plugs into a switch port, it must authenticate before getting network access. Check: 1) Is 802.1X configured on switch ports? (show dot1x all on Cisco) 2) Is there a RADIUS server for authentication? (NPS on Windows, FreeRADIUS, Cisco ISE, Aruba ClearPass) 3) What happens to devices that fail authentication? (should go to a quarantine/guest VLAN, not get full access). Most SMBs will NOT have 802.1X. Note it as a recommendation. Alternative compensating control: MAC address filtering on switch ports (weak but better than nothing), or monitoring for new devices via network scanning. Also check if unused switch ports are administratively disabled - if you can plug in anywhere and get an IP, that is a finding.' Compliance='NIST CSF PR.AC-1, PR.AC-3 | CIS Control 1.4, 12.5 | HIPAA 164.312(a)(1)' } @{ ID='NA06'; Severity='Medium'; Weight=5 Text='Management interfaces isolated from production traffic' Hint='Management interfaces include: switch management IPs, AP management consoles, firewall admin portals, IPMI/iLO/iDRAC (server out-of-band management), UPS management cards, printer admin pages. These should NOT be accessible from the general user VLAN. Check: 1) Can you reach the switch admin page from a regular workstation? (try browsing to the switch IP from a user PC) 2) Is there a dedicated management VLAN? 3) Are iLO/iDRAC interfaces on their own network segment? 4) Is SSH/HTTPS used for management, not Telnet/HTTP? If management interfaces are on the same VLAN as users, a compromised workstation can access the entire infrastructure. Common finding: all management IPs are on the same subnet as users with no access restrictions.' Compliance='NIST CSF PR.AC-5, PR.PT-3 | CIS Control 12.2, 12.7 | HIPAA 164.312(e)(1)' } @{ ID='NA07'; Severity='High'; Weight=7 Text='Switch port security and unused port management' Hint='Check managed switches for: 1) Unused ports - are they administratively disabled? (show interfaces status on Cisco, look for "disabled" vs "notconnect") If unused ports are up and active, someone could plug in an unauthorized device. 2) Port security - is MAC address limiting configured to prevent MAC flooding attacks? 3) DHCP snooping - prevents rogue DHCP servers from handing out malicious network configs. 4) Dynamic ARP inspection (DAI) - prevents ARP spoofing/MITM attacks. On smaller switches (UniFi, Netgear, etc.), at least verify unused ports are disabled. Walk the physical space: are there active ethernet jacks in lobbies, conference rooms, or public areas? These should be on the guest VLAN or disabled.' Compliance='NIST CSF PR.AC-5, PR.PT-4 | CIS Control 1.4, 12.2, 12.5 | HIPAA 164.312(e)(1)' } ) } 'Physical Security' = @{ Desc = 'Physical access controls and environmental protections' Items = @( @{ ID='PS01'; Severity='High'; Weight=7 Text='Server room / MDF / IDF locked with access control and logging' Hint='Physically inspect the server room and all network closets (MDF = Main Distribution Frame, IDF = Intermediate Distribution Frame). Check: 1) Is the door locked? (test it), 2) What type of lock? (key lock, badge reader, combination), 3) Who has access? (get a list), 4) Is access logged? (badge reader logs, sign-in sheet), 5) Is the door propped open? (common finding). Also check: are there windows into the server room that could allow visual access to screens/labels? Is the room shared with other uses (storage, break room)? Is there environmental monitoring (temperature/humidity)? Is there water detection (pipes overhead that could leak)? The classic finding: server "room" is actually an unlocked closet that doubles as a supply storage area with no access logging.' Compliance='NIST CSF PR.AC-2 | CIS Control 1.1 | HIPAA 164.310(a)(1), 164.310(a)(2)(ii-iv)' } @{ ID='PS02'; Severity='Medium'; Weight=4 Text='Visitor sign-in/sign-out log maintained at reception' Hint='Check the front desk/reception area. Is there a visitor log? Can you walk into the building without signing in? Key checks: 1) Is there a sign-in process for visitors? (paper log, digital system like Envoy), 2) Are visitors given badges that distinguish them from employees? 3) Are visitors escorted in sensitive areas? 4) Is there a sign-OUT process? (many have sign-in but no sign-out, so you cannot tell if someone is still in the building). Test: can you tailgate through a badge door behind an employee without challenge? For compliance-heavy environments (healthcare, financial), visitor logs may need to be retained. Ask how long they keep the logs and whether they can produce a list of who was in the building on a specific date.' Compliance='NIST CSF PR.AC-2 | CIS Control 1.1 | HIPAA 164.310(a)(2)(iii), 164.310(b)' } @{ ID='PS03'; Severity='Medium'; Weight=4 Text='Security cameras covering entry points and sensitive areas' Hint='Walk the facility and note camera placement. Key coverage areas: main entrance, server room door, parking lot, emergency exits, shipping/receiving. Check: 1) Are cameras operational? (look at the NVR/DVR - is it recording?), 2) What is the retention period? (30 days minimum, 90 days recommended), 3) Is the NVR in a secure location? (if the NVR is in an unlocked closet, someone could steal it and the footage). 4) Are cameras on a separate VLAN? (IoT cameras on the production network is a security risk - they are notoriously hackable). 5) Are default passwords changed on the cameras and NVR? (check for admin/admin or admin/12345). 6) Can cameras be accessed remotely? If so, is that access MFA-protected?' Compliance='NIST CSF PR.AC-2, DE.CM-2 | CIS Control 1.1 | HIPAA 164.310(a)(2)(iii), 164.310(d)(1)' } @{ ID='PS04'; Severity='Medium'; Weight=4 Text='Clean desk policy enforced - no credentials on sticky notes' Hint='Walk through the office space and look at desks, monitors, and walls. Common findings: passwords on sticky notes stuck to monitors, whiteboards with network diagrams and credentials, unlocked workstations with users away, sensitive documents left on printers, server credentials taped inside server room cabinets/doors. Check: 1) Are screens locked when users step away? (Windows key + L should be habit), 2) Is there an auto-lock GPO? (Screen saver timeout with password on resume - check GPO: User Config > Admin Templates > Control Panel > Personalization > Screen saver timeout), 3) Are sensitive printouts in the open? 4) Look inside the server room specifically for credentials written on equipment, taped to walls, or in obvious locations.' Compliance='NIST CSF PR.AC-2, PR.AT-1 | CIS Control 5.2 | HIPAA 164.310(b), 164.310(c)' } @{ ID='PS05'; Severity='Low'; Weight=3 Text='Network jacks in public areas disabled or on guest VLAN' Hint='Walk the public-facing areas: lobby, conference rooms, waiting areas, break rooms. Look for active ethernet wall jacks. Test by plugging in a laptop: 1) Do you get an IP address? 2) What VLAN/subnet are you on? 3) Can you ping internal servers? If a network jack in the lobby gives access to the production network, any visitor can plug in a device and access internal resources or run network attacks. These ports should either be: administratively disabled on the switch, placed on a guest VLAN with no internal access, or controlled via 802.1X. Also check for exposed network equipment (switches, patch panels) in non-secure areas like ceiling tiles in public spaces. Check conference rooms carefully - these often have active drops for presentation systems.' Compliance='NIST CSF PR.AC-2, PR.AC-5 | CIS Control 1.4, 12.5 | HIPAA 164.310(c)' } @{ ID='PS06'; Severity='Low'; Weight=3 Text='UPS/generator for critical infrastructure with regular testing' Hint='Check the server room/MDF power: 1) Is there a UPS (Uninterruptible Power Supply)? Check brand/model, battery age, and load percentage (batteries typically last 3-5 years), 2) Is the UPS monitored? (network card, USB to server for graceful shutdown), 3) Is there a generator for extended outages? 4) When was the UPS last tested under load? (not just a self-test - an actual power outage simulation), 5) Are all critical devices plugged into the UPS, or are some on regular power strips? Check UPS health: most have a front panel display or web interface showing battery health, load percentage, and estimated runtime. If the battery is showing "replace" or the UPS is overloaded (>80%), flag it. A dead UPS during a power blip means unclean server shutdowns and potential data loss.' Compliance='NIST CSF PR.PT-5 | CIS Control 1.1 | HIPAA 164.310(a)(2)(ii)' } ) } 'Common Findings' = @{ Desc = 'Items found in virtually every SMB audit - verify these are addressed first' Items = @( @{ ID='CF01'; Severity='Critical'; Weight=10 Text='Service accounts with Domain Admin privileges and weak/default passwords' Hint='This is finding #1 across SMB audits. Run: Get-ADGroupMember "Domain Admins" -Recursive | Where { $_.objectClass -eq "user" } | ForEach { Get-ADUser $_ -Properties PasswordLastSet,Description,ServicePrincipalName } | Where { $_.ServicePrincipalName -or $_.Description -match "service|svc|sql|backup" } | Select Name,PasswordLastSet. If you find service accounts in DA with passwords set years ago, the password is almost certainly weak. Common passwords: CompanyName + Year (Acme2019), Season + Year (Summer2023), Password1, Welcome1, or the account name itself. This is how most real-world breaches escalate to full domain compromise. Kerberoasting attacks specifically target service accounts with SPNs. Recommend: remove from DA, set 25+ character random passwords, use Group Managed Service Accounts (gMSA) where possible.' Compliance='NIST CSF PR.AC-1, PR.AC-4 | CIS Control 5.2, 5.4, 5.5 | HIPAA 164.312(a)(1), 164.312(d)' } @{ ID='CF02'; Severity='Critical'; Weight=10 Text='No egress filtering configured on the firewall' Hint='This means the firewall allows ALL outbound traffic. Log into the firewall and check the outbound/LAN-to-WAN rules. If there is a single "Allow All" rule for outbound traffic with no restrictions, flag it. Why this matters: when malware infects a workstation, it needs to communicate with its command-and-control (C2) server. With no egress filtering, the malware can use ANY port to call home and exfiltrate data. Proper egress filtering: 1) Allow only needed outbound ports (80, 443, plus documented business needs), 2) Block direct IP connections that bypass DNS, 3) Force DNS through the filtering server. Even basic egress filtering (block everything except 80/443 outbound) would prevent most malware C2 channels. This is one of the highest-impact, lowest-cost improvements an SMB can make.' Compliance='NIST CSF PR.AC-5, PR.DS-5 | CIS Control 4.4, 4.5, 9.3 | HIPAA 164.312(e)(1)' } @{ ID='CF03'; Severity='Critical'; Weight=10 Text='Backups exist but have never been restore-tested' Hint='Ask the direct question: "When was the last time you performed a test restore from backup? Not a backup verification - an actual restore." If the answer is "never" or "I cannot remember," flag it as critical. Then ask: "How do you know your backups work?" Common response: "The backup software says it completed successfully." Backup software can report success while the data is corrupt, incomplete, or missing critical components. The ONLY way to verify a backup works is to restore it and confirm the data/application is functional. Recommend: perform a test restore during the audit if possible, then establish quarterly test restore schedule. Document: what was tested, how long the restore took (this is the actual RTO), and whether the data was intact.' Compliance='NIST CSF PR.IP-4, PR.IP-9 | CIS Control 11.4, 11.5 | HIPAA 164.308(a)(7)(ii)(D)' } @{ ID='CF04'; Severity='Critical'; Weight=10 Text='Former employee accounts remain active in AD/Entra ID' Hint='Get the HR termination list and cross-reference with: 1) AD: Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonDate,WhenCreated | Export-Csv, 2) Entra ID/Azure AD: Get-MgUser -Filter "accountEnabled eq true" | Select DisplayName,UserPrincipalName, 3) M365: Get-Mailbox | Select Alias, 4) VPN: check VPN user list, 5) Any other system with separate authentication. Common finding: 20-50% of employees who left in the last year still have active accounts. This is a critical risk because a disgruntled former employee or an attacker who finds these credentials has full access. Ask: "Is there a documented offboarding checklist? Who is responsible for disabling accounts?" If there is no formal process, recommend one that triggers the same day as separation with IT receiving automated notification from HR.' Compliance='NIST CSF PR.AC-1, PR.AC-6 | CIS Control 5.1, 5.3 | HIPAA 164.308(a)(3)(ii)(C), 164.312(a)(2)(ii)' } @{ ID='CF05'; Severity='High'; Weight=7 Text='Temporary firewall rules still in place from 3+ years ago' Hint='Export the full firewall rule list and sort by creation date. Flag any rule where: 1) Description contains "temp", "test", "troubleshooting", "vendor", or a person name, 2) Creation date is more than 2 years old, 3) Hit count is zero (rule is not being used), 4) There is no associated change ticket or documentation. Common story: "We opened this port for a vendor 4 years ago to fix something. We meant to close it after but forgot." These forgotten rules are often overly permissive (allow all from vendor IP) and the vendor may have recycled that IP address by now, meaning a random internet host now has access. Document each stale rule and recommend a firewall rule review process: every rule gets a review date, an owner, and an expiration.' Compliance='NIST CSF PR.AC-5, ID.AM-4 | CIS Control 4.5 | HIPAA 164.312(e)(1)' } @{ ID='CF06'; Severity='High'; Weight=8 Text='Flat network with no segmentation between workstations and servers' Hint='Test this: from a regular user workstation, can you ping the domain controller? The file server? The SQL server? Security cameras? Printers? If the answer is yes to all, the network is flat. Run: tracert to a server IP from a workstation - if it is a direct route (no hops through a firewall/router), there is no segmentation. Also run: arp -a to see what devices are visible on the same broadcast domain. In a flat network, if one workstation gets compromised (phishing email, drive-by download), the attacker can immediately see and reach every server. Proper segmentation forces traffic through a firewall with rules, creating choke points where you can detect and block lateral movement. This is one of the most common and most impactful findings in SMB audits.' Compliance='NIST CSF PR.AC-5 | CIS Control 12.2, 12.8 | HIPAA 164.312(e)(1)' } @{ ID='CF07'; Severity='High'; Weight=7 Text='Local admin rights granted broadly without documentation' Hint='On 5-10 sample workstations, run: net localgroup Administrators. If you see "Domain Users" or large security groups in the local Administrators group, every user is a local admin. Also check for specific users in the group beyond the built-in Administrator and Domain Admins. Ask IT: "Is there a policy for who gets local admin and how it is approved?" If the answer is "everyone has it because they complained they could not install things," that is the finding. Local admin allows: installing software (including malware), disabling antivirus, accessing other users data on the machine, running credential harvesting tools (Mimikatz), and pivoting to other systems. Recommend: remove local admin for all standard users, implement a self-service elevation tool (MakeMeAdmin, AutoElevate) or a software deployment solution.' Compliance='NIST CSF PR.AC-4, PR.AC-6 | CIS Control 5.4, 5.5 | HIPAA 164.312(a)(1)' } @{ ID='CF08'; Severity='High'; Weight=7 Text='No DNS filtering or content filtering in place' Hint='Test this from a workstation: run "nslookup" and check what DNS server is being used. If it is the ISP DNS directly (Comcast, AT&T, etc.) or a public resolver (8.8.8.8, 1.1.1.1) with no filtering layer, there is no DNS filtering. Try browsing to a known test malware domain - if it resolves and loads, there is no filtering. DNS filtering is one of the cheapest and most effective security controls: it blocks access to known malware, phishing, and C2 domains before the connection is even established. Solutions: Cisco Umbrella (enterprise, per-user pricing), NextDNS (very affordable for SMBs), Cloudflare Gateway (free tier available), pfBlockerNG (free on pfSense). If the org has none of these, recommend implementing one immediately. It can typically be deployed in under an hour by changing the DHCP DNS settings.' Compliance='NIST CSF DE.CM-1, PR.DS-5 | CIS Control 9.2, 9.3 | HIPAA 164.312(e)(1)' } ) } } # ── Auto-Check Definitions ────────────────────────────────────────────────── # Each entry maps an audit item ID to a scriptblock that returns: # @{ Status='Pass|Fail|Partial'; Findings='text'; Evidence='text' } # Type: AD = requires domain controller, Local = runs on target endpoint, # Remote = runs via Invoke-Command on target $script:AutoChecks = @{ # ── Identity & Access ──────────────────────────────────────────────────── 'IA01' = @{ Type='AD'; Label='Scan Privileged Groups + Delegation' Script = { $groups = @('Domain Admins','Enterprise Admins','Schema Admins','Administrators') $sb = [System.Text.StringBuilder]::new() $totalPriv = 0; $issues = 0 foreach ($g in $groups) { try { $members = Get-ADGroupMember $g -Recursive -EA Stop | Select-Object Name,SamAccountName,objectClass $count = ($members | Measure-Object).Count; $totalPriv += $count [void]$sb.AppendLine("[$g] ($count members):") foreach ($m in $members) { [void]$sb.AppendLine(" $($m.SamAccountName) ($($m.objectClass))") } # CIS: Enterprise Admins and Schema Admins should be empty if ($g -in @('Enterprise Admins','Schema Admins') -and $count -gt 0) { $issues++; [void]$sb.AppendLine(" [!] CIS: $g should be EMPTY except during schema changes") } [void]$sb.AppendLine("") } catch { [void]$sb.AppendLine("[$g] Error: $_`n") } } # Protected Users group check try { $protectedUsers = (Get-ADGroupMember 'Protected Users' -EA SilentlyContinue | Measure-Object).Count [void]$sb.AppendLine("[Protected Users] ($protectedUsers members)") if ($protectedUsers -eq 0) { $issues++; [void]$sb.AppendLine(" [!] No privileged accounts in Protected Users group - Tier 0 accounts should be members") } } catch { [void]$sb.AppendLine("[Protected Users] Could not query") } # Kerberos unconstrained delegation scan try { $unconst = Get-ADComputer -Filter {TrustedForDelegation -eq $true -and PrimaryGroupID -ne 516} -Properties TrustedForDelegation,OperatingSystem -EA SilentlyContinue if ($unconst) { $issues += $unconst.Count [void]$sb.AppendLine("`n[CRITICAL] UNCONSTRAINED DELEGATION ($($unconst.Count) systems):") foreach ($u in $unconst) { [void]$sb.AppendLine(" $($u.Name) | $($u.OperatingSystem)") } } else { [void]$sb.AppendLine("`nUnconstrained Delegation: None found (excluding DCs) [OK]") } $unconstUsers = Get-ADUser -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation -EA SilentlyContinue if ($unconstUsers) { $issues += $unconstUsers.Count [void]$sb.AppendLine("[CRITICAL] USER accounts with unconstrained delegation:") foreach ($uu in $unconstUsers) { [void]$sb.AppendLine(" $($uu.SamAccountName)") } } } catch {} $status = if ($issues -eq 0 -and $totalPriv -le 5) {'Pass'} elseif ($issues -eq 0 -and $totalPriv -le 10) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Privileged groups + delegation scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'IA02' = @{ Type='AD'; Label='Scan Service Accounts + Kerberoast Risk' Script = { $spn = Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties PasswordLastSet,PasswordNeverExpires,ServicePrincipalName,MemberOf,Enabled,AdminCount -EA Stop $named = Get-ADUser -Filter 'SamAccountName -like "svc*" -or SamAccountName -like "*service*" -or SamAccountName -like "sql*" -or SamAccountName -like "backup*"' -Properties PasswordLastSet,PasswordNeverExpires,MemberOf,Enabled,AdminCount -EA SilentlyContinue $all = @($spn) + @($named) | Sort-Object -Property SamAccountName -Unique $sb = [System.Text.StringBuilder]::new(); $issues = 0; $kerberoastable = 0 foreach ($a in $all) { $age = if ($a.PasswordLastSet) { ((Get-Date) - $a.PasswordLastSet).Days } else { 9999 } $inDA = ($a.MemberOf | Where-Object { $_ -match 'Domain Admins' }).Count -gt 0 $hasSPN = ($a.ServicePrincipalName | Measure-Object).Count -gt 0 $flags = @() if ($age -gt 365) { $flags += "PW_OLD_${age}d"; $issues++ } if ($a.PasswordNeverExpires) { $flags += 'NO_EXPIRE'; $issues++ } if ($inDA) { $flags += 'DOMAIN_ADMIN'; $issues++ } # Kerberoast risk: user account with SPN + old password + admin = CRITICAL if ($hasSPN -and $a.Enabled) { $kerberoastable++ $risk = 'LOW' if ($age -gt 365) { $risk = 'MEDIUM' } if ($age -gt 365 -and ($a.AdminCount -eq 1 -or $inDA)) { $risk = 'CRITICAL' } $flags += "KERBEROAST_RISK:$risk" if ($risk -eq 'CRITICAL') { $issues += 2 } } $f = if ($flags) { " [$(($flags -join ', '))]" } else { '' } [void]$sb.AppendLine("$($a.SamAccountName) | Enabled:$($a.Enabled) | PW Age:${age}d$f") } # gMSA adoption check try { $gmsa = Get-ADServiceAccount -Filter * -EA SilentlyContinue $gmsaCount = ($gmsa | Measure-Object).Count [void]$sb.AppendLine("`ngMSA accounts found: $gmsaCount $(if($gmsaCount -eq 0){'[Consider migrating service accounts to gMSA]'}else{'[OK]'})") } catch { [void]$sb.AppendLine("`ngMSA: Could not query (requires AD schema support)") } [void]$sb.AppendLine("Kerberoastable accounts: $kerberoastable") $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Service account + Kerberoast scan ($($all.Count) found, $issues issues) @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'IA04' = @{ Type='AD'; Label='Scan Terminated Accounts' Script = { $users = Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonDate,WhenCreated,Description -EA Stop $stale90 = $users | Where-Object { $_.LastLogonDate -and $_.LastLogonDate -lt (Get-Date).AddDays(-90) } | Sort-Object LastLogonDate $neverLogon = $users | Where-Object { -not $_.LastLogonDate } | Sort-Object WhenCreated $sb = [System.Text.StringBuilder]::new() [void]$sb.AppendLine("ENABLED accounts with NO logon in 90+ days: $($stale90.Count)") foreach ($u in ($stale90 | Select-Object -First 20)) { [void]$sb.AppendLine(" $($u.SamAccountName) | Last: $($u.LastLogonDate.ToString('yyyy-MM-dd')) | Created: $($u.WhenCreated.ToString('yyyy-MM-dd'))") } if ($stale90.Count -gt 20) { [void]$sb.AppendLine(" ... and $($stale90.Count - 20) more") } [void]$sb.AppendLine("`nENABLED accounts that have NEVER logged in: $($neverLogon.Count)") foreach ($u in ($neverLogon | Select-Object -First 10)) { [void]$sb.AppendLine(" $($u.SamAccountName) | Created: $($u.WhenCreated.ToString('yyyy-MM-dd'))") } $status = if ($stale90.Count -eq 0 -and $neverLogon.Count -eq 0) {'Pass'} elseif ($stale90.Count -le 5) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="AD stale account scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'IA05' = @{ Type='AD'; Label='Scan Password Policy' Script = { $pol = Get-ADDefaultDomainPasswordPolicy -EA Stop $fgpp = Get-ADFineGrainedPasswordPolicy -Filter * -EA SilentlyContinue $sb = [System.Text.StringBuilder]::new(); $issues = 0 [void]$sb.AppendLine("DEFAULT DOMAIN PASSWORD POLICY (CIS Benchmarks 2025):") # Min Length: CIS requires 14+ [void]$sb.AppendLine(" Min Length : $($pol.MinPasswordLength) $(if($pol.MinPasswordLength -lt 14){'[WEAK - CIS requires 14+]'; $issues++} else {'[OK]'})") # Complexity [void]$sb.AppendLine(" Complexity : $($pol.ComplexityEnabled) $(if(-not $pol.ComplexityEnabled){'[DISABLED]'; $issues++} else {'[OK]'})") # History: CIS requires 24 [void]$sb.AppendLine(" History Count : $($pol.PasswordHistoryCount) $(if($pol.PasswordHistoryCount -lt 24){'[LOW - CIS requires 24]'; $issues++} else {'[OK]'})") # Max Age $maxDays = $pol.MaxPasswordAge.Days $maxFlag = if ($maxDays -eq 0) {'[NEVER EXPIRES - passwords should have max age]'; $issues++} elseif ($maxDays -gt 365) {"[>365d - CIS max 365]"; $issues++} else {'[OK]'} [void]$sb.AppendLine(" Max Age : ${maxDays}d $maxFlag") # Min Age: CIS requires >= 1 day $minDays = $pol.MinPasswordAge.Days [void]$sb.AppendLine(" Min Age : ${minDays}d $(if($minDays -lt 1){'[Should be 1+ day to prevent cycling]'; $issues++} else {'[OK]'})") # Lockout Threshold: CIS requires 1-5 $lockThresh = $pol.LockoutThreshold $lockFlag = if ($lockThresh -eq 0) {'[NO LOCKOUT - CRITICAL!]'; $issues += 2} elseif ($lockThresh -gt 5) {"[HIGH - CIS max 5 attempts]"; $issues++} else {'[OK]'} [void]$sb.AppendLine(" Lockout Thresh : $lockThresh $lockFlag") # Lockout Duration: CIS requires >= 15 min $lockDurMin = $pol.LockoutDuration.TotalMinutes $lockDurFlag = if ($lockThresh -gt 0 -and $lockDurMin -lt 15) {"[SHORT - CIS min 15min]"; $issues++} elseif ($lockThresh -eq 0) {'[N/A - no lockout]'} else {'[OK]'} [void]$sb.AppendLine(" Lockout Duration: $($pol.LockoutDuration) $lockDurFlag") [void]$sb.AppendLine(" Lockout Window : $($pol.LockoutObservationWindow)") # Reversible Encryption: must be disabled [void]$sb.AppendLine(" Reversible Enc : $($pol.ReversibleEncryptionEnabled) $(if($pol.ReversibleEncryptionEnabled){'[CRITICAL - must be disabled!]'; $issues += 2} else {'[OK]'})") # Azure AD Password Protection agent try { $aadPP = Get-Service AzureADPasswordProtectionDCAgent -EA SilentlyContinue if ($aadPP -and $aadPP.Status -eq 'Running') { [void]$sb.AppendLine("`nAzure AD Password Protection: Active [OK]") } else { [void]$sb.AppendLine("`nAzure AD Password Protection: Not detected") } } catch {} if ($fgpp) { [void]$sb.AppendLine("`nFINE-GRAINED PASSWORD POLICIES:") foreach ($f in $fgpp) { [void]$sb.AppendLine(" $($f.Name) | MinLen:$($f.MinPasswordLength) | History:$($f.PasswordHistoryCount) | Complexity:$($f.ComplexityEnabled) | Precedence:$($f.Precedence)") } } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Password policy scan (CIS 2025) @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'IA10' = @{ Type='AD'; Label='Scan Inactive Accounts' Script = { $threshold = (Get-Date).AddDays(-90) $inactive = Get-ADUser -Filter {Enabled -eq $true -and LastLogonDate -lt $threshold} -Properties LastLogonDate,Description -EA Stop | Sort-Object LastLogonDate | Select-Object -First 30 SamAccountName,Name,LastLogonDate,Description $sb = [System.Text.StringBuilder]::new() [void]$sb.AppendLine("Enabled accounts with no logon in 90+ days: $($inactive.Count)+ found") foreach ($u in $inactive) { [void]$sb.AppendLine(" $($u.SamAccountName) | Last: $(if($u.LastLogonDate){$u.LastLogonDate.ToString('yyyy-MM-dd')}else{'Never'})") } $status = if ($inactive.Count -eq 0) {'Pass'} elseif ($inactive.Count -le 5) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Inactive account scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } # ── Endpoint Security ──────────────────────────────────────────────────── 'EP01' = @{ Type='Local'; Label='Scan Defender / Endpoint Protection' Script = { $mp = Get-MpComputerStatus -EA Stop $pref = Get-MpPreference -EA SilentlyContinue $sigDate = $mp.AntivirusSignatureLastUpdated $daysOld = if ($sigDate -is [datetime]) { ((Get-Date) - $sigDate).Days } else { 999 } $sb = [System.Text.StringBuilder]::new(); $issues = 0 [void]$sb.AppendLine("CORE PROTECTION:") [void]$sb.AppendLine(" AV Enabled : $($mp.AntivirusEnabled) $(if(-not $mp.AntivirusEnabled){'[DISABLED!]';$issues++} else {'[OK]'})") [void]$sb.AppendLine(" Real-Time Protect : $($mp.RealTimeProtectionEnabled) $(if(-not $mp.RealTimeProtectionEnabled){'[DISABLED!]';$issues++} else {'[OK]'})") [void]$sb.AppendLine(" Behavior Monitor : $($mp.BehaviorMonitorEnabled) $(if(-not $mp.BehaviorMonitorEnabled){'[DISABLED]';$issues++} else {'[OK]'})") [void]$sb.AppendLine(" Tamper Protection : $($mp.IsTamperProtected) $(if(-not $mp.IsTamperProtected){'[NOT PROTECTED]';$issues++} else {'[OK]'})") [void]$sb.AppendLine(" Signature Age : ${daysOld}d $(if($daysOld -gt 7){'[STALE!]';$issues++} else {'[OK]'})") [void]$sb.AppendLine(" Signature Updated : $(if($sigDate -is [datetime]){$sigDate.ToString('yyyy-MM-dd HH:mm')}else{'Unknown'})") [void]$sb.AppendLine(" Engine Version : $($mp.AMEngineVersion)") [void]$sb.AppendLine(" Product Version : $($mp.AMProductVersion)") # Defender for Endpoint (MDE) / Sense service $mde = Get-Service Sense -EA SilentlyContinue if ($mde -and $mde.Status -eq 'Running') { [void]$sb.AppendLine(" MDE Onboarding : Active (Sense service running) [OK]") } elseif ($mde) { [void]$sb.AppendLine(" MDE Onboarding : Sense service $($mde.Status) [!]") } else { [void]$sb.AppendLine(" MDE Onboarding : Not installed") } # AMSI provider check try { $amsiProviders = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers' -EA SilentlyContinue [void]$sb.AppendLine(" AMSI Providers : $(if($amsiProviders){$amsiProviders.Count}else{0})") } catch {} # ASR (Attack Surface Reduction) rules if ($pref) { [void]$sb.AppendLine("`nATTACK SURFACE REDUCTION (ASR):") $asrIds = $pref.AttackSurfaceReductionRules_Ids $asrActions = $pref.AttackSurfaceReductionRules_Actions if ($asrIds -and $asrIds.Count -gt 0) { $blockCount = 0; $auditCount = 0; $offCount = 0 for ($i = 0; $i -lt $asrIds.Count; $i++) { $action = if ($i -lt $asrActions.Count) { $asrActions[$i] } else { 0 } switch ($action) { 1 { $blockCount++ } 2 { $auditCount++ } 6 { $blockCount++ } default { $offCount++ } } } [void]$sb.AppendLine(" Rules configured : $($asrIds.Count) (Block:$blockCount, Audit:$auditCount, Off:$offCount)") if ($blockCount -eq 0) { $issues++; [void]$sb.AppendLine(" [!] No ASR rules in Block mode") } } else { $issues++; [void]$sb.AppendLine(" No ASR rules configured [!]") } # Controlled Folder Access $cfa = $pref.EnableControlledFolderAccess $cfaStatus = switch ($cfa) { 1 {'Enabled [OK]'} 2 {'Audit Mode'} default {'Disabled [!]'} } [void]$sb.AppendLine(" Controlled Folder : $cfaStatus") # Network Protection $np = $pref.EnableNetworkProtection $npStatus = switch ($np) { 1 {'Enabled (Block) [OK]'} 2 {'Audit Mode'} default {'Disabled [!]'} } [void]$sb.AppendLine(" Network Protection: $npStatus") # PUA Protection $pua = $pref.PUAProtection [void]$sb.AppendLine(" PUA Protection : $(if($pua -eq 1){'Enabled [OK]'}elseif($pua -eq 2){'Audit'}else{'Disabled'})") # Cloud Protection [void]$sb.AppendLine(" Cloud Protection : $(if($mp.CloudEnabled -or $pref.MAPSReporting -gt 0){'Enabled [OK]'}else{'Disabled'})") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Defender + ASR scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP02' = @{ Type='Local'; Label='Scan BitLocker' Script = { $vols = Get-BitLockerVolume -EA Stop $sb = [System.Text.StringBuilder]::new(); $issues = 0 foreach ($v in $vols) { $ok = $v.ProtectionStatus -eq 'On' -and $v.VolumeStatus -eq 'FullyEncrypted' if (-not $ok) { $issues++ } [void]$sb.AppendLine("$($v.MountPoint) | Status:$($v.VolumeStatus) | Protection:$($v.ProtectionStatus) | Method:$($v.EncryptionMethod) $(if($ok){'[OK]'}else{'[ISSUE]'})") } $status = if ($issues -eq 0 -and $vols.Count -gt 0) {'Pass'} elseif ($vols.Count -eq 0) {'Fail'} else {'Partial'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Get-BitLockerVolume @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP03' = @{ Type='Local'; Label='Scan SMB / Protocol Hardening' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # SMBv1 and SMB server configuration try { $cfg = Get-SmbServerConfiguration -EA Stop [void]$sb.AppendLine("SMB SERVER CONFIGURATION:") [void]$sb.AppendLine(" SMB1Protocol : $($cfg.EnableSMB1Protocol) $(if($cfg.EnableSMB1Protocol){'[ENABLED - VULNERABLE!]'; $issues += 2}else{'[OK - Disabled]'})") [void]$sb.AppendLine(" SMB2Protocol : $($cfg.EnableSMB2Protocol)") [void]$sb.AppendLine(" EncryptData : $($cfg.EncryptData) $(if(-not $cfg.EncryptData){'[Should be True for SMB 3.0+ encryption]'; $issues++}else{'[OK]'})") [void]$sb.AppendLine(" RejectUnencrypted: $($cfg.RejectUnencryptedAccess) $(if(-not $cfg.RejectUnencryptedAccess){'[Consider enabling]'}else{'[OK]'})") # SMB Signing [void]$sb.AppendLine(" RequireSign (Srv): $($cfg.RequireSecuritySignature) $(if(-not $cfg.RequireSecuritySignature){'[NOT REQUIRED - relay risk!]'; $issues++}else{'[OK]'})") [void]$sb.AppendLine(" EnableSign (Srv) : $($cfg.EnableSecuritySignature)") } catch { [void]$sb.AppendLine("SmbServerConfiguration: $_"); $issues++ } # SMB Client signing try { $cliSign = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters' -EA SilentlyContinue) if ($cliSign) { [void]$sb.AppendLine(" RequireSign (Cli): $(if($cliSign.RequireSecuritySignature -eq 1){'True [OK]'}else{'False [!]'; $issues++})") } } catch {} # SMB1 Feature State try { $feat = Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -EA Stop [void]$sb.AppendLine(" SMB1 Feature : $($feat.State) $(if($feat.State -eq 'Enabled'){'[STILL INSTALLED]'}else{'[OK - Removed]'})") if ($feat.State -eq 'Enabled') { $issues++ } } catch { [void]$sb.AppendLine(" SMB1 Feature : Not available (may require elevation)") } # NTLM Configuration [void]$sb.AppendLine("`nNTLM / AUTHENTICATION:") try { $lsa = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -EA SilentlyContinue $lmLevel = $lsa.LmCompatibilityLevel $lmDesc = switch ($lmLevel) { 0 {'LM & NTLM'} 1 {'LM & NTLM + NTLMv2 session'} 2 {'NTLM only'} 3 {'NTLMv2 only'} 4 {'NTLMv2 only, refuse LM'} 5 {'NTLMv2 only, refuse LM & NTLM'} default {'Not set (OS default)'} } $lmFlag = if ($lmLevel -ge 5) {'[OK]'} elseif ($lmLevel -ge 3) {'[PARTIAL - CIS recommends 5]'} else {'[WEAK - CIS requires 5 (NTLMv2 only)]'; $issues++} [void]$sb.AppendLine(" LmCompatibility : $lmLevel ($lmDesc) $lmFlag") } catch {} # LLMNR (should be disabled) try { $llmnr = (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' -EA SilentlyContinue).EnableMulticast [void]$sb.AppendLine(" LLMNR : $(if($llmnr -eq 0){'Disabled [OK]'}else{'Enabled/Not configured [!] - poisoning risk'; $issues++})") } catch { [void]$sb.AppendLine(" LLMNR : Could not query") } # NetBIOS over TCP/IP try { $nbAdapters = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=true' -EA SilentlyContinue $nbEnabled = @($nbAdapters | Where-Object { $_.TcpipNetbiosOptions -ne 2 }) [void]$sb.AppendLine(" NetBIOS over TCP : $(if($nbEnabled.Count -eq 0){'Disabled on all adapters [OK]'}else{"Enabled on $($nbEnabled.Count) adapter(s) [!]"; $issues++})") } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="SMB/protocol hardening scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP04' = @{ Type='Local'; Label='Scan Patch Level' Script = { $fixes = Get-HotFix -EA Stop | Sort-Object InstalledOn -Descending -EA SilentlyContinue $latest = $fixes | Select-Object -First 1 $sb = [System.Text.StringBuilder]::new() $daysSince = if ($latest.InstalledOn) { ((Get-Date) - $latest.InstalledOn).Days } else { 999 } [void]$sb.AppendLine("Total patches installed: $($fixes.Count)") [void]$sb.AppendLine("Most recent patch : $($latest.HotFixID) on $(if($latest.InstalledOn){$latest.InstalledOn.ToString('yyyy-MM-dd')}else{'Unknown'}) ($daysSince days ago)") [void]$sb.AppendLine("`nLast 10 patches:") foreach ($h in ($fixes | Select-Object -First 10)) { [void]$sb.AppendLine(" $($h.HotFixID) | $($h.Description) | $(if($h.InstalledOn){$h.InstalledOn.ToString('yyyy-MM-dd')}else{'N/A'})") } $status = if ($daysSince -le 30) {'Pass'} elseif ($daysSince -le 60) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Get-HotFix @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP05' = @{ Type='Local'; Label='Scan Local Admin / Privilege Escalation' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Local admin group members try { $admins = Get-LocalGroupMember -Group 'Administrators' -EA Stop [void]$sb.AppendLine("LOCAL ADMINISTRATORS ($($admins.Count) members):") foreach ($m in $admins) { $concern = $m.Name -match 'Domain Users|Everyone|Authenticated Users|Users' if ($concern) { $issues++ } [void]$sb.AppendLine(" $($m.Name) | Type:$($m.ObjectClass) | Source:$($m.PrincipalSource) $(if($concern){'[BROAD ACCESS!]'})") } } catch { $raw = net localgroup Administrators 2>&1 | Where-Object { $_ -and $_ -notmatch '^(The command|Members|---|-$|Alias)' } [void]$sb.AppendLine("LOCAL ADMINISTRATORS:") foreach ($r in $raw) { if ($r.Trim()) { [void]$sb.AppendLine(" $($r.Trim())") } } } # Unquoted service paths (privilege escalation) [void]$sb.AppendLine("`nUNQUOTED SERVICE PATHS:") try { $services = Get-CimInstance Win32_Service -EA SilentlyContinue | Where-Object { $_.PathName -and $_.PathName -notmatch '^"' -and $_.PathName -match '\s' -and $_.PathName -notmatch '^[A-Za-z]:\\Windows\\' } if ($services) { foreach ($svc in ($services | Select-Object -First 10)) { $issues++ [void]$sb.AppendLine(" [!] $($svc.Name): $($svc.PathName)") } if ($services.Count -gt 10) { [void]$sb.AppendLine(" ... +$($services.Count - 10) more") } } else { [void]$sb.AppendLine(" None found [OK]") } } catch { [void]$sb.AppendLine(" Could not query services") } # AlwaysInstallElevated (privesc via MSI) [void]$sb.AppendLine("`nALWAYS INSTALL ELEVATED:") try { $aieLM = (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Installer' -Name AlwaysInstallElevated -EA SilentlyContinue).AlwaysInstallElevated $aieCU = (Get-ItemProperty 'HKCU:\SOFTWARE\Policies\Microsoft\Windows\Installer' -Name AlwaysInstallElevated -EA SilentlyContinue).AlwaysInstallElevated if ($aieLM -eq 1 -and $aieCU -eq 1) { $issues += 2; [void]$sb.AppendLine(" [CRITICAL] AlwaysInstallElevated enabled in BOTH HKLM+HKCU - any user can install as SYSTEM!") } elseif ($aieLM -eq 1 -or $aieCU -eq 1) { $issues++; [void]$sb.AppendLine(" [!] AlwaysInstallElevated partially set (HKLM=$aieLM, HKCU=$aieCU)") } else { [void]$sb.AppendLine(" Not enabled [OK]") } } catch { [void]$sb.AppendLine(" Could not query") } # Cached logon count try { $cached = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -EA SilentlyContinue).CachedLogonsCount [void]$sb.AppendLine("`nCached Logons: $cached $(if([int]$cached -gt 4){'[HIGH - CIS recommends 4 or less]'}else{'[OK]'})") } catch {} # Token privileges: SeImpersonatePrivilege on service accounts (Potato attacks) [void]$sb.AppendLine("`nDANGEROUS TOKEN PRIVILEGES:") try { $dangerousPrivs = @('SeImpersonatePrivilege','SeAssignPrimaryTokenPrivilege','SeTcbPrivilege','SeDebugPrivilege','SeLoadDriverPrivilege','SeRestorePrivilege','SeTakeOwnershipPrivilege') $svcAccounts = Get-CimInstance Win32_Service -EA SilentlyContinue | Where-Object { $_.StartName -and $_.StartName -notmatch '^(LocalSystem|NT AUTHORITY|NT SERVICE|LocalService|NetworkService)$' -and $_.State -eq 'Running' } | Select-Object Name,StartName -Unique if ($svcAccounts) { foreach ($svc in ($svcAccounts | Select-Object -First 15)) { # Services running as domain/local user accounts with SeImpersonate are Potato-exploitable [void]$sb.AppendLine(" Service: $($svc.Name) runs as $($svc.StartName)") } [void]$sb.AppendLine(" [i] $($svcAccounts.Count) services run as non-built-in accounts - verify these don't have SeImpersonatePrivilege") if ($svcAccounts.Count -gt 3) { $issues++ } } else { [void]$sb.AppendLine(" All services run as built-in accounts [OK]") } # Check current process token for dangerous privs (shows what this admin session has) $myPrivs = whoami /priv /fo csv 2>&1 | ConvertFrom-Csv -EA SilentlyContinue if ($myPrivs) { $dangerFound = $myPrivs | Where-Object { $_.'Privilege Name' -in $dangerousPrivs -and $_.State -eq 'Enabled' } if ($dangerFound) { [void]$sb.AppendLine(" Current session elevated privileges:") foreach ($dp in $dangerFound) { [void]$sb.AppendLine(" $($_.'Privilege Name') = Enabled") } } } } catch { [void]$sb.AppendLine(" Token privilege scan: Could not query") } # DLL search order hijacking: writable directories in system PATH [void]$sb.AppendLine("`nDLL SEARCH ORDER HIJACKING:") try { $systemPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') -split ';' | Where-Object { $_ } $writablePaths = @() foreach ($dir in $systemPath) { if (-not (Test-Path $dir -EA SilentlyContinue)) { continue } # Skip Windows and Program Files (normally protected) if ($dir -match '^[A-Za-z]:\\(Windows|Program Files)') { continue } try { $acl = Get-Acl $dir -EA SilentlyContinue $builtinUsers = $acl.Access | Where-Object { $_.IdentityReference -match 'BUILTIN\\Users|Everyone|Authenticated Users' -and $_.FileSystemRights -match 'Write|FullControl|Modify' -and $_.AccessControlType -eq 'Allow' } if ($builtinUsers) { $writablePaths += $dir } } catch {} } if ($writablePaths.Count -gt 0) { $issues += $writablePaths.Count [void]$sb.AppendLine(" [!] Writable directories in system PATH (DLL hijack risk):") foreach ($wp in ($writablePaths | Select-Object -First 10)) { [void]$sb.AppendLine(" $wp") } } else { [void]$sb.AppendLine(" No user-writable directories in system PATH [OK]") } } catch { [void]$sb.AppendLine(" PATH analysis: Could not check") } $status = if ($issues -eq 0 -and $admins.Count -le 3) {'Pass'} elseif ($issues -gt 2) {'Fail'} else {'Partial'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Local admin + privesc scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP06' = @{ Type='Local'; Label='Scan Host Firewall + Attack Surface' Script = { $profiles = Get-NetFirewallProfile -EA Stop $sb = [System.Text.StringBuilder]::new(); $issues = 0 [void]$sb.AppendLine("FIREWALL PROFILES:") foreach ($p in $profiles) { $ok = $p.Enabled if (-not $ok) { $issues++ } $outBlock = $p.DefaultOutboundAction -eq 'Block' [void]$sb.AppendLine(" $($p.Name): Enabled=$($p.Enabled) | InDefault=$($p.DefaultInboundAction) | OutDefault=$($p.DefaultOutboundAction) $(if(-not $ok){'[DISABLED!]'}elseif($outBlock){'[OK - outbound filtered]'}else{'[Outbound=Allow]'})") # CIS: Firewall log size >= 16,384 KB per profile $logSize = $p.LogMaxSizeKilobytes $logOk = $logSize -ge 16384 if (-not $logOk -and $ok) { $issues++ } [void]$sb.AppendLine(" Log Size: ${logSize} KB $(if($logOk){'[OK]'}else{"[LOW - CIS requires 16,384+ KB]"}) | LogBlocked: $($p.LogBlocked) | LogAllowed: $($p.LogAllowed)") } # Inbound allow rule summary $inboundAllow = Get-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -EA SilentlyContinue | Measure-Object [void]$sb.AppendLine("`nInbound Allow rules (enabled): $($inboundAllow.Count)") # High-risk inbound ports check $riskyPorts = @(21,23,69,135,139,445,1433,3389,5900,5985,5986) $riskyOpen = @() foreach ($port in $riskyPorts) { $found = Get-NetFirewallPortFilter -EA SilentlyContinue | Where-Object { $_.LocalPort -eq $port } | Get-NetFirewallRule -EA SilentlyContinue | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' } if ($found) { $riskyOpen += $port } } if ($riskyOpen.Count -gt 0) { [void]$sb.AppendLine("[!] High-risk inbound ports allowed: $($riskyOpen -join ', ')") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Host firewall + attack surface scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP09' = @{ Type='Local'; Label='Scan AutoRun/AutoPlay' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 $paths = @( @{Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer';Scope='Machine'} @{Path='HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer';Scope='User'} ) foreach ($p in $paths) { try { $val = (Get-ItemProperty $p.Path -Name NoDriveTypeAutoRun -EA Stop).NoDriveTypeAutoRun $disabled = $val -eq 255 if (-not $disabled) { $issues++ } [void]$sb.AppendLine("$($p.Scope) NoDriveTypeAutoRun: $val $(if($disabled){'[OK - All disabled]'}else{'[NOT FULLY DISABLED]'})") } catch { $issues++ [void]$sb.AppendLine("$($p.Scope) NoDriveTypeAutoRun: NOT SET [AutoRun may be enabled]") } } try { $gp = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer' -Name NoAutorun -EA SilentlyContinue [void]$sb.AppendLine("NoAutorun policy : $(if($gp.NoAutorun -eq 1){'Enabled [OK]'}else{'Not set'})") } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -eq 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="AutoRun registry scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP10' = @{ Type='AD'; Label='Scan EOL Operating Systems' Script = { $comps = Get-ADComputer -Filter {Enabled -eq $true} -Properties OperatingSystem,OperatingSystemVersion,LastLogonDate -EA Stop $eolPatterns = @('Windows XP','Windows Vista','Windows 7','Windows 8','Server 2003','Server 2008','Server 2012') $sb = [System.Text.StringBuilder]::new(); $eolCount = 0 $grouped = $comps | Group-Object OperatingSystem | Sort-Object Count -Descending [void]$sb.AppendLine("OS DISTRIBUTION ($($comps.Count) total computers):") foreach ($g in $grouped) { $isEOL = $eolPatterns | Where-Object { $g.Name -match [regex]::Escape($_) } if ($isEOL) { $eolCount += $g.Count } [void]$sb.AppendLine(" $($g.Count.ToString().PadLeft(4)) x $($g.Name) $(if($isEOL){'[END OF LIFE!]'})") } if ($eolCount -gt 0) { [void]$sb.AppendLine("`nEOL SYSTEMS ($eolCount):") $eolSystems = $comps | Where-Object { $eolPatterns | Where-Object { $_.OperatingSystem -match [regex]::Escape($_) } } | Select-Object -First 20 foreach ($c in $eolSystems) { [void]$sb.AppendLine(" $($c.Name) | $($c.OperatingSystem) | Last logon: $(if($c.LastLogonDate){$c.LastLogonDate.ToString('yyyy-MM-dd')}else{'Never'})") } } $status = if ($eolCount -eq 0) {'Pass'} elseif ($eolCount -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="AD computer OS scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } # ── Logging & Monitoring ───────────────────────────────────────────────── 'LM03' = @{ Type='Local'; Label='Scan Audit Policy + PowerShell Logging' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check legacy audit policy override prerequisite try { $scenoLegacy = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -EA SilentlyContinue).SCENoApplyLegacyAuditPolicy if ($scenoLegacy -ne 1) { [void]$sb.AppendLine("[!] SCENoApplyLegacyAuditPolicy not set - advanced audit may be overridden by basic policy") } } catch {} # Parse audit policy via CSV for reliable subcategory checking [void]$sb.AppendLine("ADVANCED AUDIT POLICY (CIS Required Subcategories):") try { $csvRaw = auditpol /get /category:* /r 2>&1 $auditData = $csvRaw | ConvertFrom-Csv -EA Stop # CIS-required subcategories and their minimum setting $cisRequired = @{ 'Credential Validation' = 'Success and Failure' 'Application Group Management' = 'Success and Failure' 'Security Group Management' = 'Success and Failure' 'User Account Management' = 'Success and Failure' 'Computer Account Management' = 'Success' 'Logon' = 'Success and Failure' 'Logoff' = 'Success' 'Account Lockout' = 'Failure' 'Special Logon' = 'Success' 'Audit Policy Change' = 'Success' 'Authentication Policy Change' = 'Success' 'Sensitive Privilege Use' = 'Success and Failure' 'System Integrity' = 'Success and Failure' 'Security State Change' = 'Success' 'Security System Extension' = 'Success' 'Other Object Access Events' = 'Success and Failure' 'Removable Storage' = 'Success and Failure' 'Process Creation' = 'Success' } foreach ($sub in $cisRequired.Keys) { $entry = $auditData | Where-Object { $_.Subcategory -match [regex]::Escape($sub) } | Select-Object -First 1 if ($entry) { $setting = $entry.'Inclusion Setting' $noAudit = $setting -match 'No Auditing' if ($noAudit) { $issues++; [void]$sb.AppendLine(" [!] $sub : No Auditing [FAIL]") } } } $noAuditCount = ($auditData | Where-Object { $_.'Inclusion Setting' -match 'No Auditing' }).Count $totalSubs = $auditData.Count [void]$sb.AppendLine(" Subcategories audited: $($totalSubs - $noAuditCount)/$totalSubs") } catch { # Fallback to text parsing $raw = auditpol /get /category:* 2>&1 $critical = @('Logon','Account Logon','Account Management','Object Access','Policy Change','Privilege Use') $current = '' foreach ($line in $raw) { $l = $line.ToString().Trim() if ($l -match '^[A-Z]') { $current = $l } if ($l -match 'No Auditing' -and ($critical | Where-Object { $current -match $_ })) { $issues++; [void]$sb.AppendLine(" [!] $current > $l") } } } if ($issues -eq 0) { [void]$sb.AppendLine(" All CIS-required subcategories have auditing enabled. [OK]") } # PowerShell logging [void]$sb.AppendLine("`nPOWERSHELL LOGGING:") try { $psScriptBlock = (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -EA SilentlyContinue).EnableScriptBlockLogging $psModule = (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' -EA SilentlyContinue).EnableModuleLogging $psTranscript = (Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -EA SilentlyContinue).EnableTranscripting [void]$sb.AppendLine(" Script Block Logging : $(if($psScriptBlock -eq 1){'Enabled [OK]'}else{'Disabled [!]'; $issues++})") [void]$sb.AppendLine(" Module Logging : $(if($psModule -eq 1){'Enabled [OK]'}else{'Disabled [!]'; $issues++})") [void]$sb.AppendLine(" Transcription : $(if($psTranscript -eq 1){'Enabled [OK]'}else{'Disabled'})") } catch {} # Command-line process auditing try { $cmdLine = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit' -EA SilentlyContinue).ProcessCreationIncludeCmdLine_Enabled [void]$sb.AppendLine(" Cmd-line in 4688 : $(if($cmdLine -eq 1){'Enabled [OK]'}else{'Disabled [!]'; $issues++})") } catch { [void]$sb.AppendLine(" Cmd-line in 4688 : Not configured") } # PowerShell v2 engine (AMSI bypass risk) try { $psv2 = (Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2 -EA SilentlyContinue).State [void]$sb.AppendLine(" PowerShell v2 Engine : $(if($psv2 -eq 'Enabled'){'Installed [!] - AMSI bypass risk, remove if not needed'; $issues++}else{'Removed [OK]'})") } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Audit policy + PS logging scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'LM05' = @{ Type='Local'; Label='Scan Failed Logons' Script = { $sb = [System.Text.StringBuilder]::new() try { $events = Get-WinEvent -FilterHashtable @{LogName='Security';Id=4625;StartTime=(Get-Date).AddDays(-7)} -MaxEvents 100 -EA Stop [void]$sb.AppendLine("Failed logon events (4625) in last 7 days: $($events.Count)+ found") $byUser = $events | ForEach-Object { $xml = [xml]$_.ToXml(); $user = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text' $ip = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' }).'#text' [PSCustomObject]@{User=$user;IP=$ip} } | Group-Object User | Sort-Object Count -Descending | Select-Object -First 10 [void]$sb.AppendLine("`nTop accounts by failed attempts:") foreach ($u in $byUser) { [void]$sb.AppendLine(" $($u.Count.ToString().PadLeft(4))x $($u.Name)") } $highCount = ($byUser | Where-Object { $_.Count -ge 20 }).Count $status = if ($events.Count -le 10) {'Pass'} elseif ($highCount -gt 0) {'Fail'} else {'Partial'} } catch { [void]$sb.AppendLine("No failed logon events found in last 7 days (or access denied)") $status = 'Pass' } @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Event 4625 scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } # ── Common Findings ────────────────────────────────────────────────────── 'CF01' = @{ Type='AD'; Label='Scan Privileged Service Accounts' Script = { $da = Get-ADGroupMember 'Domain Admins' -Recursive -EA Stop | Where-Object { $_.objectClass -eq 'user' } $sb = [System.Text.StringBuilder]::new(); $issues = 0 foreach ($m in $da) { $u = Get-ADUser $m -Properties PasswordLastSet,ServicePrincipalName,PasswordNeverExpires -EA SilentlyContinue if (-not $u) { continue } $isSvc = $u.SamAccountName -match 'svc|service|sql|backup' -or $u.ServicePrincipalName if ($isSvc) { $age = if ($u.PasswordLastSet) { ((Get-Date) - $u.PasswordLastSet).Days } else { 9999 } $issues++ [void]$sb.AppendLine("[!] $($u.SamAccountName) | DA + Service Account | PW Age: ${age}d | NeverExpires: $($u.PasswordNeverExpires)") } } if ($issues -eq 0) { [void]$sb.AppendLine("No service accounts found in Domain Admins. Good.") } # gMSA adoption check try { $gmsa = Get-ADServiceAccount -Filter * -Properties PasswordLastSet,Enabled,PrincipalsAllowedToRetrieveManagedPassword -EA SilentlyContinue $gmsaCount = ($gmsa | Measure-Object).Count [void]$sb.AppendLine("`ngMSA ACCOUNTS: $gmsaCount found") if ($gmsaCount -gt 0) { foreach ($g in ($gmsa | Select-Object -First 10)) { [void]$sb.AppendLine(" $($g.SamAccountName) | Enabled:$($g.Enabled)") } } else { [void]$sb.AppendLine(" No gMSA accounts - consider migrating service accounts to gMSA for automatic password rotation") } } catch {} # GPP password check (cpassword in SYSVOL) if ($script:Env.DomainName) { try { $sysvolPath = "\\$($script:Env.DomainName)\SYSVOL\$($script:Env.DomainName)\Policies" $gppFiles = @('Groups.xml','Services.xml','Scheduledtasks.xml','DataSources.xml','Drives.xml') $gppFound = @() foreach ($gppFile in $gppFiles) { $matches = Get-ChildItem $sysvolPath -Recurse -Filter $gppFile -EA SilentlyContinue | Select-String 'cpassword' -EA SilentlyContinue if ($matches) { $gppFound += "$gppFile ($($matches.Count) entries)" } } if ($gppFound.Count -gt 0) { $issues += 2 [void]$sb.AppendLine("`n[CRITICAL] GPP PASSWORDS IN SYSVOL (trivially decryptable):") foreach ($g in $gppFound) { [void]$sb.AppendLine(" $g") } } else { [void]$sb.AppendLine("`nGPP Passwords: None found in SYSVOL [OK]") } } catch { [void]$sb.AppendLine("`nGPP Passwords: Could not scan SYSVOL") } } # ADCS (Active Directory Certificate Services) vulnerability scan if ($script:Env.HasAD) { [void]$sb.AppendLine("`nAD CERTIFICATE SERVICES (ADCS):") try { $configNC = (Get-ADRootDSE -EA Stop).configurationNamingContext # Find Enterprise CAs $cas = Get-ADObject -SearchBase "CN=Enrollment Services,CN=Public Key Services,CN=Services,$configNC" -Filter {objectClass -eq 'pKIEnrollmentService'} -Properties * -EA SilentlyContinue if ($cas) { foreach ($ca in $cas) { [void]$sb.AppendLine(" CA: $($ca.Name) | DNS: $($ca.dNSHostName)") } # ESC1: Templates allowing SAN (Subject Alternative Name) from requester $templates = Get-ADObject -SearchBase "CN=Certificate Templates,CN=Public Key Services,CN=Services,$configNC" -Filter {objectClass -eq 'pKICertificateTemplate'} -Properties * -EA SilentlyContinue $esc1Count = 0; $esc6Count = 0 foreach ($tmpl in $templates) { # ESC1: CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT (bit 0x00000001) in msPKI-Certificate-Name-Flag $nameFlag = $tmpl.'msPKI-Certificate-Name-Flag' $enrolleeSAN = ($nameFlag -band 1) -eq 1 # Check if template allows client auth EKU $ekus = $tmpl.'pKIExtendedKeyUsage' $clientAuth = $ekus -contains '1.3.6.1.5.5.7.3.2' # Client Authentication $anyPurpose = $ekus -contains '2.5.29.37.0' # Any Purpose $hasAuthEKU = $clientAuth -or $anyPurpose -or ($ekus.Count -eq 0) if ($enrolleeSAN -and $hasAuthEKU) { $esc1Count++ if ($esc1Count -le 5) { [void]$sb.AppendLine(" [CRITICAL] ESC1: $($tmpl.Name) - enrollee supplies SAN + client auth") } } # ESC6: EDITF_ATTRIBUTESUBJECTALTNAME2 flag (checked at CA level below) } if ($esc1Count -gt 5) { [void]$sb.AppendLine(" ... +$($esc1Count - 5) more ESC1 templates") } if ($esc1Count -gt 0) { $issues += 2 } else { [void]$sb.AppendLine(" ESC1 (enrollee SAN + auth): None found [OK]") } # ESC6: Check CA for EDITF_ATTRIBUTESUBJECTALTNAME2 via registry (if local CA) try { $caEditFlags = (certutil -getreg policy\EditFlags 2>&1) -join ' ' if ($caEditFlags -match 'EDITF_ATTRIBUTESUBJECTALTNAME2') { $issues += 2 [void]$sb.AppendLine(" [CRITICAL] ESC6: EDITF_ATTRIBUTESUBJECTALTNAME2 enabled on local CA - any cert can specify SAN!") } else { [void]$sb.AppendLine(" ESC6 (SAN edit flag on CA): Not detected on this host [OK*] (*check all CA servers)") } } catch { [void]$sb.AppendLine(" ESC6: Could not check (not a CA server or certutil unavailable)") } # ESC8: HTTP enrollment endpoints (NTLM relay to web enrollment) try { $webEnroll = Get-Service CertSvc -EA SilentlyContinue if ($webEnroll -and $webEnroll.Status -eq 'Running') { $httpBinding = netsh http show sslcert 2>&1 | Select-String 'certsrv' -EA SilentlyContinue if (-not $httpBinding) { $issues++ [void]$sb.AppendLine(" [HIGH] ESC8: Certificate web enrollment may be on HTTP (NTLM relay risk)") } } } catch {} # ESC10: Certificate mapping - StrongCertificateBindingEnforcement try { $certBinding = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Kdc' -EA SilentlyContinue).StrongCertificateBindingEnforcement $cbStatus = switch ($certBinding) { 0 {'Disabled [CRITICAL - vulnerable to ESC10]'; $issues += 2} 1 {'Compatibility mode [!]'; $issues++} 2 {'Full enforcement [OK]'} default {'Not set (default behavior)'} } [void]$sb.AppendLine(" ESC10 (StrongCertificateBinding): $cbStatus") } catch {} } else { [void]$sb.AppendLine(" No Enterprise CAs found in AD") } } catch { [void]$sb.AppendLine(" ADCS scan: Could not query ($($_.Exception.Message))") } # LDAP Signing + Channel Binding (DC relay protection) [void]$sb.AppendLine("`nLDAP SIGNING / CHANNEL BINDING:") try { $ldapSigning = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' -EA SilentlyContinue).LDAPServerIntegrity $ldapSignStatus = switch ($ldapSigning) { 0 {'None [CRITICAL - unsigned LDAP allowed]'; $issues += 2} 1 {'Negotiated (default)'} 2 {'Required [OK]'} default {'Not set (default=negotiated)'} } [void]$sb.AppendLine(" LDAP Server Signing: $ldapSignStatus") } catch { [void]$sb.AppendLine(" LDAP Signing: Could not query (may not be a DC)") } try { $channelBind = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' -EA SilentlyContinue).LdapEnforceChannelBinding $cbStatus = switch ($channelBind) { 0 {'Never [!] - LDAP relay vulnerable'; $issues++} 1 {'When supported'} 2 {'Always [OK]'} default {'Not set (default=never)'; $issues++} } [void]$sb.AppendLine(" LDAP Channel Binding: $cbStatus") } catch {} # DSRM (Directory Services Restore Mode) admin logon behavior try { $dsrm = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -EA SilentlyContinue).DsrmAdminLogonBehavior if ($dsrm -eq 2) { $issues += 2 [void]$sb.AppendLine(" [CRITICAL] DsrmAdminLogonBehavior=2: DSRM account can logon at any time (DC persistence backdoor!)") } elseif ($dsrm -eq 1) { $issues++ [void]$sb.AppendLine(" [!] DsrmAdminLogonBehavior=1: DSRM account can logon when NTDS service is stopped") } else { [void]$sb.AppendLine(" DsrmAdminLogonBehavior: $dsrm (default - DSRM only in restore mode) [OK]") } } catch {} } $status = if ($issues -eq 0) {'Pass'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="DA svc + gMSA + GPP + ADCS + LDAP scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'CF07' = @{ Type='Local'; Label='Scan Local Admin Rights' Script = { $sb = [System.Text.StringBuilder]::new(); $broadAccess = $false try { $admins = Get-LocalGroupMember -Group 'Administrators' -EA Stop [void]$sb.AppendLine("Local Administrators ($($admins.Count) members):") foreach ($m in $admins) { $broad = $m.Name -match 'Domain Users|Everyone|Authenticated Users|^Users$' if ($broad) { $broadAccess = $true } [void]$sb.AppendLine(" $($m.Name) ($($m.ObjectClass)) $(if($broad){'[OVERLY BROAD!]'})") } } catch { [void]$sb.AppendLine("Could not enumerate local admins: $_") } $status = if ($broadAccess) {'Fail'} elseif ($admins.Count -gt 4) {'Partial'} else {'Pass'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Local admin audit @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } # ── Phase 2 Auto-Checks ────────────────────────────────────────────────── 'NP01' = @{ Type='Local'; Label='Scan Firewall Rules' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 $rules = Get-NetFirewallRule -Enabled True -EA Stop $inbound = $rules | Where-Object { $_.Direction -eq 'Inbound' } $outbound = $rules | Where-Object { $_.Direction -eq 'Outbound' } [void]$sb.AppendLine("FIREWALL RULES SUMMARY:") [void]$sb.AppendLine(" Total enabled : $($rules.Count)") [void]$sb.AppendLine(" Inbound Allow : $(($inbound | Where-Object Action -eq 'Allow').Count)") [void]$sb.AppendLine(" Inbound Block : $(($inbound | Where-Object Action -eq 'Block').Count)") [void]$sb.AppendLine(" Outbound Allow : $(($outbound | Where-Object Action -eq 'Allow').Count)") [void]$sb.AppendLine(" Outbound Block : $(($outbound | Where-Object Action -eq 'Block').Count)") # Check for any/any rules (inbound allow with no port restriction) $anyAny = @() foreach ($r in ($inbound | Where-Object Action -eq 'Allow')) { $ports = ($r | Get-NetFirewallPortFilter -EA SilentlyContinue) $addr = ($r | Get-NetFirewallAddressFilter -EA SilentlyContinue) if ($ports.LocalPort -eq 'Any' -and $addr.RemoteAddress -eq 'Any') { $anyAny += $r; $issues++ } } if ($anyAny.Count -gt 0) { [void]$sb.AppendLine("`n[!] INBOUND ANY/ANY ALLOW RULES ($($anyAny.Count)):") foreach ($a in ($anyAny | Select-Object -First 15)) { [void]$sb.AppendLine(" $($a.DisplayName) | Profile:$($a.Profile) | Program:$($a.Program)") } } else { [void]$sb.AppendLine("`nNo inbound any/any allow rules found. Good.") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Get-NetFirewallRule scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'IA07' = @{ Type='AD'; Label='Scan Shared/Generic Accounts' Script = { $patterns = @('shared','generic','admin','scanner','reception','front*desk','warehouse','conference','kiosk','training','test','temp') $sb = [System.Text.StringBuilder]::new(); $found = 0 foreach ($p in $patterns) { $accts = Get-ADUser -Filter "SamAccountName -like '*$p*' -or Name -like '*$p*'" -Properties Enabled,LastLogonDate,Description,PasswordLastSet -EA SilentlyContinue foreach ($a in $accts) { $found++ $age = if ($a.PasswordLastSet) { ((Get-Date) - $a.PasswordLastSet).Days } else { 9999 } [void]$sb.AppendLine("$($a.SamAccountName) | Enabled:$($a.Enabled) | PW Age:${age}d | Last:$(if($a.LastLogonDate){$a.LastLogonDate.ToString('yyyy-MM-dd')}else{'Never'}) | Desc:$($a.Description)") } } if ($found -eq 0) { [void]$sb.AppendLine("No shared/generic accounts found by pattern matching.") } else { [void]$sb.Insert(0, "Potential shared/generic accounts ($found found):`n") } $status = if ($found -eq 0) {'Pass'} elseif ($found -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="AD shared account scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'IA08' = @{ Type='AD'; Label='Scan Guest/Vendor Accounts' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 $patterns = @('vendor','contractor','consultant','extern','guest','partner','3rdparty','thirdparty') $vendorAccts = @() foreach ($p in $patterns) { $vendorAccts += Get-ADUser -Filter "SamAccountName -like '*$p*' -or Name -like '*$p*' -or Description -like '*$p*'" -Properties Enabled,AccountExpirationDate,LastLogonDate,Description -EA SilentlyContinue } $vendorAccts = $vendorAccts | Sort-Object -Property SamAccountName -Unique # Also find accounts WITH expiration dates (common for vendors) $expiring = Get-ADUser -Filter {AccountExpirationDate -ne "$null"} -Properties AccountExpirationDate,Enabled,LastLogonDate -EA SilentlyContinue [void]$sb.AppendLine("VENDOR/GUEST ACCOUNTS BY NAME ($($vendorAccts.Count) found):") foreach ($a in $vendorAccts) { $noExpiry = -not $a.AccountExpirationDate $expired = $a.AccountExpirationDate -and $a.AccountExpirationDate -lt (Get-Date) $flags = @() if ($a.Enabled -and $noExpiry) { $flags += 'NO_EXPIRY'; $issues++ } if ($a.Enabled -and $expired) { $flags += 'EXPIRED_BUT_ENABLED'; $issues++ } $f = if ($flags) { " [$(($flags -join ', '))]" } else { '' } [void]$sb.AppendLine(" $($a.SamAccountName) | Enabled:$($a.Enabled) | Expires:$(if($a.AccountExpirationDate){$a.AccountExpirationDate.ToString('yyyy-MM-dd')}else{'NEVER'})$f") } [void]$sb.AppendLine("`nACCOUNTS WITH EXPIRATION DATES: $($expiring.Count)") $status = if ($issues -eq 0 -and $vendorAccts.Count -eq 0) {'Pass'} elseif ($issues -eq 0) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="AD guest/vendor scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'EP07' = @{ Type='Local'; Label='Scan Application Control + Macro Policy' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # AppLocker try { $policy = Get-AppLockerPolicy -Effective -EA Stop $rules = $policy.RuleCollections [void]$sb.AppendLine("APPLOCKER POLICY DETECTED:") foreach ($rc in $rules) { [void]$sb.AppendLine(" Collection: $($rc.RuleCollectionType) ($($rc.Count) rules)") foreach ($r in ($rc | Select-Object -First 5)) { [void]$sb.AppendLine(" $($r.Name) | Action:$($r.Action) | Type:$($r.GetType().Name)") } if ($rc.Count -gt 5) { [void]$sb.AppendLine(" ... +$($rc.Count - 5) more") } } } catch { [void]$sb.AppendLine("AppLocker: NOT configured on this system.") $issues++ } # WDAC (Windows Defender Application Control) - check for active code integrity policy [void]$sb.AppendLine("`nWDAC / CODE INTEGRITY:") try { $ciPolicy = Get-CimInstance -ClassName Win32_DeviceGuard -Namespace 'root\Microsoft\Windows\DeviceGuard' -EA SilentlyContinue if ($ciPolicy) { $ciStatus = $ciPolicy.CodeIntegrityPolicyEnforcementStatus $umci = $ciPolicy.UsermodeCodeIntegrityPolicyEnforcementStatus [void]$sb.AppendLine(" Kernel CI Enforcement: $(switch($ciStatus){ 0 {'Off'} 1 {'Audit'} 2 {'Enforced [OK]'} default {'Unknown'} })") [void]$sb.AppendLine(" User-mode CI (UMCI) : $(switch($umci){ 0 {'Off'} 1 {'Audit'} 2 {'Enforced [OK]'} default {'Unknown'} })") } } catch { [void]$sb.AppendLine(" WDAC: Could not query DeviceGuard WMI") } # Check for CI policy files try { $sipolicy = Test-Path "$env:SystemRoot\System32\CodeIntegrity\SIPolicy.p7b" -EA SilentlyContinue $cipolicies = Get-ChildItem "$env:SystemRoot\System32\CodeIntegrity\CIPolicies\Active\*.cip" -EA SilentlyContinue if ($sipolicy -or $cipolicies) { [void]$sb.AppendLine(" CI Policy files found: SIPolicy.p7b=$sipolicy, CIP files=$($cipolicies.Count)") } else { [void]$sb.AppendLine(" No WDAC policy files deployed") } } catch {} # Office macro restrictions (VBA Warnings via GPO registry) [void]$sb.AppendLine("`nOFFICE MACRO RESTRICTIONS:") $officeVersions = @('16.0','15.0') # Office 2016/365, Office 2013 $officeApps = @( @{App='Word'; Key='word'}; @{App='Excel'; Key='excel'} @{App='PowerPoint'; Key='powerpoint'}; @{App='Outlook'; Key='outlook'} ) $macroConfigured = $false; $macroBlocked = $false foreach ($ver in $officeVersions) { foreach ($app in $officeApps) { $policyPath = "HKCU:\SOFTWARE\Policies\Microsoft\Office\$ver\$($app.Key)\security" $vbaWarning = (Get-ItemProperty $policyPath -Name 'VBAWarnings' -EA SilentlyContinue).VBAWarnings $blockExec = (Get-ItemProperty $policyPath -Name 'blockcontentexecutionfrominternet' -EA SilentlyContinue).blockcontentexecutionfrominternet if ($vbaWarning) { $macroConfigured = $true $vbaDesc = switch ($vbaWarning) { 1 {'Enable all (DANGEROUS!)'} 2 {'Disable with notification (default)'} 3 {'Disable except digitally signed'} 4 {'Disable all [SECURE]'} default {"Unknown ($vbaWarning)"} } if ($vbaWarning -le 1) { $issues++ } if ($vbaWarning -ge 3) { $macroBlocked = $true } [void]$sb.AppendLine(" $($app.App) $ver VBAWarnings: $vbaDesc") } if ($blockExec -eq 1) { $macroBlocked = $true } } } # Machine-level macro policy (GPO: Computer Config) foreach ($ver in $officeVersions) { foreach ($app in $officeApps) { $machinePath = "HKLM:\SOFTWARE\Policies\Microsoft\Office\$ver\$($app.Key)\security" $vbaWarning = (Get-ItemProperty $machinePath -Name 'VBAWarnings' -EA SilentlyContinue).VBAWarnings if ($vbaWarning) { $macroConfigured = $true $vbaDesc = switch ($vbaWarning) { 1 {'Enable all (DANGEROUS!)'} 2 {'Disable with notification'} 3 {'Disable except signed'} 4 {'Disable all [SECURE]'} default {"Unknown"} } if ($vbaWarning -ge 3) { $macroBlocked = $true } [void]$sb.AppendLine(" $($app.App) $ver (Machine): $vbaDesc") } } } # Block macros from internet (Office 2016+ feature) foreach ($ver in $officeVersions) { foreach ($app in $officeApps) { foreach ($hive in @('HKCU:','HKLM:')) { $blockInet = (Get-ItemProperty "$hive\SOFTWARE\Policies\Microsoft\Office\$ver\$($app.Key)\security" -Name 'blockcontentexecutionfrominternet' -EA SilentlyContinue).blockcontentexecutionfrominternet if ($blockInet -eq 1) { $macroBlocked = $true [void]$sb.AppendLine(" $($app.App) ${ver}: Block macros from internet = Enabled [OK]") break } } } } if (-not $macroConfigured) { $issues++ [void]$sb.AppendLine(" No Office macro GPO restrictions detected [!]") [void]$sb.AppendLine(" Recommend: Set VBAWarnings=4 or blockcontentexecutionfrominternet=1 via GPO") } if ($issues -eq 0 -and $macroBlocked) { $status = 'Pass' } elseif ($issues -eq 0) { $status = 'Partial' } else { $status = 'Fail' } @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="AppLocker + WDAC + macro scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'LM01' = @{ Type='Local'; Label='Scan DNS Logging' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check if DNS Server role is installed try { $dns = Get-DnsServerDiagnostics -EA Stop [void]$sb.AppendLine("DNS SERVER DIAGNOSTICS:") [void]$sb.AppendLine(" Query Logging : $($dns.EnableLoggingForLocalLookupEvent) $(if(-not $dns.EnableLoggingForLocalLookupEvent){'[DISABLED]';$issues++}else{'[OK]'})") [void]$sb.AppendLine(" Recursive Queries: $($dns.EnableLoggingForRecursiveLookupEvent)") [void]$sb.AppendLine(" Remote Server : $($dns.EnableLoggingForRemoteServerEvent)") [void]$sb.AppendLine(" Plugin Events : $($dns.EnableLoggingForPluginDllEvent)") [void]$sb.AppendLine(" Log File Path : $($dns.LogFilePath)") [void]$sb.AppendLine(" Max Log Size : $($dns.MaxMBFileSize) MB") } catch { [void]$sb.AppendLine("DNS Server role not detected or not accessible on this host.") [void]$sb.AppendLine("Check DNS logging on the actual DNS server.") $issues++ } # Check DNS client analytics log try { $log = Get-WinEvent -ListLog 'Microsoft-Windows-DNS-Client/Operational' -EA Stop [void]$sb.AppendLine("`nDNS CLIENT OPERATIONAL LOG:") [void]$sb.AppendLine(" Enabled: $($log.IsEnabled) | MaxSize: $([math]::Round($log.MaximumSizeInBytes/1MB,1)) MB | Records: $($log.RecordCount)") } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -eq 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="DNS logging scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'LM04' = @{ Type='Local'; Label='Scan Firewall Logging' Script = { $profiles = Get-NetFirewallProfile -EA Stop $sb = [System.Text.StringBuilder]::new(); $issues = 0 foreach ($p in $profiles) { $logAllowed = $p.LogAllowed; $logBlocked = $p.LogBlocked; $logFile = $p.LogFileName; $logSize = $p.LogMaxSizeKilobytes $ok = $logBlocked -eq 'True' if (-not $ok) { $issues++ } # CIS: Firewall log size >= 16,384 KB $sizeOk = $logSize -ge 16384 if (-not $sizeOk) { $issues++ } [void]$sb.AppendLine("$($p.Name) Profile:") [void]$sb.AppendLine(" Log Allowed : $logAllowed") [void]$sb.AppendLine(" Log Blocked : $logBlocked $(if(-not $ok){'[NOT LOGGING BLOCKED TRAFFIC]'}else{'[OK]'})") [void]$sb.AppendLine(" Log File : $logFile") [void]$sb.AppendLine(" Max Size : $logSize KB $(if($sizeOk){'[OK]'}else{'[LOW - CIS requires 16,384+ KB]'})") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Firewall logging scan (CIS 2025) @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'LM07' = @{ Type='Local'; Label='Scan Log Retention + Event Log Sizes' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # CIS 2025 minimum log sizes in KB $cisMinKB = @{ 'Security' = 196608 # 192 MB 'Application' = 32768 # 32 MB 'System' = 32768 # 32 MB 'Setup' = 32768 # 32 MB 'Microsoft-Windows-PowerShell/Operational' = 16384 # 16 MB } $logs = @('Security','System','Application','Setup','Microsoft-Windows-PowerShell/Operational','Microsoft-Windows-Sysmon/Operational') [void]$sb.AppendLine("EVENT LOG SIZES (CIS Benchmarks 2025):") foreach ($logName in $logs) { try { $log = Get-WinEvent -ListLog $logName -EA Stop $sizeKB = [math]::Round($log.MaximumSizeInBytes / 1KB) $sizeMB = [math]::Round($log.MaximumSizeInBytes / 1MB, 1) $minKB = $cisMinKB[$logName] $small = $minKB -and $sizeKB -lt $minKB if ($small) { $issues++ } $minLabel = if ($minKB) { " (CIS min: $([math]::Round($minKB/1024)) MB)" } else { '' } [void]$sb.AppendLine(" $logName : ${sizeMB} MB | Enabled:$($log.IsEnabled) | Records:$($log.RecordCount) $(if($small){"[LOW$minLabel]"}else{if($minKB){'[OK]'}})$minLabel") } catch { [void]$sb.AppendLine(" $logName : NOT FOUND") } } # Sysmon detection $sysmon = Get-Service Sysmon,Sysmon64 -EA SilentlyContinue | Where-Object { $_.Status -eq 'Running' } [void]$sb.AppendLine("`nSysmon: $(if($sysmon){'RUNNING [OK]'}else{'Not installed'})") # Security log overflow action try { $sec = Get-WinEvent -ListLog 'Security' -EA Stop [void]$sb.AppendLine("Security log mode: $($sec.LogMode) $(if($sec.LogMode -eq 'Circular'){'[Circular - old events overwritten]'})") } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Event log size scan (CIS 2025) @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NA01' = @{ Type='Local'; Label='Scan Network Segmentation' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 $adapters = Get-NetIPConfiguration -Detailed -EA Stop | Where-Object { $_.IPv4Address } [void]$sb.AppendLine("NETWORK CONFIGURATION:") foreach ($a in $adapters) { [void]$sb.AppendLine(" Interface: $($a.InterfaceAlias)") [void]$sb.AppendLine(" IP : $($a.IPv4Address.IPAddress)/$($a.IPv4Address.PrefixLength)") [void]$sb.AppendLine(" Gateway: $($a.IPv4DefaultGateway.NextHop)") [void]$sb.AppendLine(" DNS : $(($a.DNSServer.ServerAddresses) -join ', ')") # Large subnet = likely flat network if ($a.IPv4Address.PrefixLength -le 16) { $issues++; [void]$sb.AppendLine(" [!] Very large subnet (/$($a.IPv4Address.PrefixLength)) - possible flat network") } elseif ($a.IPv4Address.PrefixLength -le 22) { [void]$sb.AppendLine(" [!] Large subnet (/$($a.IPv4Address.PrefixLength)) - verify segmentation") } } # ARP table analysis $arp = Get-NetNeighbor -State Reachable,Stale -EA SilentlyContinue | Where-Object { $_.IPAddress -notmatch ':' -and $_.IPAddress -ne '255.255.255.255' } $arpCount = ($arp | Measure-Object).Count [void]$sb.AppendLine("`nARP TABLE: $arpCount reachable/stale entries") if ($arpCount -gt 50) { $issues++; [void]$sb.AppendLine(" [!] Large number of neighbors visible - suggests flat network or large subnet") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Network segmentation scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NA02' = @{ Type='Local'; Label='Scan VLAN Configuration' Script = { $sb = [System.Text.StringBuilder]::new() $adapters = Get-NetAdapter -EA Stop | Where-Object { $_.Status -eq 'Up' } [void]$sb.AppendLine("ACTIVE NETWORK ADAPTERS:") foreach ($a in $adapters) { $ip = Get-NetIPAddress -InterfaceIndex $a.InterfaceIndex -AddressFamily IPv4 -EA SilentlyContinue $vlanId = $a.VlanID [void]$sb.AppendLine(" $($a.Name) | MAC: $($a.MacAddress) | Speed: $($a.LinkSpeed)") [void]$sb.AppendLine(" IP: $(if($ip){$ip.IPAddress}else{'N/A'}) | VLAN ID: $(if($vlanId){"$vlanId"}else{'None/Default'})") } # Check if multiple subnets are reachable (suggests routing between VLANs) $routes = Get-NetRoute -AddressFamily IPv4 -EA SilentlyContinue | Where-Object { $_.DestinationPrefix -ne '0.0.0.0/0' -and $_.DestinationPrefix -ne '255.255.255.255/32' -and $_.NextHop -ne '0.0.0.0' } if ($routes) { [void]$sb.AppendLine("`nSTATIC ROUTES (non-default):") foreach ($r in ($routes | Select-Object -First 10)) { [void]$sb.AppendLine(" $($r.DestinationPrefix) via $($r.NextHop) if#$($r.InterfaceIndex)") } } $vlanCount = ($adapters | Where-Object { $_.VlanID }).Count $status = if ($vlanCount -gt 0) {'Pass'} else {'Partial'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="VLAN/adapter scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'CF02' = @{ Type='Local'; Label='Test Egress Filtering' Script = { $sb = [System.Text.StringBuilder]::new(); $openPorts = 0 $testPorts = @( @{Port=25; Desc='SMTP (email relay)'} @{Port=445; Desc='SMB (file sharing)'} @{Port=3389; Desc='RDP (remote desktop)'} @{Port=1433; Desc='MSSQL (database)'} @{Port=3306; Desc='MySQL (database)'} @{Port=22; Desc='SSH'} @{Port=23; Desc='Telnet'} @{Port=4444; Desc='Metasploit default'} @{Port=8080; Desc='Alt HTTP/Proxy'} ) [void]$sb.AppendLine("EGRESS PORT TEST (outbound to 1.1.1.1):") foreach ($tp in $testPorts) { try { $tcp = New-Object System.Net.Sockets.TcpClient $connect = $tcp.BeginConnect('1.1.1.1', $tp.Port, $null, $null) $wait = $connect.AsyncWaitHandle.WaitOne(2000, $false) if ($wait -and $tcp.Connected) { $openPorts++; [void]$sb.AppendLine(" Port $($tp.Port) ($($tp.Desc)): OPEN [!]") } else { [void]$sb.AppendLine(" Port $($tp.Port) ($($tp.Desc)): Blocked [OK]") } $tcp.Close() } catch { [void]$sb.AppendLine(" Port $($tp.Port) ($($tp.Desc)): Blocked/Error [OK]") } } [void]$sb.AppendLine("`n$openPorts of $($testPorts.Count) non-standard ports reachable outbound") $status = if ($openPorts -le 1) {'Pass'} elseif ($openPorts -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Egress port test @ $(Get-Date -f 'yyyy-MM-dd HH:mm') from $env:COMPUTERNAME" } } } 'CF04' = @{ Type='AD'; Label='Scan Former Employee Accounts' Script = { $threshold = (Get-Date).AddDays(-90) $users = Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonDate,WhenCreated,Description,Manager -EA Stop $stale = $users | Where-Object { $_.LastLogonDate -and $_.LastLogonDate -lt $threshold } | Sort-Object LastLogonDate $noManager = $users | Where-Object { -not $_.Manager -and $_.LastLogonDate -and $_.LastLogonDate -lt (Get-Date).AddDays(-60) } $sb = [System.Text.StringBuilder]::new() [void]$sb.AppendLine("POTENTIALLY ORPHANED ACCOUNTS (enabled, no logon 90+ days): $($stale.Count)") foreach ($u in ($stale | Select-Object -First 25)) { [void]$sb.AppendLine(" $($u.SamAccountName) | Last: $($u.LastLogonDate.ToString('yyyy-MM-dd')) | Created: $($u.WhenCreated.ToString('yyyy-MM-dd'))") } if ($stale.Count -gt 25) { [void]$sb.AppendLine(" ... +$($stale.Count - 25) more") } [void]$sb.AppendLine("`nACCOUNTS WITH NO MANAGER + 60d INACTIVE: $($noManager.Count)") $status = if ($stale.Count -eq 0) {'Pass'} elseif ($stale.Count -le 5) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Former employee account scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'CF06' = @{ Type='Local'; Label='Scan Network Flatness' Script = { $sb = [System.Text.StringBuilder]::new(); $flat = $false $ip = Get-NetIPAddress -AddressFamily IPv4 -EA Stop | Where-Object { $_.PrefixOrigin -ne 'WellKnown' -and $_.IPAddress -ne '127.0.0.1' } | Select-Object -First 1 $arp = Get-NetNeighbor -State Reachable,Stale -EA SilentlyContinue | Where-Object { $_.IPAddress -notmatch ':' } $arpCount = ($arp | Measure-Object).Count [void]$sb.AppendLine("THIS HOST: $($ip.IPAddress)/$($ip.PrefixLength)") [void]$sb.AppendLine("Visible neighbors (ARP): $arpCount") if ($ip.PrefixLength -le 22 -and $arpCount -gt 30) { $flat = $true [void]$sb.AppendLine("`n[!] FLAT NETWORK INDICATORS:") [void]$sb.AppendLine(" Large subnet (/$($ip.PrefixLength)) with $arpCount visible hosts") [void]$sb.AppendLine(" Workstations and servers likely share same broadcast domain") } # Try pinging common server ports to assess reachability $gw = (Get-NetRoute -DestinationPrefix '0.0.0.0/0' -EA SilentlyContinue | Select-Object -First 1).NextHop [void]$sb.AppendLine("`nDefault Gateway: $gw") [void]$sb.AppendLine("Subnet mask: /$($ip.PrefixLength) (~$([math]::Pow(2, 32 - $ip.PrefixLength)) addresses)") $status = if ($flat) {'Fail'} elseif ($arpCount -gt 20) {'Partial'} else {'Pass'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Network flatness scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'CF08' = @{ Type='Local'; Label='Test DNS Filtering' Script = { $sb = [System.Text.StringBuilder]::new(); $filtered = $false $testDomains = @( @{Domain='examplemalwaredomain.com'; Desc='Umbrella malware test'} @{Domain='internetbadguys.com'; Desc='Umbrella test block'} @{Domain='testmalware.cf'; Desc='Generic malware test'} ) # Get current DNS servers $dns = Get-DnsClientServerAddress -AddressFamily IPv4 -EA SilentlyContinue | Where-Object { $_.ServerAddresses } | Select-Object -First 1 [void]$sb.AppendLine("DNS SERVERS: $(($dns.ServerAddresses) -join ', ')") [void]$sb.AppendLine("`nDNS FILTERING TEST:") foreach ($td in $testDomains) { try { $r = Resolve-DnsName $td.Domain -EA Stop -DnsOnly [void]$sb.AppendLine(" $($td.Domain): RESOLVED ($($r.IPAddress -join ',')) [NOT FILTERED]") } catch { $filtered = $true [void]$sb.AppendLine(" $($td.Domain): BLOCKED/NXDOMAIN [FILTERED - Good]") } } # Check if using known filtering DNS $knownFilters = @('208.67.222.222','208.67.220.220','9.9.9.9','149.112.112.112') $usingFilter = ($dns.ServerAddresses | Where-Object { $_ -in $knownFilters }).Count -gt 0 if ($usingFilter) { [void]$sb.AppendLine("`nUsing known filtering DNS resolver. Good."); $filtered = $true } $status = if ($filtered) {'Pass'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="DNS filtering test @ $(Get-Date -f 'yyyy-MM-dd HH:mm') from $env:COMPUTERNAME" } } } # ── Phase 3: Full Coverage Auto-Checks ──────────────────────────────────── 'IA03' = @{ Type='AD'; Label='Scan MFA Coverage' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check RDP NLA (Network Level Auth - basic MFA indicator) try { $rdp = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -EA Stop $nla = $rdp.UserAuthentication -eq 1 [void]$sb.AppendLine("RDP Network Level Auth: $(if($nla){'Enabled [OK]'}else{'DISABLED [!]'; $issues++})") } catch { [void]$sb.AppendLine("RDP NLA: Could not check") } # Check for Azure AD / Entra modules $hasAzureAD = (Get-Module AzureAD,AzureADPreview,Microsoft.Graph -ListAvailable -EA SilentlyContinue | Measure-Object).Count -gt 0 [void]$sb.AppendLine("Azure AD/Graph modules installed: $hasAzureAD") # Check for ADFS try { $adfs = Get-Service adfssrv -EA SilentlyContinue; if ($adfs) { [void]$sb.AppendLine("ADFS Service: $($adfs.Status)") } else { [void]$sb.AppendLine("ADFS: Not installed on this host") } } catch {} # Check for common MFA/SSO agents (registry - fast) $mfaAgents = @('Duo Authentication','Okta Verify','RSA Authentication','Azure MFA','AuthPoint') $regPaths = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*','HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') $allApps = Get-ItemProperty $regPaths -EA SilentlyContinue | Where-Object { $_.DisplayName } $installed = @($allApps | Where-Object { $n=$_.DisplayName; ($mfaAgents | Where-Object { $n -match $_ }).Count -gt 0 }) if ($installed.Count -gt 0) { foreach ($a in $installed) { [void]$sb.AppendLine("MFA Agent found: $($a.DisplayName) v$($a.DisplayVersion)") } } else { [void]$sb.AppendLine("No MFA agent software detected on this host"); $issues++ } # Check smart card enforcement try { $scLogon = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'scforceoption' -EA SilentlyContinue).scforceoption [void]$sb.AppendLine("Smart card required for logon: $(if($scLogon -eq 1){'Yes'}else{'No'})") } catch {} # Windows Hello for Business [void]$sb.AppendLine("`nWINDOWS HELLO FOR BUSINESS:") try { $whfbPolicy = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\PassportForWork' -EA SilentlyContinue $whfbEnabled = $whfbPolicy.Enabled if ($whfbEnabled -eq 1) { [void]$sb.AppendLine(" Policy: Enabled [OK]") } elseif ($whfbEnabled -eq 0) { [void]$sb.AppendLine(" Policy: Explicitly Disabled") } else { [void]$sb.AppendLine(" Policy: Not configured via GPO") } # Check user enrollment status $ngcKeys = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\NgcPin' -EA SilentlyContinue $ngcContainers = Get-ChildItem "$env:SystemDrive\Windows\ServiceProfiles\LocalService\AppData\Local\Microsoft\Ngc" -EA SilentlyContinue if ($ngcKeys -or $ngcContainers) { [void]$sb.AppendLine(" Enrollment: Keys detected on this device [OK]") } else { [void]$sb.AppendLine(" Enrollment: No WHfB keys found on this device") } # Check if PIN complexity is configured $pinPolicy = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\PassportForWork\PINComplexity' -EA SilentlyContinue if ($pinPolicy) { [void]$sb.AppendLine(" PIN Complexity: Configured (MinLength:$($pinPolicy.MinimumPINLength))") } } catch { [void]$sb.AppendLine(" Windows Hello: Could not query") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -eq 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="MFA coverage scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'IA06' = @{ Type='AD'; Label='Scan PAM / Privileged Access' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check LAPS deployment - Windows LAPS (built-in since Apr 2023) vs Legacy LAPS $lapsType = 'None' try { # Try Windows LAPS first (msLAPS-EncryptedPassword attribute) $lapsComputers = Get-ADComputer -Filter * -Properties 'msLAPS-EncryptedPassword','msLAPS-PasswordExpirationTime','ms-Mcs-AdmPwd','ms-Mcs-AdmPwdExpirationTime' -EA Stop $total = $lapsComputers.Count $winLAPS = ($lapsComputers | Where-Object { $_.'msLAPS-EncryptedPassword' }).Count $legLAPS = ($lapsComputers | Where-Object { $_.'ms-Mcs-AdmPwd' }).Count $lapsDeployed = [math]::Max($winLAPS, $legLAPS) $pct = if ($total -gt 0) { [math]::Round(($lapsDeployed/$total)*100,1) } else { 0 } if ($winLAPS -gt 0) { $lapsType = 'Windows LAPS (encrypted)' } elseif ($legLAPS -gt 0) { $lapsType = 'Legacy LAPS (cleartext)' } [void]$sb.AppendLine("LAPS DEPLOYMENT: $lapsDeployed/$total computers ($pct%)") [void]$sb.AppendLine(" LAPS Type: $lapsType") if ($lapsType -eq 'Legacy LAPS (cleartext)') { [void]$sb.AppendLine(" [!] Legacy LAPS stores passwords in cleartext - migrate to Windows LAPS") } if ($pct -lt 80) { $issues++; [void]$sb.AppendLine(" [!] Low LAPS coverage - target 80%+") } if ($winLAPS -gt 0 -and $legLAPS -gt 0) { [void]$sb.AppendLine(" [i] Mixed deployment: $winLAPS Windows LAPS, $legLAPS Legacy LAPS") } } catch { [void]$sb.AppendLine("LAPS: Could not query (schema extension may not be deployed)"); $issues++ } # Check admin logon sessions (who is currently admin-logged-in) try { $daMembers = (Get-ADGroupMember 'Domain Admins' -EA Stop).SamAccountName $recentAdminLogons = Get-WinEvent -FilterHashtable @{LogName='Security';Id=4624;StartTime=(Get-Date).AddDays(-7)} -MaxEvents 500 -EA SilentlyContinue | Where-Object { $xml=[xml]$_.ToXml(); $u=($xml.Event.EventData.Data|Where-Object{$_.Name -eq 'TargetUserName'}).'#text'; $u -in $daMembers } | Select-Object -First 20 [void]$sb.AppendLine("`nDA LOGON EVENTS (last 7d): $($recentAdminLogons.Count)+ events") if ($recentAdminLogons.Count -gt 50) { $issues++; [void]$sb.AppendLine(" [!] High admin logon volume - admin accounts may be used for daily work") } } catch { [void]$sb.AppendLine("`nAdmin logon event scan: access denied or audit not configured") } # Check for PAM solutions (registry - fast) $pamIndicators = @('CyberArk','BeyondTrust','Thycotic','Delinea','Privileged Access','ManageEngine PAM') $regPaths = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*','HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') $allApps = Get-ItemProperty $regPaths -EA SilentlyContinue | Where-Object { $_.DisplayName } $foundPAM = @($allApps | Where-Object { $n=$_.DisplayName; ($pamIndicators | Where-Object { $n -match $_ }).Count -gt 0 }) if ($foundPAM.Count -gt 0) { foreach ($p in $foundPAM) { [void]$sb.AppendLine("`nPAM Software: $($p.DisplayName)") } } else { [void]$sb.AppendLine("`nNo PAM/JIT access solution detected"); $issues++ } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="PAM/LAPS/Privileged Access scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'IA09' = @{ Type='Local'; Label='Scan Conditional Access / Remote Access' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check RDP settings try { $ts = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -EA Stop $rdpEnabled = $ts.fDenyTSConnections -eq 0 [void]$sb.AppendLine("RDP Enabled: $rdpEnabled") $nla = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -EA SilentlyContinue).UserAuthentication [void]$sb.AppendLine("RDP NLA Required: $(if($nla -eq 1){'Yes [OK]'}else{'No [!]'; if($rdpEnabled){$issues++}})") $rdpPort = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -EA SilentlyContinue).PortNumber [void]$sb.AppendLine("RDP Port: $rdpPort $(if($rdpPort -eq 3389){'(default)'}else{'(custom)'})") } catch {} # Check VPN connections/adapters $vpnAdapters = Get-NetAdapter -EA SilentlyContinue | Where-Object { $_.InterfaceDescription -match 'VPN|WireGuard|OpenVPN|Cisco|Fortinet|Palo Alto|SonicWall|Juniper|GlobalProtect|Pulse|AnyConnect' } if ($vpnAdapters) { foreach ($v in $vpnAdapters) { [void]$sb.AppendLine("`nVPN Adapter: $($v.Name) ($($v.InterfaceDescription)) Status:$($v.Status)") } } $vpnConnections = Get-VpnConnection -EA SilentlyContinue if ($vpnConnections) { [void]$sb.AppendLine("`nCONFIGURED VPN CONNECTIONS:") foreach ($vc in $vpnConnections) { $split = if ($vc.SplitTunneling) {'SPLIT TUNNEL [!]'} else {'Full Tunnel [OK]'} [void]$sb.AppendLine(" $($vc.Name) | Server:$($vc.ServerAddress) | Auth:$($vc.AuthenticationMethod) | $split") if ($vc.SplitTunneling) { $issues++ } } } else { [void]$sb.AppendLine("`nNo built-in VPN connections configured") } # Check for VPN software $vpnSoft = Get-CimInstance Win32_Product -EA SilentlyContinue | Where-Object { $_.Name -match 'VPN|AnyConnect|GlobalProtect|FortiClient|Pulse|WireGuard|OpenVPN' } if ($vpnSoft) { foreach ($vs in $vpnSoft) { [void]$sb.AppendLine("VPN Software: $($vs.Name) v$($vs.Version)") } } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Remote access scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'EP08' = @{ Type='Local'; Label='Scan Hardware Security (UEFI/TPM/VBS)' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Secure Boot try { $sb2 = Confirm-SecureBootUEFI -EA Stop [void]$sb.AppendLine("Secure Boot : $(if($sb2){'ENABLED [OK]'}else{'DISABLED [!]'; $issues++})") } catch { [void]$sb.AppendLine("Secure Boot : Not supported or inaccessible"); $issues++ } # TPM with version check try { $tpm = Get-Tpm -EA Stop [void]$sb.AppendLine("TPM Present : $($tpm.TpmPresent) | Ready: $($tpm.TpmReady) | Enabled: $($tpm.TpmEnabled)") $tpmSpec = (Get-CimInstance -Namespace 'root\cimv2\Security\MicrosoftTpm' -ClassName Win32_Tpm -EA SilentlyContinue).SpecVersion $tpm2 = $tpmSpec -match '^2\.' [void]$sb.AppendLine("TPM Version : $tpmSpec $(if($tpm2){'[TPM 2.0 OK]'}else{'[TPM 1.2 - upgrade recommended]'})") if (-not $tpm.TpmPresent -or -not $tpm.TpmReady) { $issues++ } } catch { [void]$sb.AppendLine("TPM: Could not query"); $issues++ } # Boot mode / firmware type try { $firmwareType = if (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot') {'UEFI'} else {'Legacy BIOS'} [void]$sb.AppendLine("Firmware Type : $firmwareType $(if($firmwareType -eq 'Legacy BIOS'){'[!] UEFI required for VBS/Credential Guard'})") } catch {} # VBS / Credential Guard via Win32_DeviceGuard WMI (running state, not just configured) [void]$sb.AppendLine("`nVIRTUALIZATION-BASED SECURITY (VBS):") try { $dg = Get-CimInstance -ClassName Win32_DeviceGuard -Namespace 'root\Microsoft\Windows\DeviceGuard' -EA Stop $vbsStatus = switch ($dg.VirtualizationBasedSecurityStatus) { 0 {'Not running [!]'} 1 {'Not running (reboot required)'} 2 {'Running [OK]'} default {'Unknown'} } [void]$sb.AppendLine(" VBS Status : $vbsStatus") if ($dg.VirtualizationBasedSecurityStatus -ne 2) { $issues++ } # SecurityServicesRunning: 1=Credential Guard, 2=HVCI, 3=System Guard $runningServices = $dg.SecurityServicesRunning $credGuard = 1 -in $runningServices $hvci = 2 -in $runningServices [void]$sb.AppendLine(" Credential Guard: $(if($credGuard){'RUNNING [OK]'}else{'Not running [!]'; $issues++})") [void]$sb.AppendLine(" HVCI (Memory Integrity): $(if($hvci){'RUNNING [OK]'}else{'Not running [!]'; $issues++})") # Configured services $cfgServices = $dg.SecurityServicesConfigured [void]$sb.AppendLine(" Configured : $(if($cfgServices){($cfgServices -join ', ')}else{'None'})") } catch { [void]$sb.AppendLine(" Win32_DeviceGuard: Not available (VBS may not be supported)"); $issues++ } # LSA Protection (RunAsPPL) [void]$sb.AppendLine("`nCREDENTIAL PROTECTION:") try { $runAsPPL = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -EA SilentlyContinue).RunAsPPL $lsaCfg = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -EA SilentlyContinue).LsaCfgFlags [void]$sb.AppendLine(" LSA Protection (RunAsPPL): $(if($runAsPPL -eq 1){'Enabled [OK]'}else{'Disabled [!]'; $issues++})") if ($lsaCfg) { [void]$sb.AppendLine(" LsaCfgFlags : $lsaCfg") } } catch {} # WDigest credential caching (should be 0 or absent) try { $wdigest = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest' -EA SilentlyContinue).UseLogonCredential [void]$sb.AppendLine(" WDigest Cache : $(if($wdigest -eq 1){'ENABLED [CRITICAL - cleartext passwords in memory!]'; $issues += 2}else{'Disabled [OK]'})") } catch { [void]$sb.AppendLine(" WDigest Cache : Key not present (disabled by default on Win10+) [OK]") } # BIOS Info try { $bios = Get-CimInstance Win32_BIOS -EA Stop $biosAge = if ($bios.ReleaseDate -is [datetime]) { ((Get-Date) - $bios.ReleaseDate).Days } else { $null } [void]$sb.AppendLine("`nBIOS: $($bios.Manufacturer) | $($bios.SMBIOSBIOSVersion) | $(if($bios.ReleaseDate -is [datetime]){$bios.ReleaseDate.ToString('yyyy-MM-dd')}else{'Unknown'})") if ($biosAge -and $biosAge -gt 730) { [void]$sb.AppendLine(" [!] BIOS is $biosAge days old - check for firmware updates"); $issues++ } } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Hardware security + VBS/CG scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'LM02' = @{ Type='Local'; Label='Scan Centralized Logging / SIEM' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for Sysmon $sysmon = Get-Service Sysmon,Sysmon64 -EA SilentlyContinue | Where-Object { $_.Status -eq 'Running' } if ($sysmon) { [void]$sb.AppendLine("Sysmon: RUNNING [OK]") } else { [void]$sb.AppendLine("Sysmon: NOT INSTALLED [!]"); $issues++ } # Check Windows Event Forwarding try { $wef = Get-Service wecsvc -EA SilentlyContinue [void]$sb.AppendLine("WEF Collector Service: $(if($wef){$wef.Status}else{'Not installed'})") $subs = wecutil es 2>$null if ($subs) { [void]$sb.AppendLine("WEF Subscriptions: $($subs.Count)"); foreach ($s in ($subs|Select-Object -First 5)) { [void]$sb.AppendLine(" $s") } } } catch {} # Check for SIEM agents $siemServices = @( @{Name='SplunkForwarder';Desc='Splunk Universal Forwarder'} @{Name='ossec*';Desc='Wazuh/OSSEC Agent'} @{Name='filebeat';Desc='Elastic Filebeat'} @{Name='winlogbeat';Desc='Elastic Winlogbeat'} @{Name='nxlog';Desc='NXLog'} @{Name='snare*';Desc='Snare Agent'} @{Name='QualysAgent';Desc='Qualys Agent'} @{Name='TaniumClient';Desc='Tanium Client'} @{Name='cb*Defense*';Desc='Carbon Black'} @{Name='MsSense';Desc='Microsoft Defender for Endpoint'} ) $foundAgents = @() foreach ($ss in $siemServices) { $svc = Get-Service $ss.Name -EA SilentlyContinue if ($svc) { $foundAgents += "$($ss.Desc): $($svc.Status)"; [void]$sb.AppendLine("$($ss.Desc): $($svc.Status)") } } if ($foundAgents.Count -eq 0) { [void]$sb.AppendLine("`nNo SIEM/log forwarding agents detected [!]"); $issues++ } # Check PowerShell logging try { $psLog = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -EA SilentlyContinue $psTranscript = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription' -EA SilentlyContinue [void]$sb.AppendLine("`nPS Script Block Logging: $(if($psLog.EnableScriptBlockLogging -eq 1){'Enabled [OK]'}else{'Disabled [!]'; $issues++})") [void]$sb.AppendLine("PS Transcription: $(if($psTranscript.EnableTranscripting -eq 1){'Enabled [OK]'}else{'Disabled'})") } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="SIEM/centralized logging scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'LM06' = @{ Type='Local'; Label='Scan File Integrity Monitoring' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for Sysmon (file create/modify events) $sysmon = Get-Service Sysmon,Sysmon64 -EA SilentlyContinue | Where-Object { $_.Status -eq 'Running' } if ($sysmon) { [void]$sb.AppendLine("Sysmon: RUNNING (provides file creation monitoring)") try { $sysmonEvents = (Get-WinEvent -LogName 'Microsoft-Windows-Sysmon/Operational' -MaxEvents 1 -EA Stop) [void]$sb.AppendLine(" Sysmon log has events - actively collecting") } catch { [void]$sb.AppendLine(" Sysmon log: no events or inaccessible") } } else { [void]$sb.AppendLine("Sysmon: NOT INSTALLED"); $issues++ } # Check for common FIM solutions $fimServices = @('OSSEC','Wazuh','Tripwire','AIDE','SamhainSvc','MsSense','CarbonBlack') foreach ($f in $fimServices) { $svc = Get-Service "*$f*" -EA SilentlyContinue if ($svc) { [void]$sb.AppendLine("FIM Agent: $($svc.DisplayName) ($($svc.Status))") } } # Check Windows built-in auditing for file system try { $objAccess = auditpol /get /subcategory:"File System" 2>&1 | Select-String 'File System' [void]$sb.AppendLine("`nFile System Auditing: $($objAccess.ToString().Trim())") if ($objAccess -match 'No Auditing') { $issues++; [void]$sb.AppendLine(" [!] File system auditing not configured") } } catch {} # Check for SACLs on critical paths $criticalPaths = @("$env:SystemRoot\System32","$env:ProgramFiles") foreach ($cp in $criticalPaths) { try { $acl = Get-Acl $cp -Audit -EA SilentlyContinue $auditRules = $acl.Audit.Count [void]$sb.AppendLine("Audit rules on $cp`: $auditRules") } catch {} } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -eq 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="FIM scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'LM08' = @{ Type='Local'; Label='Scan Security Alerting' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for scheduled tasks related to monitoring/alerting $monTasks = @() $monPattern = 'monitor|alert|backup|security|scan|update|patch|audit' try { # Check root and key Microsoft paths where monitoring/security tasks typically live foreach ($tp in @('\','\Microsoft\Windows\','\Microsoft\Windows\Backup\','\Microsoft\Windows\WindowsUpdate\','\Microsoft\Windows\Windows Defender\')) { try { $monTasks += @(Get-ScheduledTask -TaskPath $tp -EA SilentlyContinue | Where-Object { $_.TaskName -match $monPattern -and $_.State -ne 'Disabled' }) } catch {} } } catch {} [void]$sb.AppendLine("ACTIVE MONITORING SCHEDULED TASKS ($($monTasks.Count)):") foreach ($t in ($monTasks | Select-Object -First 15)) { [void]$sb.AppendLine(" $($t.TaskName) | State:$($t.State) | Path:$($t.TaskPath)") } if ($monTasks.Count -eq 0) { $issues++ } # Check for Event Log subscriptions (email triggers) $eventSubs = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Subscriptions' -EA SilentlyContinue [void]$sb.AppendLine("`nEvent Log Subscriptions: $(if($eventSubs){$eventSubs.Count}else{0})") # Check for monitoring agents $monServices = @('zabbix*','snmpd','SNMP','nagios*','prtg*','DatadogAgent','newrelic*','Icinga*','CheckMK*','prometheus*','grafana*') $foundMon = @() foreach ($ms in $monServices) { $svc = Get-Service $ms -EA SilentlyContinue if ($svc) { $foundMon += $svc; [void]$sb.AppendLine("Monitoring Agent: $($svc.DisplayName) ($($svc.Status))") } } if ($foundMon.Count -eq 0) { [void]$sb.AppendLine("`nNo monitoring agents detected"); $issues++ } # Check SNMP $snmp = Get-Service SNMP -EA SilentlyContinue if ($snmp) { [void]$sb.AppendLine("`nSNMP Service: $($snmp.Status)") $community = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\SNMP\Parameters\ValidCommunities' -EA SilentlyContinue if ($community) { $names = $community.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object { $_.Name } if ('public' -in $names) { [void]$sb.AppendLine(" [!] Default 'public' community string in use"); $issues++ } } } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Security alerting scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NA03' = @{ Type='Local'; Label='Scan Wireless Security' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Get wireless profiles try { $profiles = netsh wlan show profiles 2>&1 if ($profiles -match 'is not running') { [void]$sb.AppendLine("WLAN AutoConfig service is not running - no wireless capability") $status = 'Pass' } else { $profileNames = ($profiles | Select-String 'All User Profile\s+:\s+(.+)$' | ForEach-Object { $_.Matches.Groups[1].Value.Trim() }) [void]$sb.AppendLine("WIRELESS PROFILES ($($profileNames.Count)):") foreach ($pn in $profileNames) { $detail = netsh wlan show profile name="$pn" key=clear 2>&1 $auth = ($detail | Select-String 'Authentication\s+:\s+(.+)$' | Select-Object -First 1) $cipher = ($detail | Select-String 'Cipher\s+:\s+(.+)$' | Select-Object -First 1) $connMode = ($detail | Select-String 'Connection mode\s+:\s+(.+)$' | Select-Object -First 1) $authType = if ($auth) { $auth.Matches.Groups[1].Value.Trim() } else { 'Unknown' } $cipherType = if ($cipher) { $cipher.Matches.Groups[1].Value.Trim() } else { 'Unknown' } $weak = $authType -match 'Open|WEP|WPA-Personal' -and $authType -notmatch 'WPA2|WPA3' if ($weak) { $issues++ } [void]$sb.AppendLine(" $pn | Auth:$authType | Cipher:$cipherType $(if($weak){'[WEAK!]'})") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} } } catch { [void]$sb.AppendLine("Wireless scan failed: $_"); $status = 'Partial' } @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Wireless security scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NA04' = @{ Type='Local'; Label='Scan Network Documentation / Diagram Data' Script = { $sb = [System.Text.StringBuilder]::new() # Gather comprehensive network info that would appear on a diagram [void]$sb.AppendLine("=== NETWORK INFRASTRUCTURE DISCOVERY ===") # All interfaces $adapters = Get-NetIPConfiguration -Detailed -EA SilentlyContinue [void]$sb.AppendLine("`nINTERFACES ($($adapters.Count)):") foreach ($a in $adapters) { if ($a.IPv4Address) { [void]$sb.AppendLine(" $($a.InterfaceAlias): $($a.IPv4Address.IPAddress)/$($a.IPv4Address.PrefixLength) GW:$($a.IPv4DefaultGateway.NextHop) DNS:$(($a.DNSServer.ServerAddresses) -join ',')") } } # Domain controllers try { $dcs = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().DomainControllers [void]$sb.AppendLine("`nDOMAIN CONTROLLERS ($($dcs.Count)):") foreach ($dc in $dcs) { [void]$sb.AppendLine(" $($dc.Name) ($($dc.IPAddress)) - Roles: $($dc.Roles -join ', ')") } } catch { [void]$sb.AppendLine("`nDomain Controllers: Not in domain or cannot query") } # Default gateway and routes $routes = Get-NetRoute -AddressFamily IPv4 -EA SilentlyContinue | Where-Object { $_.NextHop -ne '0.0.0.0' -and $_.DestinationPrefix -ne '255.255.255.255/32' } | Select-Object -First 15 [void]$sb.AppendLine("`nROUTING TABLE (non-default, $($routes.Count) entries):") foreach ($r in $routes) { [void]$sb.AppendLine(" $($r.DestinationPrefix) via $($r.NextHop)") } # DNS configuration $dnsServers = Get-DnsClientServerAddress -AddressFamily IPv4 -EA SilentlyContinue | Where-Object { $_.ServerAddresses } | Select-Object InterfaceAlias,ServerAddresses -Unique [void]$sb.AppendLine("`nDNS SERVERS:") foreach ($d in $dnsServers) { [void]$sb.AppendLine(" $($d.InterfaceAlias): $($d.ServerAddresses -join ', ')") } [void]$sb.AppendLine("`n[NOTE] Use this data to verify network diagram accuracy. If no diagram exists, this is the finding.") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="Network infrastructure discovery @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NA05' = @{ Type='Local'; Label='Scan 802.1X / NAC Status' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check 802.1X service $dot1x = Get-Service dot3svc -EA SilentlyContinue [void]$sb.AppendLine("Wired AutoConfig (802.1X) Service: $(if($dot1x){"$($dot1x.Status)"}else{'Not found'})") if (-not $dot1x -or $dot1x.Status -ne 'Running') { $issues++ } # Check for EAP configuration try { $eap = Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services\Eaphost\Methods' -EA SilentlyContinue -Recurse [void]$sb.AppendLine("EAP Methods configured: $(if($eap){$eap.Count}else{0})") } catch {} # Check for NAC agents $nacAgents = @('Cisco ISE','ForeScout','Aruba ClearPass','Portnox','PacketFence','Bradford','Forescout') $foundNAC = Get-CimInstance Win32_Product -EA SilentlyContinue | Where-Object { $n=$_.Name; $nacAgents | Where-Object { $n -match $_ } } if ($foundNAC) { foreach ($na in $foundNAC) { [void]$sb.AppendLine("NAC Agent: $($na.Name)") } } else { [void]$sb.AppendLine("No NAC agent software detected"); $issues++ } # Check for certificate-based auth $certs = Get-ChildItem Cert:\LocalMachine\My -EA SilentlyContinue | Where-Object { $_.EnhancedKeyUsageList.FriendlyName -match 'Client Authentication' } [void]$sb.AppendLine("`nClient auth certificates: $(if($certs){$certs.Count}else{0})") $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="802.1X/NAC scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NA06' = @{ Type='Local'; Label='Scan Management Interface Isolation' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for management ports listening $mgmtPorts = @( @{Port=22;Desc='SSH'},@{Port=23;Desc='Telnet'},@{Port=161;Desc='SNMP'}, @{Port=443;Desc='HTTPS Mgmt'},@{Port=3389;Desc='RDP'},@{Port=5985;Desc='WinRM HTTP'}, @{Port=5986;Desc='WinRM HTTPS'},@{Port=8443;Desc='Alt HTTPS'},@{Port=9090;Desc='Cockpit/Mgmt'} ) $listeners = Get-NetTCPConnection -State Listen -EA SilentlyContinue [void]$sb.AppendLine("MANAGEMENT PORTS LISTENING:") foreach ($mp in $mgmtPorts) { $listening = $listeners | Where-Object { $_.LocalPort -eq $mp.Port } if ($listening) { $bindAddr = ($listening.LocalAddress | Select-Object -Unique) -join ', ' $allInterfaces = $bindAddr -match '0\.0\.0\.0|::' if ($allInterfaces) { $issues++ } [void]$sb.AppendLine(" Port $($mp.Port) ($($mp.Desc)): LISTENING on $bindAddr $(if($allInterfaces){'[ALL INTERFACES - not isolated!]'}else{'[Specific bind]'})") } } # Check if WinRM has IP restrictions try { $winrmFilter = (Get-Item WSMan:\localhost\Service\IPv4Filter -EA SilentlyContinue).Value [void]$sb.AppendLine("`nWinRM IPv4 Filter: $(if($winrmFilter){"$winrmFilter"}else{'* (all)'})") if ($winrmFilter -eq '*' -or -not $winrmFilter) { $issues++ } } catch {} # Check for IPMI/iLO/iDRAC $bmc = Get-CimInstance Win32_NetworkAdapter -EA SilentlyContinue | Where-Object { $_.Name -match 'BMC|IPMI|iLO|iDRAC|Baseboard' } if ($bmc) { foreach ($b in $bmc) { [void]$sb.AppendLine("`nBMC/OOB Interface: $($b.Name)") } } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Management isolation scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NA07' = @{ Type='Local'; Label='Scan Switch Port / Network Port Security' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Enumerate active adapters vs total $allAdapters = Get-NetAdapter -EA SilentlyContinue $active = $allAdapters | Where-Object { $_.Status -eq 'Up' } $down = $allAdapters | Where-Object { $_.Status -ne 'Up' } [void]$sb.AppendLine("NETWORK ADAPTERS:") [void]$sb.AppendLine(" Active: $($active.Count) | Inactive: $($down.Count)") foreach ($a in $active) { [void]$sb.AppendLine(" [UP] $($a.Name) | $($a.InterfaceDescription) | Speed:$($a.LinkSpeed) | MAC:$($a.MacAddress)") } foreach ($d in ($down | Select-Object -First 5)) { [void]$sb.AppendLine(" [--] $($d.Name) | $($d.InterfaceDescription)") } # Check for MAC address filtering indicators $macFilter = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name 'DisableMediaSense' -EA SilentlyContinue # Check for network bridge $bridge = $allAdapters | Where-Object { $_.InterfaceDescription -match 'Bridge|MAC Bridge' } if ($bridge) { [void]$sb.AppendLine("`n[!] Network bridge detected: $($bridge.Name)"); $issues++ } # Check for promiscuous mode indicators try { $promiscReg = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -Name 'EnablePromiscuousMode' -EA SilentlyContinue if ($promiscReg.EnablePromiscuousMode) { [void]$sb.AppendLine("[!] Promiscuous mode enabled"); $issues++ } } catch {} [void]$sb.AppendLine("`n[NOTE] Physical switch port security (802.1X, port disable, MAC limit) must be verified on the switch itself.") $status = if ($issues -eq 0) {'Pass'} else {'Partial'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Network port scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP02' = @{ Type='Local'; Label='Scan Open Ports' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 $listeners = Get-NetTCPConnection -State Listen -EA Stop | Sort-Object LocalPort $grouped = $listeners | Group-Object LocalPort [void]$sb.AppendLine("LISTENING TCP PORTS ($($grouped.Count) unique):") foreach ($g in $grouped) { $port = $g.Name; $binds = ($g.Group.LocalAddress | Select-Object -Unique) -join ', ' $proc = $g.Group[0].OwningProcess $procName = try { (Get-Process -Id $proc -EA Stop).ProcessName } catch { 'Unknown' } $concern = $port -in @(21,23,25,69,110,135,139,445,1433,1434,3306,3389,5432,5900,8080,8443) if ($concern -and $binds -match '0\.0\.0\.0|::') { $issues++ } [void]$sb.AppendLine(" :$port | Bind:$binds | Process:$procName (PID:$proc) $(if($concern){'[REVIEW]'})") } # UDP listeners $udp = Get-NetUDPEndpoint -EA SilentlyContinue | Where-Object { $_.LocalAddress -eq '0.0.0.0' -or $_.LocalAddress -eq '::' } | Group-Object LocalPort | Sort-Object { [int]$_.Name } [void]$sb.AppendLine("`nUDP LISTENERS (all-interface, $($udp.Count) ports):") foreach ($u in ($udp | Select-Object -First 15)) { [void]$sb.AppendLine(" :$($u.Name)") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Open port scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP03' = @{ Type='Local'; Label='Scan VPN Configuration' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Built-in VPN connections $vpns = Get-VpnConnection -EA SilentlyContinue if ($vpns) { [void]$sb.AppendLine("CONFIGURED VPN CONNECTIONS ($($vpns.Count)):") foreach ($v in $vpns) { $flags = @() if ($v.SplitTunneling) { $flags += 'SPLIT_TUNNEL'; $issues++ } if ($v.AuthenticationMethod -contains 'Pap') { $flags += 'PAP_AUTH'; $issues++ } $f = if ($flags) { " [$(($flags -join ', '))]" } else { '' } [void]$sb.AppendLine(" $($v.Name) | Server:$($v.ServerAddress) | Protocol:$($v.TunnelType) | Auth:$($v.AuthenticationMethod -join ',')$f") } } else { [void]$sb.AppendLine("No built-in VPN connections configured") } # Check for VPN software $vpnSoft = @('AnyConnect','GlobalProtect','FortiClient','PulseSecure','Ivanti','WireGuard','OpenVPN','SonicWall','Palo Alto','NetExtender') $procs = Get-Process -EA SilentlyContinue | Where-Object { $n=$_.ProcessName; $vpnSoft | Where-Object { $n -match $_ } } if ($procs) { [void]$sb.AppendLine("`nVPN PROCESSES RUNNING:") foreach ($p in $procs) { [void]$sb.AppendLine(" $($p.ProcessName) (PID:$($p.Id))") } } # Check for VPN adapters $vpnAdapters = Get-NetAdapter -EA SilentlyContinue | Where-Object { $_.InterfaceDescription -match 'VPN|WireGuard|TAP-Windows|Cisco|Fortinet|Palo Alto|SonicWall|Juniper|Pulse' } if ($vpnAdapters) { [void]$sb.AppendLine("`nVPN ADAPTERS:") foreach ($va in $vpnAdapters) { [void]$sb.AppendLine(" $($va.Name) | $($va.InterfaceDescription) | Status:$($va.Status)") } } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="VPN config scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP04' = @{ Type='Local'; Label='Scan DNS Filtering Config' Script = { $sb = [System.Text.StringBuilder]::new(); $filtered = $false # DNS servers in use $dnsConfig = Get-DnsClientServerAddress -AddressFamily IPv4 -EA SilentlyContinue | Where-Object { $_.ServerAddresses } [void]$sb.AppendLine("DNS CONFIGURATION:") foreach ($dc in $dnsConfig) { [void]$sb.AppendLine(" $($dc.InterfaceAlias): $($dc.ServerAddresses -join ', ')") } # Check for known filtering DNS $filterDNS = @{ '208.67.222.222'='OpenDNS/Umbrella'; '208.67.220.220'='OpenDNS/Umbrella' '9.9.9.9'='Quad9'; '149.112.112.112'='Quad9' '185.228.168.168'='CleanBrowsing'; '185.228.169.168'='CleanBrowsing' '76.76.2.0'='ControlD'; '76.76.10.0'='ControlD' } $allDNS = $dnsConfig.ServerAddresses | Select-Object -Unique foreach ($d in $allDNS) { if ($filterDNS.Contains($d)) { [void]$sb.AppendLine(" [OK] $d = $($filterDNS[$d]) (filtering DNS)"); $filtered = $true } } # Check for Umbrella/DNS proxy agents $umbrellaAgent = Get-Service 'Umbrella*','csc_*' -EA SilentlyContinue if ($umbrellaAgent) { [void]$sb.AppendLine("`nCisco Umbrella agent: $($umbrellaAgent.DisplayName) ($($umbrellaAgent.Status))"); $filtered = $true } # Test known bad domains [void]$sb.AppendLine("`nDNS FILTER TEST:") $testDomains = @('examplemalwaredomain.com','internetbadguys.com') foreach ($td in $testDomains) { try { $r = Resolve-DnsName $td -EA Stop -DnsOnly; [void]$sb.AppendLine(" $td`: RESOLVED [NOT FILTERED]") } catch { $filtered = $true; [void]$sb.AppendLine(" $td`: BLOCKED [FILTERED - Good]") } } $status = if ($filtered) {'Pass'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="DNS filtering scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP05' = @{ Type='Local'; Label='Scan Egress / Outbound Rules' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 $profiles = Get-NetFirewallProfile -EA Stop [void]$sb.AppendLine("FIREWALL DEFAULT OUTBOUND ACTIONS:") foreach ($p in $profiles) { $blockOut = $p.DefaultOutboundAction -eq 'Block' if (-not $blockOut) { $issues++ } [void]$sb.AppendLine(" $($p.Name): DefaultOutbound=$($p.DefaultOutboundAction) $(if($blockOut){'[RESTRICTIVE - Good]'}else{'[ALLOW ALL - No egress filtering]'})") } # Check outbound block rules $outBlock = Get-NetFirewallRule -Direction Outbound -Action Block -Enabled True -EA SilentlyContinue [void]$sb.AppendLine("`nOutbound BLOCK rules (enabled): $(($outBlock | Measure-Object).Count)") if ($outBlock) { foreach ($r in ($outBlock | Select-Object -First 10)) { [void]$sb.AppendLine(" $($r.DisplayName)") } } # Check proxy configuration try { $proxy = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -EA SilentlyContinue [void]$sb.AppendLine("`nProxy Enabled: $($proxy.ProxyEnable -eq 1)") if ($proxy.ProxyServer) { [void]$sb.AppendLine("Proxy Server: $($proxy.ProxyServer)") } if ($proxy.AutoConfigURL) { [void]$sb.AppendLine("PAC/WPAD URL: $($proxy.AutoConfigURL)") } } catch {} $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Egress filtering scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP06' = @{ Type='Local'; Label='Scan Stale Firewall Rules' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 $rules = Get-NetFirewallRule -Enabled True -EA Stop # Find potentially stale rules (common indicators) $staleIndicators = @('temp','test','troubleshoot','vendor','old','backup','delete','remove','fixme','TODO','trial') $staleRules = @() foreach ($r in $rules) { $isStale = $staleIndicators | Where-Object { $r.DisplayName -match $_ -or $r.Description -match $_ } if ($isStale) { $staleRules += $r } } [void]$sb.AppendLine("POTENTIALLY STALE FIREWALL RULES ($($staleRules.Count)):") foreach ($sr in ($staleRules | Select-Object -First 20)) { $issues++ [void]$sb.AppendLine(" $($sr.DisplayName) | Dir:$($sr.Direction) | Action:$($sr.Action) | Profile:$($sr.Profile)") } if ($staleRules.Count -eq 0) { [void]$sb.AppendLine(" No rules with stale-looking names found") } # Check total rule count (excessive rules = likely uncurated) [void]$sb.AppendLine("`nTOTAL ENABLED RULES: $($rules.Count)") if ($rules.Count -gt 200) { [void]$sb.AppendLine(" [!] High rule count suggests rules may not be regularly reviewed"); $issues++ } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 3) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Stale firewall rule scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP07' = @{ Type='Local'; Label='Scan IDS/IPS Presence' Script = { $sb = [System.Text.StringBuilder]::new(); $found = $false # Check for IDS/IPS services $idsServices = @( @{Name='Snort*';Desc='Snort IDS'},@{Name='Suricata*';Desc='Suricata IDS'}, @{Name='OSSEC*';Desc='OSSEC HIDS'},@{Name='Wazuh*';Desc='Wazuh HIDS'}, @{Name='MsSense';Desc='Defender for Endpoint'},@{Name='cb*';Desc='Carbon Black'}, @{Name='CrowdStrike*';Desc='CrowdStrike Falcon'},@{Name='SentinelAgent*';Desc='SentinelOne'}, @{Name='SophosSafestore*';Desc='Sophos'},@{Name='Symantec*';Desc='Symantec/Broadcom'} ) [void]$sb.AppendLine("IDS/IPS AND EDR DETECTION:") foreach ($ids in $idsServices) { $svc = Get-Service $ids.Name -EA SilentlyContinue if ($svc) { $found = $true; [void]$sb.AppendLine(" $($ids.Desc): $($svc.DisplayName) ($($svc.Status))") } } if (-not $found) { [void]$sb.AppendLine(" No IDS/IPS/EDR agents detected on this host [!]") } # Check Windows Defender advanced features try { $mp = Get-MpPreference -EA SilentlyContinue if ($mp) { [void]$sb.AppendLine("`nDEFENDER FEATURES:") [void]$sb.AppendLine(" Network Protection: $(if($mp.EnableNetworkProtection -eq 1){'Enabled'}else{'Disabled'})") [void]$sb.AppendLine(" PUA Protection: $($mp.PUAProtection)") [void]$sb.AppendLine(" Cloud Protection: $($mp.MAPSReporting)") [void]$sb.AppendLine(" ASR Rules: $(($mp.AttackSurfaceReductionRules_Actions | Where-Object {$_ -gt 0}).Count) active") } } catch {} $status = if ($found) {'Pass'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="IDS/IPS scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP08' = @{ Type='Local'; Label='Scan TLS / Crypto Configuration' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check TLS registry settings with proper Enabled + DisabledByDefault validation $protocols = @( @{Name='SSL 2.0'; Legacy=$true}; @{Name='SSL 3.0'; Legacy=$true} @{Name='TLS 1.0'; Legacy=$true}; @{Name='TLS 1.1'; Legacy=$true} @{Name='TLS 1.2'; Legacy=$false}; @{Name='TLS 1.3'; Legacy=$false} ) [void]$sb.AppendLine("PROTOCOL STATUS (SCHANNEL Registry):") foreach ($p in $protocols) { $basePath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$($p.Name)" $sEnabled = (Get-ItemProperty "$basePath\Server" -Name 'Enabled' -EA SilentlyContinue).Enabled $sDisabled = (Get-ItemProperty "$basePath\Server" -Name 'DisabledByDefault' -EA SilentlyContinue).DisabledByDefault $cEnabled = (Get-ItemProperty "$basePath\Client" -Name 'Enabled' -EA SilentlyContinue).Enabled # Determine effective state $explicitlyDisabled = ($sEnabled -eq 0) -or ($sDisabled -eq 1) $status_str = if ($sEnabled -eq 0) {'Explicitly Disabled'} elseif ($sEnabled -eq 1) {'Explicitly Enabled'} else {'OS Default'} # Legacy protocols should be explicitly disabled if ($p.Legacy -and -not $explicitlyDisabled) { $issues++ [void]$sb.AppendLine(" $($p.Name): $status_str [!] Should be explicitly disabled (Enabled=0, DisabledByDefault=1)") } else { [void]$sb.AppendLine(" $($p.Name): $status_str $(if($p.Legacy -and $explicitlyDisabled){'[OK]'})") } } # .NET TLS enforcement (both 64-bit and WOW6432Node) [void]$sb.AppendLine("`n.NET FRAMEWORK TLS SETTINGS:") $dotNetPaths = @( @{Path='HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319'; Label='64-bit'} @{Path='HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319'; Label='32-bit'} ) foreach ($dp in $dotNetPaths) { try { $strong = (Get-ItemProperty $dp.Path -EA SilentlyContinue).SchUseStrongCrypto $sysDef = (Get-ItemProperty $dp.Path -EA SilentlyContinue).SystemDefaultTlsVersions $strongOk = $strong -eq 1 $sysOk = $sysDef -eq 1 if (-not $strongOk) { $issues++ } if (-not $sysOk) { $issues++ } [void]$sb.AppendLine(" $($dp.Label): SchUseStrongCrypto=$(if($strongOk){'1 [OK]'}else{'Not set [!]'}) | SystemDefaultTlsVersions=$(if($sysOk){'1 [OK]'}else{'Not set [!]'})") } catch { [void]$sb.AppendLine(" $($dp.Label): Could not query") } } # Certificates $certs = Get-ChildItem Cert:\LocalMachine\My -EA SilentlyContinue if ($certs) { [void]$sb.AppendLine("`nMACHINE CERTIFICATES ($($certs.Count)):") foreach ($c in ($certs | Select-Object -First 10)) { $daysLeft = ($c.NotAfter - (Get-Date)).Days $weak = $c.SignatureAlgorithm.FriendlyName -match 'SHA1|MD5' if ($daysLeft -lt 30) { $issues++ } if ($weak) { $issues++ } [void]$sb.AppendLine(" $($c.Subject) | Expires:$($c.NotAfter.ToString('yyyy-MM-dd')) (${daysLeft}d) | Algo:$($c.SignatureAlgorithm.FriendlyName) $(if($weak){'[WEAK ALGO!]'})$(if($daysLeft -lt 30){'[EXPIRING!]'})") } } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="TLS/crypto scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP09' = @{ Type='Local'; Label='Scan NAT / Port Forwarding' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check netsh port proxy rules try { $portProxy = netsh interface portproxy show all 2>&1 if ($portProxy -match 'Listen|Connect') { $issues++ [void]$sb.AppendLine("PORT PROXY RULES DETECTED:") foreach ($line in $portProxy) { [void]$sb.AppendLine(" $line") } } else { [void]$sb.AppendLine("No port proxy rules configured") } } catch { [void]$sb.AppendLine("Port proxy check: $_") } # Check for IP routing enabled try { $ipFwd = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -EA SilentlyContinue).IPEnableRouter [void]$sb.AppendLine("`nIP Forwarding/Routing: $(if($ipFwd -eq 1){'ENABLED [!]'; $issues++}else{'Disabled'})") } catch {} # Check for ICS (Internet Connection Sharing) $ics = Get-Service SharedAccess -EA SilentlyContinue if ($ics -and $ics.Status -eq 'Running') { [void]$sb.AppendLine("Internet Connection Sharing: RUNNING [!]"); $issues++ } # Check for RRAS (Routing and Remote Access) $rras = Get-Service RemoteAccess -EA SilentlyContinue if ($rras -and $rras.Status -eq 'Running') { [void]$sb.AppendLine("RRAS Service: RUNNING - may be performing NAT/routing") } [void]$sb.AppendLine("`n[NOTE] Check perimeter firewall/router for port forwarding rules - cannot be detected from this host alone.") $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="NAT/port forwarding scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'NP10' = @{ Type='Local'; Label='Scan Firmware / Software / Config Hygiene' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # BIOS/Firmware $bios = Get-CimInstance Win32_BIOS -EA SilentlyContinue -OperationTimeoutSec 10 if ($bios) { $biosDateStr = 'Unknown' $biosAge = $null if ($bios.ReleaseDate -is [datetime]) { $biosAge = ((Get-Date) - $bios.ReleaseDate).Days $biosDateStr = $bios.ReleaseDate.ToString('yyyy-MM-dd') } [void]$sb.AppendLine("BIOS: $($bios.Manufacturer) | Version: $($bios.SMBIOSBIOSVersion) | Date: $biosDateStr$(if($biosAge){" ($biosAge days ago)"})") if ($biosAge -and $biosAge -gt 1095) { $issues++; [void]$sb.AppendLine(" [!] BIOS older than 3 years - check for firmware updates") } } # OS Build $os = Get-CimInstance Win32_OperatingSystem -EA SilentlyContinue -OperationTimeoutSec 10 if ($os) { $installStr = if ($os.InstallDate -is [datetime]) { $os.InstallDate.ToString('yyyy-MM-dd') } else { 'Unknown' } [void]$sb.AppendLine("OS: $($os.Caption) Build $($os.BuildNumber) | Installed: $installStr") } # .NET versions $dotnet = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -EA SilentlyContinue if ($dotnet) { [void]$sb.AppendLine(".NET Framework: $(($dotnet | Get-ItemProperty).Release)") } # PowerShell version [void]$sb.AppendLine("PowerShell: $($PSVersionTable.PSVersion)") # WSUS configuration - verify HTTPS [void]$sb.AppendLine("`nWSUS CONFIGURATION:") try { $wu = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -EA SilentlyContinue if ($wu -and $wu.WUServer) { $wsusHttps = $wu.WUServer -match '^https://' [void]$sb.AppendLine(" WSUS Server : $($wu.WUServer) $(if(-not $wsusHttps){'[HTTP - should use HTTPS!]'; $issues++}else{'[HTTPS OK]'})") [void]$sb.AppendLine(" Status Server: $($wu.WUStatusServer)") } else { [void]$sb.AppendLine(" No WSUS configured (using Windows Update or other)") } } catch {} # Print Spooler on servers/DCs (PrintNightmare risk) [void]$sb.AppendLine("`nPRINT SPOOLER SERVICE:") try { $spooler = Get-Service Spooler -EA SilentlyContinue $isServer = $script:Env.IsServer $isDC = $script:Env.IsDomainJoined -and ($os.Caption -match 'Server') if ($spooler -and $spooler.Status -eq 'Running' -and $isServer) { $issues++; [void]$sb.AppendLine(" Spooler: RUNNING on server$(if($isDC){' (DC)'}) [!] - disable unless print server role required (PrintNightmare)") } elseif ($spooler) { [void]$sb.AppendLine(" Spooler: $($spooler.Status) $(if($isServer -and $spooler.Status -ne 'Running'){'[OK - disabled on server]'})") } } catch {} # Network driver versions [void]$sb.AppendLine("`nNETWORK DRIVER VERSIONS:") $adapters = Get-NetAdapter -EA SilentlyContinue | Where-Object { $_.Status -eq 'Up' } foreach ($a in $adapters) { $drv = $a.DriverVersion $drvDateStr = '' if ($a.DriverDate -is [datetime]) { $drvDateStr = " ($($a.DriverDate.ToString('yyyy-MM-dd')))" } [void]$sb.AppendLine(" $($a.InterfaceDescription): v$drv$drvDateStr") } # Security software via registry (fast, avoids slow Win32_Product WMI class) [void]$sb.AppendLine("`nINSTALLED SECURITY SOFTWARE:") $secPattern = 'Security|Antivirus|Firewall|VPN|Endpoint|CrowdStrike|Sentinel|Sophos|Defender|ESET|Kaspersky|Malware|Norton|McAfee|Bitdefender' $regPaths = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*','HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') $secSoft = @() foreach ($rp in $regPaths) { try { $secSoft += @(Get-ItemProperty $rp -EA SilentlyContinue | Where-Object { $_.DisplayName -match $secPattern } | Select-Object -Property DisplayName,DisplayVersion -First 10) } catch {} } $secSoft = $secSoft | Sort-Object DisplayName -Unique | Select-Object -First 10 if ($secSoft) { foreach ($ss in $secSoft) { [void]$sb.AppendLine(" $($ss.DisplayName) v$($ss.DisplayVersion)") } } else { [void]$sb.AppendLine(" No security software detected in registry") } $status = if ($issues -eq 0) {'Pass'} else {'Partial'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Firmware/software/config scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } # ── Backup & Recovery Auto-Checks ───────────────────────────────────────── 'BR01' = @{ Type='Local'; Label='Scan Backup Solutions' Script = { $sb = [System.Text.StringBuilder]::new(); $found = $false # Single Get-Service call for all services, then match $backupPatterns = @{ 'wbengine'='Windows Server Backup'; 'vss'='Volume Shadow Copy'; 'VeeamBackupSvc'='Veeam'; 'VeeamAgent'='Veeam Agent'; 'AcronisAgent'='Acronis'; 'BackupExecAgent'='Veritas Backup Exec'; 'DattoBackupAgent'='Datto/Kaseya'; 'StorageCraft'='StorageCraft'; 'ArcserveUDP'='Arcserve'; 'CarboniteService'='Carbonite'; 'CrashPlanService'='CrashPlan'; 'CloudBerry'='MSP360/CloudBerry'; 'ShadowProtect'='ShadowProtect'; 'NableBM'='N-able Backup' } [void]$sb.AppendLine("BACKUP SOLUTIONS DETECTED:") try { $allSvc = Get-Service -EA SilentlyContinue foreach ($svc in $allSvc) { foreach ($pat in $backupPatterns.Keys) { if ($svc.ServiceName -like "*${pat}*") { $found = $true [void]$sb.AppendLine(" $($backupPatterns[$pat]): $($svc.DisplayName) ($($svc.Status))") } } } } catch { [void]$sb.AppendLine(" Service query failed: $_") } if (-not $found) { [void]$sb.AppendLine(" No backup agent/service detected [CRITICAL!]") } # Check VSS (Shadow Copies) try { $shadows = Get-CimInstance Win32_ShadowCopy -EA SilentlyContinue -OperationTimeoutSec 10 [void]$sb.AppendLine("`nSHADOW COPIES: $(if($shadows){@($shadows).Count}else{0})") if ($shadows) { $latest = $shadows | Sort-Object InstallDate -Descending | Select-Object -First 3 foreach ($sh in $latest) { [void]$sb.AppendLine(" $($sh.VolumeName) | Created: $($sh.InstallDate)") } } } catch { [void]$sb.AppendLine("`nSHADOW COPIES: Query failed") } # Check for backup scheduled tasks - targeted task paths to avoid full enumeration try { $backupTasks = @() foreach ($tp in @('\','\Microsoft\Windows\Backup\','\Microsoft\Windows\WindowsBackup\')) { try { $backupTasks += @(Get-ScheduledTask -TaskPath $tp -EA SilentlyContinue | Where-Object { $_.TaskName -match 'backup|veeam|acronis|shadow|wbadmin' -and $_.State -ne 'Disabled' }) } catch {} } if ($backupTasks.Count -gt 0) { [void]$sb.AppendLine("`nBACKUP SCHEDULED TASKS ($($backupTasks.Count)):") foreach ($bt in ($backupTasks | Select-Object -First 5)) { [void]$sb.AppendLine(" $($bt.TaskName) | State:$($bt.State)") } } } catch { [void]$sb.AppendLine("`nScheduled task query failed") } # Windows Server Backup status - only if cmdlet exists if (Get-Command Get-WBSummary -EA SilentlyContinue) { try { $wbStatus = Get-WBSummary -EA SilentlyContinue if ($wbStatus) { [void]$sb.AppendLine("`nWINDOWS SERVER BACKUP:") [void]$sb.AppendLine(" Last Success: $($wbStatus.LastSuccessfulBackupTime)") [void]$sb.AppendLine(" Last Backup: $($wbStatus.LastBackupTime)") [void]$sb.AppendLine(" Next Backup: $($wbStatus.NextBackupTime)") } } catch {} } $status = if ($found) {'Pass'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Backup solution scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'BR02' = @{ Type='Local'; Label='Scan Backup Restore Test Evidence' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check event logs for restore events [void]$sb.AppendLine("BACKUP RESTORE EVENT SEARCH:") try { $restoreEvents = Get-WinEvent -FilterHashtable @{LogName='Application';StartTime=(Get-Date).AddDays(-90)} -MaxEvents 1000 -EA SilentlyContinue | Where-Object { $_.Message -match 'restore|recovered|recovery completed' } [void]$sb.AppendLine(" Restore-related events (last 90d): $(($restoreEvents | Measure-Object).Count)") foreach ($re in ($restoreEvents | Select-Object -First 5)) { [void]$sb.AppendLine(" [$($re.TimeCreated.ToString('yyyy-MM-dd'))] $($re.Message.Substring(0,[math]::Min(120,$re.Message.Length)))...") } } catch { [void]$sb.AppendLine(" Could not search event logs") } # Check for VSS restore points try { $rp = Get-ComputerRestorePoint -EA SilentlyContinue [void]$sb.AppendLine("`nSYSTEM RESTORE POINTS: $(if($rp){$rp.Count}else{0})") if ($rp) { foreach ($r in ($rp | Select-Object -Last 3)) { [void]$sb.AppendLine(" $($r.Description) | $($r.CreationTime)") } } } catch {} [void]$sb.AppendLine("`n[!] IMPORTANT: A successful backup verification is NOT a restore test.") [void]$sb.AppendLine("[!] Ask: When was the last actual restore FROM backup performed?") [void]$sb.AppendLine("[!] If never tested, this is a CRITICAL finding.") $issues++ $status = 'Partial' @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Backup restore evidence scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'BR03' = @{ Type='Local'; Label='Scan Immutable / Air-Gapped Backups' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check backup destinations [void]$sb.AppendLine("BACKUP DESTINATION ANALYSIS:") # Check for network shares used by backup try { $shares = Get-SmbShare -EA SilentlyContinue | Where-Object { $_.Name -match 'backup|bak|archive' } if ($shares) { [void]$sb.AppendLine("BACKUP-RELATED SMB SHARES:") foreach ($sh in $shares) { [void]$sb.AppendLine(" $($sh.Name) -> $($sh.Path) (Access: $($sh.CurrentUsers) users)") } [void]$sb.AppendLine(" [!] Network shares are NOT air-gapped - ransomware can encrypt them"); $issues++ } } catch {} # Check for removable/external drives $removable = Get-CimInstance Win32_DiskDrive -EA SilentlyContinue -OperationTimeoutSec 10 | Where-Object { $_.InterfaceType -eq 'USB' -or $_.MediaType -match 'External|Removable' } if ($removable) { [void]$sb.AppendLine("`nEXTERNAL/USB DRIVES:") foreach ($r in $removable) { [void]$sb.AppendLine(" $($r.Model) | Size: $([math]::Round($r.Size/1GB,1))GB | Interface: $($r.InterfaceType)") } } # Check Volume Shadow Copy storage try { $vssStorage = vssadmin list shadowstorage 2>&1 if ($vssStorage -notmatch 'No items') { [void]$sb.AppendLine("`nVSS STORAGE:"); foreach ($l in $vssStorage) { if ($l.Trim()) { [void]$sb.AppendLine(" $($l.Trim())") } } } } catch {} [void]$sb.AppendLine("`n[!] Verify: Are backups stored on a medium that ransomware cannot reach?") [void]$sb.AppendLine("[!] True immutability requires: offline tapes, cloud with object lock, or hardware WORM.") $status = if ($issues -eq 0) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Immutable backup scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'BR04' = @{ Type='Local'; Label='Scan RTO/RPO Documentation' Script = { $sb = [System.Text.StringBuilder]::new() # Check backup frequency indicators [void]$sb.AppendLine("BACKUP FREQUENCY INDICATORS:") try { $shadows = Get-CimInstance Win32_ShadowCopy -EA SilentlyContinue -OperationTimeoutSec 10 | Sort-Object InstallDate -Descending if ($shadows -and $shadows.Count -ge 2) { $interval = ($shadows[0].InstallDate - $shadows[1].InstallDate) [void]$sb.AppendLine(" VSS snapshot interval: ~$([math]::Round($interval.TotalHours,1)) hours (RPO indicator)") [void]$sb.AppendLine(" Latest snapshot: $($shadows[0].InstallDate)") [void]$sb.AppendLine(" Snapshots in last 30d: $(($shadows | Where-Object { $_.InstallDate -gt (Get-Date).AddDays(-30) }).Count)") } } catch {} # Check backup scheduled task timing $backupTasks = @() try { foreach ($tp in @('\','\Microsoft\Windows\Backup\','\Microsoft\Windows\WindowsBackup\')) { try { $backupTasks += @(Get-ScheduledTask -TaskPath $tp -EA SilentlyContinue | Where-Object { $_.TaskName -match 'backup|veeam|acronis|shadow' -and $_.State -ne 'Disabled' }) } catch {} } } catch {} if ($backupTasks) { [void]$sb.AppendLine("`nBACKUP SCHEDULE (from scheduled tasks):") foreach ($bt in $backupTasks) { $triggers = $bt.Triggers foreach ($tr in $triggers) { [void]$sb.AppendLine(" $($bt.TaskName): $($tr.CimClass.CimClassName -replace 'MSFT_Task','')") } } } [void]$sb.AppendLine("`n[!] Ask the client:") [void]$sb.AppendLine(" 1. What is your target Recovery Time Objective (RTO)?") [void]$sb.AppendLine(" 2. What is your target Recovery Point Objective (RPO)?") [void]$sb.AppendLine(" 3. Are these documented and approved by business stakeholders?") [void]$sb.AppendLine(" 4. Has an actual restore ever been timed to validate the RTO?") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="RTO/RPO data scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'BR05' = @{ Type='Local'; Label='Scan Backup Encryption' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check BitLocker on potential backup volumes try { $vols = Get-BitLockerVolume -EA SilentlyContinue [void]$sb.AppendLine("BITLOCKER STATUS (all volumes):") foreach ($v in $vols) { $encrypted = $v.ProtectionStatus -eq 'On' [void]$sb.AppendLine(" $($v.MountPoint) $($v.VolumeStatus) | Protection:$($v.ProtectionStatus) | Method:$($v.EncryptionMethod)") } } catch { [void]$sb.AppendLine("BitLocker: Not available") } # Check EFS configuration try { $efs = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\EFS' -EA SilentlyContinue [void]$sb.AppendLine("`nEFS Policy: $(if($efs){'Configured'}else{'Default (available but not enforced)'})") } catch {} # Check for backup software encryption indicators [void]$sb.AppendLine("`n[!] Verify with backup software:") [void]$sb.AppendLine(" 1. Is backup data encrypted at rest?") [void]$sb.AppendLine(" 2. Is backup data encrypted in transit?") [void]$sb.AppendLine(" 3. Where are encryption keys stored?") [void]$sb.AppendLine(" 4. Are keys separate from the backup data?") $status = 'Partial' @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Backup encryption scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'BR06' = @{ Type='Local'; Label='Scan Backup Monitoring / Alerting' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for backup-related events in last 7 days [void]$sb.AppendLine("BACKUP EVENT LOG ACTIVITY (last 7 days):") try { $events = Get-WinEvent -FilterHashtable @{LogName='Application';StartTime=(Get-Date).AddDays(-7)} -MaxEvents 2000 -EA SilentlyContinue | Where-Object { $_.ProviderName -match 'Backup|VSS|Veeam|Acronis|Wbadmin|SPP' } $grouped = $events | Group-Object ProviderName foreach ($g in $grouped) { [void]$sb.AppendLine(" $($g.Name): $($g.Count) events") } $errors = $events | Where-Object { $_.Level -eq 2 } if ($errors) { [void]$sb.AppendLine("`n [!] BACKUP ERRORS: $($errors.Count)"); $issues++ } if (-not $events) { [void]$sb.AppendLine(" No backup events found [!]"); $issues++ } } catch { [void]$sb.AppendLine(" Event log query failed") } # Check for backup-related scheduled tasks (monitoring) $monTasks = @() try { foreach ($tp in @('\','\Microsoft\Windows\Backup\','\Microsoft\Windows\WindowsBackup\')) { try { $monTasks += @(Get-ScheduledTask -TaskPath $tp -EA SilentlyContinue | Where-Object { $_.TaskName -match 'backup.*report|backup.*alert|backup.*monitor|backup.*notify' }) } catch {} } } catch {} if ($monTasks) { [void]$sb.AppendLine("`nBACKUP MONITORING TASKS:") foreach ($mt in $monTasks) { [void]$sb.AppendLine(" $($mt.TaskName) ($($mt.State))") } } else { [void]$sb.AppendLine("`nNo backup monitoring/alerting tasks found"); $issues++ } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -eq 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Backup monitoring scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'BR07' = @{ Type='Local'; Label='Scan DR Plan / Documentation' Script = { $sb = [System.Text.StringBuilder]::new() # Check for DR-related GPOs try { $gpos = Get-GPO -All -EA SilentlyContinue | Where-Object { $_.DisplayName -match 'disaster|recovery|DR|business.continuity|BCP' } if ($gpos) { [void]$sb.AppendLine("DR-RELATED GPOs:") foreach ($g in $gpos) { [void]$sb.AppendLine(" $($g.DisplayName) | Modified: $($g.ModificationTime.ToString('yyyy-MM-dd'))") } } else { [void]$sb.AppendLine("No DR-related GPOs found") } } catch { [void]$sb.AppendLine("GPO check: Not available (non-domain or no GPMC)") } # Check for recovery partition $parts = Get-Partition -EA SilentlyContinue | Where-Object { $_.Type -eq 'Recovery' } [void]$sb.AppendLine("`nRecovery Partitions: $(if($parts){$parts.Count}else{0})") # Check System Restore try { $sr = Get-ComputerRestorePoint -EA SilentlyContinue [void]$sb.AppendLine("System Restore Points: $(if($sr){$sr.Count}else{0})") } catch {} [void]$sb.AppendLine("`n[!] DR DOCUMENTATION CHECKLIST - verify these exist:") [void]$sb.AppendLine(" [ ] Written DR plan document") [void]$sb.AppendLine(" [ ] Recovery procedure runbooks") [void]$sb.AppendLine(" [ ] Contact/escalation tree") [void]$sb.AppendLine(" [ ] Tabletop exercise completed (last 12 months)") [void]$sb.AppendLine(" [ ] Roles and responsibilities assigned") [void]$sb.AppendLine(" [ ] Off-site meeting location designated") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="DR documentation scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'BR08' = @{ Type='Local'; Label='Scan Cloud/SaaS Backup' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for cloud backup agents $cloudBackup = @( @{Name='OneDrive*';Desc='OneDrive (M365)'},@{Name='Dropbox*';Desc='Dropbox'}, @{Name='Box*Sync*';Desc='Box'},@{Name='GoogleDrive*';Desc='Google Drive'}, @{Name='iDriveService';Desc='iDrive'},@{Name='Backblaze*';Desc='Backblaze'}, @{Name='SpanningBackup*';Desc='Spanning Backup'},@{Name='AvePoint*';Desc='AvePoint'}, @{Name='Veeam*O365*';Desc='Veeam for M365'},@{Name='AFI*';Desc='AFI Backup'} ) [void]$sb.AppendLine("CLOUD BACKUP AGENTS:") $foundCloud = $false foreach ($cb in $cloudBackup) { $svc = Get-Service $cb.Name -EA SilentlyContinue if ($svc) { $foundCloud = $true; [void]$sb.AppendLine(" $($cb.Desc): $($svc.DisplayName) ($($svc.Status))") } } # Check for OneDrive Known Folder Move try { $kfm = Get-ItemProperty 'HKCU:\Software\Microsoft\OneDrive\Accounts\Business1' -Name 'KfmFoldersProtectedNow' -EA SilentlyContinue if ($kfm) { $foundCloud = $true; [void]$sb.AppendLine(" OneDrive Known Folder Move: Active") } } catch {} if (-not $foundCloud) { [void]$sb.AppendLine(" No cloud backup agents detected"); $issues++ } # Check M365 connectivity $outlook = Get-Process OUTLOOK -EA SilentlyContinue $teams = Get-Process Teams -EA SilentlyContinue [void]$sb.AppendLine("`nM365 INDICATORS:") [void]$sb.AppendLine(" Outlook running: $(if($outlook){'Yes'}else{'No'})") [void]$sb.AppendLine(" Teams running: $(if($teams){'Yes'}else{'No'})") [void]$sb.AppendLine("`n[!] CRITICAL: M365 data (Exchange, SharePoint, OneDrive, Teams)") [void]$sb.AppendLine(" is NOT backed up by Microsoft by default!") [void]$sb.AppendLine(" Ask: Do you have a third-party M365 backup solution?") $status = if ($foundCloud) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Cloud/SaaS backup scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } # ── Common Findings: Remaining ──────────────────────────────────────────── 'CF03' = @{ Type='Local'; Label='Scan Backup Restore Testing' Script = { $sb = [System.Text.StringBuilder]::new() # Check for any recent restore activity try { $restoreEvents = Get-WinEvent -FilterHashtable @{LogName='Application';StartTime=(Get-Date).AddDays(-180)} -MaxEvents 5000 -EA SilentlyContinue | Where-Object { $_.Message -match 'restore|recovery completed|recovery succeeded' } [void]$sb.AppendLine("RESTORE EVENTS (last 180 days): $(($restoreEvents | Measure-Object).Count)") foreach ($re in ($restoreEvents | Select-Object -First 5)) { [void]$sb.AppendLine(" [$($re.TimeCreated.ToString('yyyy-MM-dd'))] $($re.ProviderName): $($re.Message.Substring(0,[math]::Min(100,$re.Message.Length)))") } } catch {} # Check backup software status $veeam = Get-Service Veeam* -EA SilentlyContinue $wsb = Get-Command wbadmin -EA SilentlyContinue [void]$sb.AppendLine("`nBACKUP SOFTWARE:") if ($veeam) { [void]$sb.AppendLine(" Veeam: Installed") } if ($wsb) { [void]$sb.AppendLine(" Windows Server Backup: Available") } [void]$sb.AppendLine("`n=== THIS IS THE #1 FINDING IN SMB AUDITS ===") [void]$sb.AppendLine("Ask: 'When did you last perform an actual restore test?'") [void]$sb.AppendLine("If answer is 'never' or 'I don't remember' -> CRITICAL finding") [void]$sb.AppendLine("A backup that has never been restore-tested is NOT a backup.") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="Backup restore test scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'CF05' = @{ Type='Local'; Label='Scan Open File Shares' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check SMB shares try { $shares = Get-SmbShare -EA Stop | Where-Object { $_.Name -notmatch '^\$|^IPC\$|^ADMIN\$|^print\$' } [void]$sb.AppendLine("NON-DEFAULT SMB SHARES ($($shares.Count)):") foreach ($sh in $shares) { $access = Get-SmbShareAccess $sh.Name -EA SilentlyContinue $everyone = $access | Where-Object { $_.AccountName -match 'Everyone|ANONYMOUS|Authenticated Users|Domain Users' -and $_.AccessControlType -eq 'Allow' } if ($everyone) { $issues++ } [void]$sb.AppendLine(" \\$env:COMPUTERNAME\$($sh.Name) -> $($sh.Path) $(if($everyone){'[BROAD ACCESS!]'})") foreach ($e in $everyone) { [void]$sb.AppendLine(" [!] $($e.AccountName): $($e.AccessRight)") } } } catch { [void]$sb.AppendLine("SMB Share enumeration failed: $_") } # Check for hidden admin shares $adminShares = Get-SmbShare -EA SilentlyContinue | Where-Object { $_.Name -match '^\w\$$' } [void]$sb.AppendLine("`nADMIN SHARES: $(($adminShares | Measure-Object).Count) (C$, D$, etc.)") $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 2) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="File share scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } # ── Policies & Standards Auto-Checks ────────────────────────────────────── 'PS01' = @{ Type='Local'; Label='Scan Physical Security Indicators' Script = { $sb = [System.Text.StringBuilder]::new() # Check for screen lock policy try { $lock = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -EA SilentlyContinue $timeout = Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -EA SilentlyContinue [void]$sb.AppendLine("SCREEN LOCK POLICY:") [void]$sb.AppendLine(" Screen saver timeout: $(if($timeout.ScreenSaveTimeOut){"$($timeout.ScreenSaveTimeOut)s"}else{'Not set'})") $ssActive = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -EA SilentlyContinue).ScreenSaveActive [void]$sb.AppendLine(" Screen saver active: $ssActive") $ssSecure = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaverIsSecure -EA SilentlyContinue).ScreenSaverIsSecure [void]$sb.AppendLine(" Password on resume: $ssSecure") } catch {} # Check for camera/physical security software $secSoft = Get-Process -EA SilentlyContinue | Where-Object { $_.ProcessName -match 'camera|surveillance|DVR|NVR|milestone|genetec|exacq|avigilon|hikvision' } if ($secSoft) { [void]$sb.AppendLine("`nSECURITY CAMERA SOFTWARE RUNNING:") foreach ($ss in $secSoft) { [void]$sb.AppendLine(" $($ss.ProcessName)") } } [void]$sb.AppendLine("`n[!] PHYSICAL SECURITY CHECKLIST:") [void]$sb.AppendLine(" [ ] Server room / MDF / IDF locked") [void]$sb.AppendLine(" [ ] Access control (badge/key) with logging") [void]$sb.AppendLine(" [ ] Security cameras at entry points") [void]$sb.AppendLine(" [ ] Visitor sign-in/out process") [void]$sb.AppendLine(" [ ] Clean desk policy enforced") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="Physical security scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'PS02' = @{ Type='Local'; Label='Scan Visitor / Access Policy' Script = { $sb = [System.Text.StringBuilder]::new() # Check for badge system software $badgeSoft = Get-Process -EA SilentlyContinue | Where-Object { $_.ProcessName -match 'Lenel|S2|Brivo|Keri|HID|Verkada|openpath|swiftconnect' } if ($badgeSoft) { [void]$sb.AppendLine("ACCESS CONTROL SOFTWARE DETECTED:") foreach ($bs in $badgeSoft) { [void]$sb.AppendLine(" $($bs.ProcessName)") } } else { [void]$sb.AppendLine("No badge/access control software detected on this host") } # Check AD for visitor/guest accounts try { $guests = Get-ADUser -Filter 'SamAccountName -like "*visitor*" -or SamAccountName -like "*guest*"' -Properties Enabled,LastLogonDate -EA SilentlyContinue if ($guests) { [void]$sb.AppendLine("`nVISITOR/GUEST AD ACCOUNTS:") foreach ($g in $guests) { [void]$sb.AppendLine(" $($g.SamAccountName) Enabled:$($g.Enabled)") } } } catch {} [void]$sb.AppendLine("`n[!] VISITOR MANAGEMENT CHECKLIST:") [void]$sb.AppendLine(" [ ] Visitor sign-in/sign-out log at reception") [void]$sb.AppendLine(" [ ] Visitor badges issued and collected") [void]$sb.AppendLine(" [ ] Visitors escorted in sensitive areas") [void]$sb.AppendLine(" [ ] Visitor network access restricted to guest VLAN") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="Visitor/access policy scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm')" } } } 'PS03' = @{ Type='Local'; Label='Scan Camera / Surveillance' Script = { $sb = [System.Text.StringBuilder]::new() # Scan for camera/NVR software $camProcs = Get-Process -EA SilentlyContinue | Where-Object { $_.ProcessName -match 'camera|NVR|DVR|milestone|genetec|exacq|avigilon|hikvision|dahua|axis|verkada|reolink|blue.iris|ispy|zoneminder' } if ($camProcs) { [void]$sb.AppendLine("SURVEILLANCE SOFTWARE RUNNING:") foreach ($cp in $camProcs) { [void]$sb.AppendLine(" $($cp.ProcessName) (PID:$($cp.Id))") } } else { [void]$sb.AppendLine("No surveillance/camera software detected on this host") } # Check for camera-related services $camSvcs = Get-Service -EA SilentlyContinue | Where-Object { $_.DisplayName -match 'camera|NVR|surveillance|milestone|genetec|video' } if ($camSvcs) { [void]$sb.AppendLine("`nSURVEILLANCE SERVICES:") foreach ($cs in $camSvcs) { [void]$sb.AppendLine(" $($cs.DisplayName) ($($cs.Status))") } } # Scan network for common camera ports [void]$sb.AppendLine("`n[!] SECURITY CAMERA CHECKLIST:") [void]$sb.AppendLine(" [ ] Cameras cover all entry/exit points") [void]$sb.AppendLine(" [ ] Camera footage retained 30+ days") [void]$sb.AppendLine(" [ ] NVR/DVR password changed from default") [void]$sb.AppendLine(" [ ] Camera network isolated from production") [void]$sb.AppendLine(" [ ] Remote viewing secured with VPN/MFA") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="Surveillance scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'PS04' = @{ Type='Local'; Label='Scan Clean Desk / Credential Exposure' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for credentials in common locations $credFiles = @() $searchPaths = @("$env:USERPROFILE\Desktop","$env:USERPROFILE\Documents","$env:PUBLIC\Desktop") foreach ($sp in $searchPaths) { $found = Get-ChildItem $sp -File -EA SilentlyContinue | Where-Object { $_.Name -match 'password|credential|login|secret|\.rdp$|\.pgpass|\.my\.cnf' } if ($found) { $credFiles += $found } } if ($credFiles) { $issues++ [void]$sb.AppendLine("POTENTIAL CREDENTIAL FILES FOUND:") foreach ($cf in $credFiles) { [void]$sb.AppendLine(" $($cf.FullName) ($($cf.LastWriteTime.ToString('yyyy-MM-dd')))") } } else { [void]$sb.AppendLine("No obvious credential files found on Desktop/Documents") } # Check for saved RDP credentials $rdpFiles = Get-ChildItem "$env:USERPROFILE\Documents" -Filter '*.rdp' -Recurse -EA SilentlyContinue if ($rdpFiles) { $issues++; [void]$sb.AppendLine("`nRDP FILES (may contain saved credentials): $($rdpFiles.Count)") } # Check Credential Manager try { $creds = cmdkey /list 2>&1 $savedCreds = ($creds | Select-String 'Target:').Count [void]$sb.AppendLine("`nWindows Credential Manager: $savedCreds saved credentials") } catch {} # Check for auto-logon $autoLogon = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name 'DefaultPassword' -EA SilentlyContinue).DefaultPassword if ($autoLogon) { $issues++; [void]$sb.AppendLine("`n[!] AUTO-LOGON with stored password detected!") } $status = if ($issues -eq 0) {'Pass'} elseif ($issues -le 1) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="Credential exposure scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'PS05' = @{ Type='Local'; Label='Scan Network Jack / Guest VLAN Security' Script = { $sb = [System.Text.StringBuilder]::new() # Check for guest wireless profiles $profiles = netsh wlan show profiles 2>&1 $guestProfiles = ($profiles | Select-String 'All User Profile\s+:\s+(.+)$' | ForEach-Object { $_.Matches.Groups[1].Value.Trim() }) | Where-Object { $_ -match 'guest|visitor|public' } if ($guestProfiles) { [void]$sb.AppendLine("GUEST WIRELESS PROFILES:") foreach ($gp in $guestProfiles) { [void]$sb.AppendLine(" $gp") } } # Network adapter enumeration $adapters = Get-NetAdapter -EA SilentlyContinue $physicalAdapters = $adapters | Where-Object { $_.PhysicalMediaType -and $_.PhysicalMediaType -ne 'Unspecified' } [void]$sb.AppendLine("`nPHYSICAL NETWORK ADAPTERS ($($physicalAdapters.Count)):") foreach ($a in $physicalAdapters) { $ip = (Get-NetIPAddress -InterfaceIndex $a.InterfaceIndex -AddressFamily IPv4 -EA SilentlyContinue).IPAddress [void]$sb.AppendLine(" $($a.Name) | Status:$($a.Status) | IP:$(if($ip){$ip}else{'N/A'}) | VLAN:$(if($a.VlanID){$a.VlanID}else{'None'})") } [void]$sb.AppendLine("`n[!] NETWORK JACK SECURITY CHECKLIST:") [void]$sb.AppendLine(" [ ] Unused wall jacks in public areas disabled at switch") [void]$sb.AppendLine(" [ ] Public area jacks on guest/isolated VLAN") [void]$sb.AppendLine(" [ ] 802.1X authentication required for wired connections") [void]$sb.AppendLine(" [ ] Guest WiFi isolated from production network") @{ Status='Partial'; Findings=$sb.ToString().Trim(); Evidence="Network jack security scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } 'PS06' = @{ Type='Local'; Label='Scan UPS / Power Protection' Script = { $sb = [System.Text.StringBuilder]::new(); $issues = 0 # Check for UPS try { $battery = Get-CimInstance Win32_Battery -EA SilentlyContinue if ($battery) { [void]$sb.AppendLine("BATTERY/UPS DETECTED:") foreach ($b in $battery) { [void]$sb.AppendLine(" $($b.Name) | Status:$($b.BatteryStatus) | Charge:$($b.EstimatedChargeRemaining)% | Runtime:$($b.EstimatedRunTime)min") } } else { [void]$sb.AppendLine("No battery/UPS detected via WMI") } } catch {} # Check for UPS software/services $upsSvcs = Get-Service -EA SilentlyContinue | Where-Object { $_.DisplayName -match 'UPS|APC|CyberPower|Eaton|Liebert|Tripp|NUT|PowerChute|PowerPanel' } if ($upsSvcs) { [void]$sb.AppendLine("`nUPS MANAGEMENT SOFTWARE:") foreach ($us in $upsSvcs) { [void]$sb.AppendLine(" $($us.DisplayName) ($($us.Status))") } } # Check for UPS processes $upsProcs = Get-Process -EA SilentlyContinue | Where-Object { $_.ProcessName -match 'PowerChute|PowerPanel|Eaton|NUT' } if ($upsProcs) { foreach ($up in $upsProcs) { [void]$sb.AppendLine("UPS Process: $($up.ProcessName)") } } # Windows power settings $powerPlan = powercfg /getactivescheme 2>&1 [void]$sb.AppendLine("`nActive Power Plan: $powerPlan") [void]$sb.AppendLine("`n[!] UPS/POWER CHECKLIST:") [void]$sb.AppendLine(" [ ] UPS on all critical infrastructure (servers, switches, firewall)") [void]$sb.AppendLine(" [ ] UPS batteries tested within last 12 months") [void]$sb.AppendLine(" [ ] Graceful shutdown configured on UPS software") [void]$sb.AppendLine(" [ ] Generator available for extended outages (if applicable)") $status = if ($upsSvcs -or $battery) {'Partial'} else {'Fail'} @{ Status=$status; Findings=$sb.ToString().Trim(); Evidence="UPS/power scan @ $(Get-Date -f 'yyyy-MM-dd HH:mm') on $env:COMPUTERNAME" } } } } # Items that have auto-checks available $script:AutoCheckIDs = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($k in $script:AutoChecks.Keys) { $script:AutoCheckIDs.Add($k) | Out-Null } # ── Scan Profiles ──────────────────────────────────────────────────────────── # Quick: ~20 critical/high checks - fast field assessment (15 min) # Standard: ~45 checks - solid audit without the deep dives (30 min) # Full: all 67 checks - comprehensive compliance audit (45-60 min) # ADOnly / LocalOnly: type-filtered subsets $script:ScanProfiles = @{ Quick = @{ Label = 'Quick Assessment (~20 checks, ~15 min)' Description = 'Critical and high-severity checks only. Fast field triage.' IDs = @( 'NP01','NP02','NP07','NP08' # Firewall, open ports, IDS, SSL/TLS 'IA01','IA02','IA03','IA04','IA05' # Admin groups, service accts, MFA, stale, password policy 'EP01','EP02','EP04','EP05' # Defender, patching, BitLocker, firewall status 'LM01','LM02' # Audit config, SIEM 'BR01','BR06' # Backup solution, backup monitoring 'CF01','CF02','CF05' # SMB signing, SMBv1, open shares ) } Standard = @{ Label = 'Standard Audit (~45 checks, ~30 min)' Description = 'All critical/high plus key medium checks. Covers most compliance needs.' IDs = @( 'NP01','NP02','NP03','NP04','NP05','NP07','NP08','NP09','NP10' 'IA01','IA02','IA03','IA04','IA05','IA06','IA07','IA08','IA09' 'EP01','EP02','EP03','EP04','EP05','EP06','EP07','EP08' 'LM01','LM02','LM03','LM04','LM06','LM08' 'BR01','BR02','BR03','BR05','BR06','BR08' 'CF01','CF02','CF03','CF04','CF05' 'NA01','NA02','NA03','NA04' 'PS01','PS04' ) } Full = @{ Label = 'Full Compliance Audit (67 checks, ~60 min)' Description = 'All checks across all categories. Complete NIST/CIS/HIPAA coverage.' IDs = @() # Empty = all checks } ADOnly = @{ Label = 'AD-Focused (domain checks only)' Description = 'Only Active Directory and domain-dependent checks.' IDs = @() # Populated dynamically by Type filter } LocalOnly = @{ Label = 'Local Endpoint (local checks only)' Description = 'Only local machine checks - no AD/domain required.' IDs = @() # Populated dynamically by Type filter } # ── Framework-Specific Profiles ── HIPAA = @{ Label = 'HIPAA Assessment (~45 checks)' Description = 'Checks mapped to HIPAA Security Rule (164.3xx) requirements for healthcare compliance.' IDs = @('IA01','IA02','IA03','IA04','IA05','IA06','IA07','IA08','IA09','IA10','EP01','EP02','EP03','EP04','EP05','EP06','EP07','EP08','EP09','EP10','LM01','LM02','LM03','LM04','LM05','LM06','LM07','LM08','BR01','BR02','BR03','BR04','BR05','BR06','BR07','BR08','CF01','CF02','CF03','CF05','CF07','NP01','NP02','NP08','PS01','PS03','PS04') } PCI = @{ Label = 'PCI-DSS 4.0.1 Scan (~48 checks)' Description = 'Checks mapped to PCI-DSS 4.0.1 requirements for payment card data environments.' IDs = @('NP01','NP02','NP03','NP04','NP05','NP08','NP09','NP10','IA01','IA02','IA03','IA04','IA05','IA06','IA07','IA08','IA09','EP01','EP02','EP03','EP04','EP05','EP06','EP07','EP08','LM01','LM02','LM03','LM04','LM05','LM06','LM07','LM08','NA01','NA02','NA04','BR01','BR02','BR03','BR05','CF01','CF02','CF04','CF05','PS01','PS03','PS04','PS05','PS06') } CMMC = @{ Label = 'CMMC 2.0 Level 2 (all 67 checks)' Description = 'CMMC 2.0 Level 2 maps to NIST 800-171 - full audit coverage required for DoD contractors.' IDs = @() # All checks apply } SOC2 = @{ Label = 'SOC 2 Type II (~60 checks)' Description = 'Checks mapped to SOC 2 Trust Services Criteria (CC/A1) for service organization audits.' IDs = @('IA01','IA02','IA03','IA04','IA05','IA06','IA07','IA08','IA09','IA10','EP01','EP02','EP03','EP04','EP05','EP06','EP07','EP08','EP09','LM01','LM02','LM03','LM04','LM05','LM06','LM07','LM08','NA01','NA02','NA03','NA04','NA05','NA06','NP01','NP02','NP03','NP04','NP05','NP06','NP07','NP08','NP09','NP10','BR01','BR02','BR03','BR04','BR05','BR06','BR07','BR08','CF01','CF02','CF03','CF04','CF05','CF06','CF07','CF08','PS01','PS02','PS03','PS04','PS05','PS06') } ISO27001 = @{ Label = 'ISO 27001:2022 (all 67 checks)' Description = 'Full coverage for ISO 27001:2022 Annex A controls with specific clause mapping.' IDs = @() # All checks apply } } # ── Risk Tier Classification ───────────────────────────────────────────────── # Tier 0: Pure read-only (Get-* cmdlets only) - safe in any environment # Tier 1: Read-only remote (WinRM Get-* on remote targets) # Tier 2: Probing reads (Test-*, connectivity checks, generates log entries) # Tier 3: Potentially modifying (enables services, changes settings) - opt-in only $script:RiskTiers = @{ # ── Identity & Access (all Tier 0-1: pure AD reads) ── 'IA01' = 0; 'IA02' = 0; 'IA03' = 0; 'IA04' = 0; 'IA05' = 0 'IA06' = 0; 'IA07' = 0; 'IA08' = 0; 'IA09' = 0; 'IA10' = 0 # ── Endpoint Security (Tier 0: local reads) ── 'EP01' = 0; 'EP02' = 0; 'EP03' = 0; 'EP04' = 0; 'EP05' = 0 'EP06' = 0; 'EP07' = 0; 'EP08' = 0; 'EP09' = 0; 'EP10' = 0 # ── Logging & Monitoring (Tier 0: event log reads) ── 'LM01' = 0; 'LM02' = 0; 'LM03' = 0; 'LM04' = 0; 'LM05' = 0 'LM06' = 0; 'LM07' = 0; 'LM08' = 0 # ── Network Architecture (Tier 0-1: config reads) ── 'NA01' = 0; 'NA02' = 0; 'NA03' = 0; 'NA04' = 0; 'NA05' = 0 'NA06' = 0; 'NA07' = 0 # ── Network Perimeter (Tier 0: firewall/port reads) ── 'NP01' = 0; 'NP02' = 0; 'NP03' = 0; 'NP04' = 2 # DNS filtering tests resolution 'NP05' = 0; 'NP06' = 0; 'NP07' = 0; 'NP08' = 0 'NP09' = 0; 'NP10' = 0 # ── Backup & Recovery (Tier 0: service/event reads) ── 'BR01' = 0; 'BR02' = 0; 'BR03' = 0; 'BR04' = 0 'BR05' = 0; 'BR06' = 0; 'BR07' = 0; 'BR08' = 0 # ── Common Findings (Tier 0: SMB/config reads) ── 'CF01' = 0; 'CF02' = 0; 'CF03' = 0; 'CF04' = 0 'CF05' = 0; 'CF06' = 0; 'CF07' = 0; 'CF08' = 0 # ── Policies & Standards (Tier 0: policy reads) ── 'PS01' = 0; 'PS02' = 0; 'PS03' = 0; 'PS04' = 0; 'PS05' = 0; 'PS06' = 0 } $script:RiskTierLabels = @{ 0='Read-Only'; 1='Remote Read'; 2='Probing'; 3='Modifying' } # ── Category Risk Weights (for weighted scoring) ──────────────────────────── $script:CategoryWeights = @{ 'Identity & Access' = 1.5 # Most critical - keys to the kingdom 'Endpoint Security' = 1.2 # Direct attack surface 'Network Perimeter' = 1.3 # External exposure 'Logging & Monitoring'= 1.0 # Detection capability 'Network Architecture'= 0.9 # Infrastructure design 'Backup & Recovery' = 1.1 # Resilience 'Common Findings' = 1.0 # Frequent issues 'Physical Security' = 0.7 # Softer controls } # ── Phase 3: Compliance Framework Integration ──────────────────────────────── # Structured mapping of all 67 checks to 7 compliance frameworks with specific control IDs. # NIST CSF, CIS Controls v8, and HIPAA are already in the per-check Compliance string. # This table adds: NIST 800-171 Rev 3, CMMC 2.0, PCI-DSS 4.0.1, SOC 2, ISO 27001:2022 $script:ComplianceTarget = 'All' # Active framework filter: All, CIS, NIST, CMMC, HIPAA, PCI, SOC2, ISO27001 $script:FrameworkMeta = [ordered]@{ 'CIS' = @{ Name='CIS Controls v8.1'; Color='#38bdf8'; Short='CIS' } 'NIST' = @{ Name='NIST 800-171 Rev 3'; Color='#818cf8'; Short='800-171' } 'CMMC' = @{ Name='CMMC 2.0 Level 2'; Color='#a855f7'; Short='CMMC' } 'HIPAA' = @{ Name='HIPAA Security Rule'; Color='#22c55e'; Short='HIPAA' } 'PCI' = @{ Name='PCI-DSS 4.0.1'; Color='#f97316'; Short='PCI' } 'SOC2' = @{ Name='SOC 2 Type II'; Color='#eab308'; Short='SOC2' } 'ISO27001' = @{ Name='ISO 27001:2022'; Color='#ec4899'; Short='ISO' } } # Per-check mapping: each key = check ID, value = hashtable of framework -> control IDs # CIS and HIPAA are parsed from existing Compliance string; these add the remaining 5 frameworks $script:FrameworkMap = @{ # ── Identity & Access ── 'IA01' = @{ 'NIST'='3.1.1, 3.1.2, 3.1.5'; 'CMMC'='AC.L2-3.1.1, AC.L2-3.1.2, AC.L2-3.1.5'; 'PCI'='7.2.1, 7.2.2, 8.6.1'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.5.18, A.8.2' } 'IA02' = @{ 'NIST'='3.1.1, 3.1.5, 3.7.5'; 'CMMC'='AC.L2-3.1.1, AC.L2-3.1.5'; 'PCI'='7.2.2, 8.6.1, 8.6.2'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.5.17, A.8.2' } 'IA03' = @{ 'NIST'='3.5.3, 3.7.5'; 'CMMC'='IA.L2-3.5.3'; 'PCI'='8.4.1, 8.4.2, 8.4.3'; 'SOC2'='CC6.1, CC6.6'; 'ISO27001'='A.5.17, A.8.5' } 'IA04' = @{ 'NIST'='3.1.1, 3.1.12'; 'CMMC'='AC.L2-3.1.1, PS.L2-3.9.2'; 'PCI'='8.1.4, 8.2.6'; 'SOC2'='CC6.1, CC6.2'; 'ISO27001'='A.5.18, A.6.5' } 'IA05' = @{ 'NIST'='3.5.7, 3.5.8, 3.5.9, 3.5.10'; 'CMMC'='IA.L2-3.5.7, IA.L2-3.5.8'; 'PCI'='8.3.6, 8.3.7, 8.3.9'; 'SOC2'='CC6.1'; 'ISO27001'='A.5.17, A.8.5' } 'IA06' = @{ 'NIST'='3.1.5, 3.1.6, 3.1.7'; 'CMMC'='AC.L2-3.1.5, AC.L2-3.1.6, AC.L2-3.1.7'; 'PCI'='7.2.1, 8.2.4'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.8.2, A.8.18' } 'IA07' = @{ 'NIST'='3.1.1, 3.5.1'; 'CMMC'='AC.L2-3.1.1, IA.L2-3.5.1'; 'PCI'='8.2.1, 8.2.2'; 'SOC2'='CC6.1'; 'ISO27001'='A.5.15, A.5.17' } 'IA08' = @{ 'NIST'='3.1.1, 3.1.12'; 'CMMC'='AC.L2-3.1.1, PS.L2-3.9.2'; 'PCI'='8.1.4, 8.6.1'; 'SOC2'='CC6.1, CC6.2'; 'ISO27001'='A.5.18, A.5.19, A.5.20' } 'IA09' = @{ 'NIST'='3.1.3, 3.5.3'; 'CMMC'='AC.L2-3.1.3, IA.L2-3.5.3'; 'PCI'='7.2.1, 8.4.1'; 'SOC2'='CC6.1, CC6.6'; 'ISO27001'='A.5.15, A.8.5' } 'IA10' = @{ 'NIST'='3.1.1, 3.1.12'; 'CMMC'='AC.L2-3.1.1'; 'PCI'='8.2.6'; 'SOC2'='CC6.1, CC6.2'; 'ISO27001'='A.5.18, A.6.5' } # ── Endpoint Security ── 'EP01' = @{ 'NIST'='3.14.1, 3.14.2, 3.14.4, 3.14.5'; 'CMMC'='SI.L2-3.14.1, SI.L2-3.14.2'; 'PCI'='5.2.1, 5.2.2, 5.3.1, 5.3.2'; 'SOC2'='CC6.8, CC7.1'; 'ISO27001'='A.8.7' } 'EP02' = @{ 'NIST'='3.8.6, 3.13.11'; 'CMMC'='MP.L2-3.8.6, SC.L2-3.13.11'; 'PCI'='3.5.1, 9.4.1'; 'SOC2'='CC6.1, CC6.7'; 'ISO27001'='A.8.24' } 'EP03' = @{ 'NIST'='3.1.13, 3.13.1, 3.13.8'; 'CMMC'='AC.L2-3.1.13, SC.L2-3.13.1'; 'PCI'='2.2.7, 4.2.1'; 'SOC2'='CC6.1, CC6.7'; 'ISO27001'='A.8.20, A.8.24' } 'EP04' = @{ 'NIST'='3.14.1, 3.4.8, 3.4.9'; 'CMMC'='SI.L2-3.14.1, CM.L2-3.4.8'; 'PCI'='6.3.1, 6.3.3'; 'SOC2'='CC7.1, CC8.1'; 'ISO27001'='A.8.8, A.8.19' } 'EP05' = @{ 'NIST'='3.1.5, 3.1.6, 3.4.6'; 'CMMC'='AC.L2-3.1.5, AC.L2-3.1.6'; 'PCI'='7.2.1, 7.2.2'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.8.2' } 'EP06' = @{ 'NIST'='3.13.1, 3.13.5'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.5'; 'PCI'='1.2.1, 1.3.1, 1.4.1'; 'SOC2'='CC6.1, CC6.6'; 'ISO27001'='A.8.20, A.8.21' } 'EP07' = @{ 'NIST'='3.4.6, 3.4.8'; 'CMMC'='CM.L2-3.4.6, CM.L2-3.4.8'; 'PCI'='2.2.4, 6.3.2'; 'SOC2'='CC6.8, CC7.1'; 'ISO27001'='A.8.7, A.8.19' } 'EP08' = @{ 'NIST'='3.13.11, 3.14.1'; 'CMMC'='SC.L2-3.13.11, SI.L2-3.14.1'; 'PCI'='9.4.1, 2.2.1'; 'SOC2'='CC6.1, CC6.7'; 'ISO27001'='A.8.1, A.8.24' } 'EP09' = @{ 'NIST'='3.4.1, 3.4.2'; 'CMMC'='CM.L2-3.4.1, CM.L2-3.4.2'; 'PCI'='2.2.1, 2.2.2'; 'SOC2'='CC6.1, CC8.1'; 'ISO27001'='A.8.9, A.8.19' } 'EP10' = @{ 'NIST'='3.8.9'; 'CMMC'='MP.L2-3.8.9'; 'PCI'='9.4.1, 9.4.5'; 'SOC2'='CC6.7'; 'ISO27001'='A.7.9, A.8.1' } # ── Logging & Monitoring ── 'LM01' = @{ 'NIST'='3.3.1, 3.3.2'; 'CMMC'='AU.L2-3.3.1, AU.L2-3.3.2'; 'PCI'='10.2.1, 10.2.2'; 'SOC2'='CC7.2, CC7.3'; 'ISO27001'='A.8.15, A.8.16' } 'LM02' = @{ 'NIST'='3.3.1, 3.3.4'; 'CMMC'='AU.L2-3.3.1, AU.L2-3.3.4'; 'PCI'='10.3.1, 10.3.3'; 'SOC2'='CC7.2, CC7.3'; 'ISO27001'='A.8.15, A.8.16' } 'LM03' = @{ 'NIST'='3.3.1, 3.3.2, 3.3.8'; 'CMMC'='AU.L2-3.3.1, AU.L2-3.3.2'; 'PCI'='10.2.1, 10.2.2, 10.6.3'; 'SOC2'='CC7.2, CC7.3'; 'ISO27001'='A.8.15, A.8.16' } 'LM04' = @{ 'NIST'='3.3.1, 3.13.1'; 'CMMC'='AU.L2-3.3.1, SC.L2-3.13.1'; 'PCI'='10.2.1, 1.2.1'; 'SOC2'='CC7.2'; 'ISO27001'='A.8.15, A.8.20' } 'LM05' = @{ 'NIST'='3.3.3, 3.3.4'; 'CMMC'='AU.L2-3.3.3, AU.L2-3.3.4'; 'PCI'='10.3.1, 10.3.2'; 'SOC2'='CC7.2, CC7.3'; 'ISO27001'='A.8.15, A.8.16' } 'LM06' = @{ 'NIST'='3.3.5'; 'CMMC'='AU.L2-3.3.5'; 'PCI'='10.3.4, 10.5.1'; 'SOC2'='CC7.2, CC7.4'; 'ISO27001'='A.8.15' } 'LM07' = @{ 'NIST'='3.3.4, 3.3.8'; 'CMMC'='AU.L2-3.3.4, AU.L2-3.3.8'; 'PCI'='10.5.1, 10.7.1'; 'SOC2'='CC7.2'; 'ISO27001'='A.8.15' } 'LM08' = @{ 'NIST'='3.3.1, 3.6.1'; 'CMMC'='AU.L2-3.3.1, IR.L2-3.6.1'; 'PCI'='10.4.1, 10.7.2'; 'SOC2'='CC7.2, CC7.3'; 'ISO27001'='A.8.15, A.8.16' } # ── Network Architecture ── 'NA01' = @{ 'NIST'='3.13.1, 3.13.2'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.2'; 'PCI'='1.2.1, 1.3.1, 1.3.2'; 'SOC2'='CC6.1, CC6.6'; 'ISO27001'='A.8.20, A.8.22' } 'NA02' = @{ 'NIST'='3.13.1, 3.13.2'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.2'; 'PCI'='1.2.1, 1.3.1'; 'SOC2'='CC6.1'; 'ISO27001'='A.8.20, A.8.22' } 'NA03' = @{ 'NIST'='3.13.2, 3.13.6'; 'CMMC'='SC.L2-3.13.2, SC.L2-3.13.6'; 'PCI'='1.2.1, 1.3.2'; 'SOC2'='CC6.6'; 'ISO27001'='A.8.20' } 'NA04' = @{ 'NIST'='3.13.1, 3.13.7'; 'CMMC'='SC.L2-3.13.1'; 'PCI'='11.3.1, 11.3.2'; 'SOC2'='CC7.1'; 'ISO27001'='A.8.20, A.8.21' } 'NA05' = @{ 'NIST'='3.1.20'; 'CMMC'='AC.L2-3.1.20'; 'PCI'='1.4.1'; 'SOC2'='CC6.6'; 'ISO27001'='A.8.20' } 'NA06' = @{ 'NIST'='3.13.3'; 'CMMC'='SC.L2-3.13.3'; 'PCI'='11.4.1'; 'SOC2'='CC7.1, CC7.2'; 'ISO27001'='A.8.16, A.8.23' } 'NA07' = @{ 'NIST'='3.13.1'; 'CMMC'='SC.L2-3.13.1'; 'PCI'='1.2.5'; 'SOC2'='CC6.6'; 'ISO27001'='A.8.20' } # ── Network Perimeter ── 'NP01' = @{ 'NIST'='3.13.1, 3.13.5'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.5'; 'PCI'='1.2.1, 1.3.1, 1.4.1'; 'SOC2'='CC6.1, CC6.6'; 'ISO27001'='A.8.20, A.8.21' } 'NP02' = @{ 'NIST'='3.13.1, 3.13.5'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.5'; 'PCI'='11.3.1, 11.3.2'; 'SOC2'='CC6.6, CC7.1'; 'ISO27001'='A.8.20, A.8.34' } 'NP03' = @{ 'NIST'='3.1.12, 3.1.20'; 'CMMC'='AC.L2-3.1.12, AC.L2-3.1.20'; 'PCI'='1.4.1, 8.2.1'; 'SOC2'='CC6.1, CC6.6'; 'ISO27001'='A.8.20' } 'NP04' = @{ 'NIST'='3.13.1, 3.13.15'; 'CMMC'='SC.L2-3.13.1'; 'PCI'='1.2.5, 11.5.1'; 'SOC2'='CC6.6, CC6.8'; 'ISO27001'='A.8.20, A.8.23' } 'NP05' = @{ 'NIST'='3.13.1, 3.13.6'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.6'; 'PCI'='1.2.1, 1.3.1'; 'SOC2'='CC6.6'; 'ISO27001'='A.8.20, A.8.21' } 'NP06' = @{ 'NIST'='3.13.1, 3.13.8'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.8'; 'PCI'='11.5.1'; 'SOC2'='CC6.6, CC7.1'; 'ISO27001'='A.8.20, A.8.21' } 'NP07' = @{ 'NIST'='3.13.1, 3.14.6'; 'CMMC'='SC.L2-3.13.1, SI.L2-3.14.6'; 'PCI'='11.5.1, 11.6.1'; 'SOC2'='CC6.8, CC7.1'; 'ISO27001'='A.8.16, A.8.23' } 'NP08' = @{ 'NIST'='3.13.8, 3.13.11'; 'CMMC'='SC.L2-3.13.8, SC.L2-3.13.11'; 'PCI'='4.2.1, 4.2.2'; 'SOC2'='CC6.1, CC6.7'; 'ISO27001'='A.8.24' } 'NP09' = @{ 'NIST'='3.13.1, 3.13.5'; 'CMMC'='SC.L2-3.13.1, SC.L2-3.13.5'; 'PCI'='1.2.1, 1.3.1'; 'SOC2'='CC6.6'; 'ISO27001'='A.8.20' } 'NP10' = @{ 'NIST'='3.4.8, 3.14.1'; 'CMMC'='CM.L2-3.4.8, SI.L2-3.14.1'; 'PCI'='6.3.1, 6.3.3'; 'SOC2'='CC7.1, CC8.1'; 'ISO27001'='A.8.8, A.8.19' } # ── Backup & Recovery ── 'BR01' = @{ 'NIST'='3.8.9'; 'CMMC'='MP.L2-3.8.9'; 'PCI'='12.10.1'; 'SOC2'='CC7.5, A1.2'; 'ISO27001'='A.8.13' } 'BR02' = @{ 'NIST'='3.8.9'; 'CMMC'='MP.L2-3.8.9'; 'PCI'='12.10.1'; 'SOC2'='CC7.5, A1.2'; 'ISO27001'='A.8.13, A.8.14' } 'BR03' = @{ 'NIST'='3.6.1, 3.6.2'; 'CMMC'='IR.L2-3.6.1, IR.L2-3.6.2'; 'PCI'='12.10.1, 12.10.2'; 'SOC2'='CC7.4, CC7.5, A1.2'; 'ISO27001'='A.5.29, A.5.30, A.8.14' } 'BR04' = @{ 'NIST'='3.8.9'; 'CMMC'='MP.L2-3.8.9'; 'PCI'='12.10.1'; 'SOC2'='A1.2'; 'ISO27001'='A.8.13' } 'BR05' = @{ 'NIST'='3.6.1, 3.6.3'; 'CMMC'='IR.L2-3.6.1, IR.L2-3.6.3'; 'PCI'='12.10.1, 12.10.2'; 'SOC2'='CC7.4, CC7.5, A1.2'; 'ISO27001'='A.5.29, A.5.30' } 'BR06' = @{ 'NIST'='3.8.9'; 'CMMC'='MP.L2-3.8.9'; 'PCI'='12.10.1'; 'SOC2'='A1.2, A1.3'; 'ISO27001'='A.8.13' } 'BR07' = @{ 'NIST'='3.13.11'; 'CMMC'='SC.L2-3.13.11'; 'PCI'='3.5.1, 9.4.1'; 'SOC2'='CC6.7, A1.2'; 'ISO27001'='A.8.13, A.8.24' } 'BR08' = @{ 'NIST'='3.6.1'; 'CMMC'='IR.L2-3.6.1'; 'PCI'='12.10.1'; 'SOC2'='CC7.5, A1.2'; 'ISO27001'='A.5.29, A.8.13' } # ── Common Findings ── 'CF01' = @{ 'NIST'='3.1.5, 3.7.5, 3.13.8'; 'CMMC'='AC.L2-3.1.5, SC.L2-3.13.8'; 'PCI'='7.2.2, 8.6.1, 8.6.2'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.5.17, A.8.5' } 'CF02' = @{ 'NIST'='3.4.6, 3.4.7'; 'CMMC'='CM.L2-3.4.6, CM.L2-3.4.7'; 'PCI'='2.2.4, 2.2.7'; 'SOC2'='CC6.1, CC6.8'; 'ISO27001'='A.8.19, A.8.20' } 'CF03' = @{ 'NIST'='3.2.1, 3.2.2'; 'CMMC'='AT.L2-3.2.1, AT.L2-3.2.2'; 'PCI'='12.6.1, 12.6.2'; 'SOC2'='CC1.4, CC2.2'; 'ISO27001'='A.6.3' } 'CF04' = @{ 'NIST'='3.1.1, 3.1.2'; 'CMMC'='AC.L2-3.1.1, AC.L2-3.1.2'; 'PCI'='7.2.1, 7.2.4'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.8.3' } 'CF05' = @{ 'NIST'='3.1.1, 3.8.1'; 'CMMC'='AC.L2-3.1.1, MP.L2-3.8.1'; 'PCI'='7.2.4'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.8.3' } 'CF06' = @{ 'NIST'='3.1.17'; 'CMMC'='AC.L2-3.1.17'; 'PCI'='7.2.5'; 'SOC2'='CC6.1'; 'ISO27001'='A.5.15, A.8.20' } 'CF07' = @{ 'NIST'='3.1.5, 3.1.6'; 'CMMC'='AC.L2-3.1.5, AC.L2-3.1.6'; 'PCI'='7.2.1, 7.2.2'; 'SOC2'='CC6.1, CC6.3'; 'ISO27001'='A.5.15, A.8.2' } 'CF08' = @{ 'NIST'='3.14.1, 3.14.6'; 'CMMC'='SI.L2-3.14.1, SI.L2-3.14.6'; 'PCI'='5.2.1, 11.5.1'; 'SOC2'='CC7.1, CC7.2'; 'ISO27001'='A.8.7, A.8.8' } # ── Policies & Standards ── 'PS01' = @{ 'NIST'='3.12.1, 3.12.4'; 'CMMC'='CA.L2-3.12.1, CA.L2-3.12.4'; 'PCI'='12.1.1, 12.1.2'; 'SOC2'='CC1.1, CC1.2, CC5.2'; 'ISO27001'='A.5.1, A.5.2' } 'PS02' = @{ 'NIST'='3.12.1, 3.12.3'; 'CMMC'='CA.L2-3.12.1, CA.L2-3.12.3'; 'PCI'='12.1.1'; 'SOC2'='CC1.1, CC1.2'; 'ISO27001'='A.5.1' } 'PS03' = @{ 'NIST'='3.6.1, 3.6.2, 3.6.3'; 'CMMC'='IR.L2-3.6.1, IR.L2-3.6.2, IR.L2-3.6.3'; 'PCI'='12.10.1, 12.10.2'; 'SOC2'='CC7.3, CC7.4, CC7.5'; 'ISO27001'='A.5.24, A.5.25, A.5.26' } 'PS04' = @{ 'NIST'='3.12.1'; 'CMMC'='CA.L2-3.12.1'; 'PCI'='12.4.1'; 'SOC2'='CC1.1, CC4.1, CC4.2'; 'ISO27001'='A.5.35, A.5.36' } 'PS05' = @{ 'NIST'='3.12.2'; 'CMMC'='CA.L2-3.12.2'; 'PCI'='12.1.2, 12.3.1'; 'SOC2'='CC3.1, CC3.2'; 'ISO27001'='A.5.7, A.5.8' } 'PS06' = @{ 'NIST'='3.2.1, 3.2.2'; 'CMMC'='AT.L2-3.2.1, AT.L2-3.2.2'; 'PCI'='12.6.1, 12.6.2, 12.6.3'; 'SOC2'='CC1.4, CC2.2'; 'ISO27001'='A.6.3' } } # Checks relevant to each framework (for framework-specific scan profiles) $script:FrameworkChecks = @{ 'CIS' = @($script:FrameworkMap.Keys) # CIS covers all checks 'NIST' = @($script:FrameworkMap.Keys | Where-Object { $script:FrameworkMap[$_].NIST }) 'CMMC' = @($script:FrameworkMap.Keys | Where-Object { $script:FrameworkMap[$_].CMMC }) 'HIPAA' = @('IA01','IA02','IA03','IA04','IA05','IA06','IA07','IA08','IA09','IA10','EP01','EP02','EP03','EP04','EP05','EP06','EP07','EP08','EP09','EP10','LM01','LM02','LM03','LM04','LM05','LM06','LM07','LM08','BR01','BR02','BR03','BR04','BR05','BR06','BR07','BR08','CF01','CF02','CF03','CF05','CF07','NP01','NP02','NP08','PS01','PS03','PS04') 'PCI' = @('NP01','NP02','NP03','NP04','NP05','NP08','NP09','NP10','IA01','IA02','IA03','IA04','IA05','IA06','IA07','IA08','IA09','EP01','EP02','EP03','EP04','EP05','EP06','EP07','EP08','LM01','LM02','LM03','LM04','LM05','LM06','LM07','LM08','NA01','NA02','NA04','BR01','BR02','BR03','BR05','CF01','CF02','CF04','CF05','PS01','PS03','PS04','PS05','PS06') 'SOC2' = @('IA01','IA02','IA03','IA04','IA05','IA06','IA07','IA08','IA09','IA10','EP01','EP02','EP03','EP04','EP05','EP06','EP07','EP08','EP09','LM01','LM02','LM03','LM04','LM05','LM06','LM07','LM08','NA01','NA02','NA03','NA04','NA05','NA06','NP01','NP02','NP03','NP04','NP05','NP06','NP07','NP08','NP09','NP10','BR01','BR02','BR03','BR04','BR05','BR06','BR07','BR08','CF01','CF02','CF03','CF04','CF05','CF06','CF07','CF08','PS01','PS02','PS03','PS04','PS05','PS06') 'ISO27001' = @($script:FrameworkMap.Keys) # ISO 27001 covers all checks } # Helper: Get formatted compliance string for a check ID and optional framework filter function Get-ComplianceString { param([string]$CheckID, [string]$Framework = 'All') $parts = @() # Always include the built-in Compliance string (NIST CSF, CIS, HIPAA) parsed from check item $item = $null foreach ($cn in $script:AuditCategories.Keys) { $item = $script:AuditCategories[$cn].Items | Where-Object { $_.ID -eq $CheckID } if ($item) { break } } $builtIn = if ($item) { $item.Compliance } else { '' } if ($Framework -eq 'All' -or $Framework -eq 'CIS') { if ($builtIn -match 'CIS Control ([^|]+)') { $parts += "CIS: $($Matches[1].Trim())" } } if ($Framework -eq 'All' -or $Framework -eq 'NIST') { if ($builtIn -match 'NIST CSF ([^|]+)') { $parts += "NIST CSF: $($Matches[1].Trim())" } if ($script:FrameworkMap.Contains($CheckID) -and $script:FrameworkMap[$CheckID].NIST) { $parts += "800-171: $($script:FrameworkMap[$CheckID].NIST)" } } if ($Framework -eq 'All' -or $Framework -eq 'CMMC') { if ($script:FrameworkMap.Contains($CheckID) -and $script:FrameworkMap[$CheckID].CMMC) { $parts += "CMMC: $($script:FrameworkMap[$CheckID].CMMC)" } } if ($Framework -eq 'All' -or $Framework -eq 'HIPAA') { if ($builtIn -match 'HIPAA (.+)$') { $parts += "HIPAA: $($Matches[1].Trim())" } } if ($Framework -eq 'All' -or $Framework -eq 'PCI') { if ($script:FrameworkMap.Contains($CheckID) -and $script:FrameworkMap[$CheckID].PCI) { $parts += "PCI: $($script:FrameworkMap[$CheckID].PCI)" } } if ($Framework -eq 'All' -or $Framework -eq 'SOC2') { if ($script:FrameworkMap.Contains($CheckID) -and $script:FrameworkMap[$CheckID].SOC2) { $parts += "SOC2: $($script:FrameworkMap[$CheckID].SOC2)" } } if ($Framework -eq 'All' -or $Framework -eq 'ISO27001') { if ($script:FrameworkMap.Contains($CheckID) -and $script:FrameworkMap[$CheckID].ISO27001) { $parts += "ISO: $($script:FrameworkMap[$CheckID].ISO27001)" } } return ($parts -join ' | ') } # Framework-specific scoring: calculate pass/fail/partial per framework function Get-FrameworkScores { param([string]$Framework = 'All') $frameworks = if ($Framework -eq 'All') { $script:FrameworkMeta.Keys } else { @($Framework) } $scores = @{} foreach ($fw in $frameworks) { $checkIds = if ($script:FrameworkChecks.Contains($fw)) { $script:FrameworkChecks[$fw] } else { @() } $pass = 0; $fail = 0; $partial = 0; $na = 0; $notAssessed = 0 foreach ($id in $checkIds) { $sv = if ($script:StatusCombos[$id] -and $script:StatusCombos[$id].SelectedItem) { $script:StatusCombos[$id].SelectedItem.ToString() } else { 'Not Assessed' } switch ($sv) { 'Pass' { $pass++ } 'Fail' { $fail++ } 'Partial' { $partial++ } 'N/A' { $na++ } default { $notAssessed++ } } } $assessed = $pass + $fail + $partial $score = if ($assessed -gt 0) { [math]::Round(($pass + $partial * 0.5) / $assessed * 100) } else { 0 } $total = $checkIds.Count $scores[$fw] = @{ Pass=$pass; Fail=$fail; Partial=$partial; NA=$na; NotAssessed=$notAssessed; Assessed=$assessed; Total=$total; Score=$score } } return $scores } # ── End Phase 3A ───────────────────────────────────────────────────────────── # ── Phase 4A: MITRE ATT&CK Mapping ────────────────────────────────────────── # Maps all 67 checks to ATT&CK Enterprise techniques (v15.1) # Format: CheckID -> @{ Tactics=@('TA00xx',...); Techniques=@('T1xxx',...); Desc='short attack context' } $script:MitreMap = @{ # ── Identity & Access ── 'IA01' = @{ Tactics=@('TA0004','TA0003'); Techniques=@('T1078.002','T1078.001','T1098'); Desc='Compromised DA accounts enable domain-wide persistence and privilege escalation' } 'IA02' = @{ Tactics=@('TA0006','TA0004'); Techniques=@('T1558.003','T1558.004','T1078.002'); Desc='Service accounts with SPNs are Kerberoastable; stale passwords make cracking trivial' } 'IA03' = @{ Tactics=@('TA0001','TA0006'); Techniques=@('T1078','T1110.001','T1110.003','T1556'); Desc='Missing MFA allows credential stuffing, password spraying, and phishing-to-access' } 'IA04' = @{ Tactics=@('TA0001','TA0003'); Techniques=@('T1078.002','T1078.001'); Desc='Stale accounts from terminated employees are prime targets for unauthorized access' } 'IA05' = @{ Tactics=@('TA0006','TA0001'); Techniques=@('T1110.001','T1110.002','T1110.003'); Desc='Weak password policy enables brute force, dictionary attacks, and credential spraying' } 'IA06' = @{ Tactics=@('TA0004','TA0003','TA0006'); Techniques=@('T1078.002','T1550.002','T1550.003'); Desc='Without PAM/LAPS, lateral movement via pass-the-hash and golden ticket attacks' } 'IA07' = @{ Tactics=@('TA0001','TA0005'); Techniques=@('T1078','T1078.001'); Desc='Shared accounts eliminate attribution and enable insider threat denial' } 'IA08' = @{ Tactics=@('TA0001','TA0003'); Techniques=@('T1078','T1199'); Desc='Vendor accounts with persistent access enable trusted relationship attacks' } 'IA09' = @{ Tactics=@('TA0001','TA0005'); Techniques=@('T1078.004','T1556.006'); Desc='Missing conditional access allows cloud compromise from any device/location' } 'IA10' = @{ Tactics=@('TA0001','TA0003'); Techniques=@('T1078','T1078.002'); Desc='Stale accounts expand the attack surface for credential-based initial access' } # ── Endpoint Security ── 'EP01' = @{ Tactics=@('TA0005','TA0002'); Techniques=@('T1562.001','T1562.004','T1059'); Desc='Disabled/misconfigured AV allows malware execution, defense evasion, and payload delivery' } 'EP02' = @{ Tactics=@('TA0005','TA0002'); Techniques=@('T1486','T1059'); Desc='Missing encryption exposes data at rest; enables theft on stolen/decommissioned devices' } 'EP03' = @{ Tactics=@('TA0006','TA0008','TA0005'); Techniques=@('T1557.001','T1040','T1570','T1187'); Desc='SMB/NTLM misconfig enables relay attacks, credential capture, and lateral tool transfer' } 'EP04' = @{ Tactics=@('TA0001','TA0002'); Techniques=@('T1190','T1203','T1210'); Desc='Unpatched systems enable exploitation of public-facing apps, client-side vulns, and remote services' } 'EP05' = @{ Tactics=@('TA0004','TA0003','TA0002'); Techniques=@('T1574.009','T1574.001','T1547.001','T1053'); Desc='Unquoted service paths, AlwaysInstallElevated, cached creds enable local privesc and persistence' } 'EP06' = @{ Tactics=@('TA0005','TA0011'); Techniques=@('T1562.004','T1071','T1048'); Desc='Firewall gaps allow C2 communication, data exfiltration, and inbound exploitation' } 'EP07' = @{ Tactics=@('TA0002','TA0005'); Techniques=@('T1059','T1204.002','T1137','T1221'); Desc='Missing AppLocker/WDAC and unrestricted macros enable arbitrary code execution and initial access via documents' } 'EP08' = @{ Tactics=@('TA0006','TA0005','TA0004'); Techniques=@('T1003.001','T1003.004','T1003.005','T1547.008'); Desc='Missing Credential Guard/LSA Protection enables LSASS dumping, DCSync, and credential theft' } 'EP09' = @{ Tactics=@('TA0005','TA0003'); Techniques=@('T1562.001','T1112'); Desc='Misconfigured systems expand attack surface through unnecessary services and weak defaults' } 'EP10' = @{ Tactics=@('TA0005','TA0010'); Techniques=@('T1091','T1052'); Desc='Uncontrolled removable media enables physical delivery of malware and data exfiltration' } # ── Logging & Monitoring ── 'LM01' = @{ Tactics=@('TA0005'); Techniques=@('T1562.002','T1070.001'); Desc='Inadequate audit policy creates blind spots; attackers operate undetected' } 'LM02' = @{ Tactics=@('TA0005','TA0040'); Techniques=@('T1562.002','T1485'); Desc='No SIEM means no correlation, alerting, or forensic capability during active compromise' } 'LM03' = @{ Tactics=@('TA0002','TA0005'); Techniques=@('T1059.001','T1059.003','T1562.002','T1070'); Desc='Missing PS logging/auditing allows script-based attacks to execute without trace' } 'LM04' = @{ Tactics=@('TA0005','TA0011'); Techniques=@('T1562.002','T1071'); Desc='No firewall/IDS logging means network-based attacks go undetected' } 'LM05' = @{ Tactics=@('TA0005'); Techniques=@('T1562.002','T1070.001','T1070.002'); Desc='Logs without integrity protection can be tampered with to cover tracks' } 'LM06' = @{ Tactics=@('TA0005'); Techniques=@('T1070.001','T1562.002'); Desc='Missing log review means alerts are generated but never acted upon' } 'LM07' = @{ Tactics=@('TA0005'); Techniques=@('T1070.001','T1562.002'); Desc='Small log sizes cause critical events to be overwritten before detection' } 'LM08' = @{ Tactics=@('TA0005','TA0011'); Techniques=@('T1562.002','T1071'); Desc='Missing alerting means real-time attacks proceed without response' } # ── Network Architecture ── 'NA01' = @{ Tactics=@('TA0008'); Techniques=@('T1021','T1570','T1210'); Desc='Flat networks enable unrestricted lateral movement after initial compromise' } 'NA02' = @{ Tactics=@('TA0008','TA0011'); Techniques=@('T1021','T1071'); Desc='Missing segmentation between client/server tiers enables lateral movement to high-value targets' } 'NA03' = @{ Tactics=@('TA0008','TA0011'); Techniques=@('T1021','T1071','T1048'); Desc='No DMZ exposes internal services directly and enables pivot from compromised public services' } 'NA04' = @{ Tactics=@('TA0008','TA0011'); Techniques=@('T1021','T1071'); Desc='Missing wireless segmentation enables network pivot from compromised WiFi clients' } 'NA05' = @{ Tactics=@('TA0001','TA0011'); Techniques=@('T1133','T1071'); Desc='VPN without segmentation grants full network access on compromise' } 'NA06' = @{ Tactics=@('TA0008','TA0040'); Techniques=@('T1021','T1570','T1210'); Desc='Missing IDS/monitoring means lateral movement and exploitation go undetected' } 'NA07' = @{ Tactics=@('TA0011','TA0010'); Techniques=@('T1071','T1048','T1568'); Desc='Missing DNS filtering allows C2 channels, data exfil via DNS, and drive-by downloads' } # ── Network Perimeter ── 'NP01' = @{ Tactics=@('TA0005','TA0011'); Techniques=@('T1562.004','T1071'); Desc='Weak firewall rules expose attack surface and allow C2/exfil channels' } 'NP02' = @{ Tactics=@('TA0001','TA0043'); Techniques=@('T1190','T1046'); Desc='Open ports expose services to exploitation and enable reconnaissance' } 'NP03' = @{ Tactics=@('TA0001','TA0008'); Techniques=@('T1133','T1021.001'); Desc='Exposed RDP/remote access enables brute force and RDP-based ransomware delivery' } 'NP04' = @{ Tactics=@('TA0001','TA0005'); Techniques=@('T1190','T1562.004'); Desc='WAF/edge protection gaps allow web app exploitation and injection attacks' } 'NP05' = @{ Tactics=@('TA0001','TA0008'); Techniques=@('T1190','T1210'); Desc='Permissive ACLs expose internal services to external exploitation' } 'NP06' = @{ Tactics=@('TA0001','TA0011'); Techniques=@('T1190','T1071.001'); Desc='Missing SSL inspection allows encrypted C2, malware delivery, and data exfiltration' } 'NP07' = @{ Tactics=@('TA0005','TA0011'); Techniques=@('T1071','T1568','T1562.004'); Desc='No IDS/IPS means network-level attacks bypass perimeter undetected' } 'NP08' = @{ Tactics=@('TA0006','TA0009'); Techniques=@('T1557','T1040','T1552.001'); Desc='Weak TLS/SSL enables credential interception, MitM, and data collection from encrypted channels' } 'NP09' = @{ Tactics=@('TA0001','TA0008'); Techniques=@('T1190','T1021'); Desc='Unnecessary NAT/port forwards expose internal hosts to direct exploitation' } 'NP10' = @{ Tactics=@('TA0001','TA0002'); Techniques=@('T1190','T1210'); Desc='Unpatched perimeter firmware contains known exploitable vulnerabilities' } # ── Backup & Recovery ── 'BR01' = @{ Tactics=@('TA0040'); Techniques=@('T1486','T1490','T1485'); Desc='No backup means ransomware encryption is catastrophic with no recovery path' } 'BR02' = @{ Tactics=@('TA0040'); Techniques=@('T1486','T1490'); Desc='Backups without offsite/immutable copies are destroyed alongside primary in ransomware attacks' } 'BR03' = @{ Tactics=@('TA0040'); Techniques=@('T1486','T1490','T1485'); Desc='No DR plan means extended downtime and uncoordinated recovery during incidents' } 'BR04' = @{ Tactics=@('TA0040'); Techniques=@('T1486','T1490'); Desc='Untested backups may fail during actual recovery, extending downtime' } 'BR05' = @{ Tactics=@('TA0040'); Techniques=@('T1486','T1489'); Desc='No documented RTO/RPO means no recovery time expectations or prioritization' } 'BR06' = @{ Tactics=@('TA0040'); Techniques=@('T1490','T1486'); Desc='Unmonitored backup failures mean data loss is discovered only during recovery attempt' } 'BR07' = @{ Tactics=@('TA0040','TA0010'); Techniques=@('T1486','T1048'); Desc='Unencrypted backups expose sensitive data if storage is compromised or stolen' } 'BR08' = @{ Tactics=@('TA0040'); Techniques=@('T1486','T1490','T1561'); Desc='Missing backup for critical systems means targeted destruction is irrecoverable' } # ── Common Findings ── 'CF01' = @{ Tactics=@('TA0006','TA0004','TA0003'); Techniques=@('T1558.003','T1078.002','T1098'); Desc='DA service accounts, missing LAPS, GPP passwords, ADCS vulns enable domain compromise chains' } 'CF02' = @{ Tactics=@('TA0008','TA0005'); Techniques=@('T1021.002','T1570'); Desc='SMBv1 and legacy protocols enable EternalBlue-class exploits and relay attacks' } 'CF03' = @{ Tactics=@('TA0001','TA0043'); Techniques=@('T1566.001','T1566.002','T1598'); Desc='Untrained users fall for phishing, social engineering, and credential harvesting campaigns' } 'CF04' = @{ Tactics=@('TA0009','TA0010'); Techniques=@('T1005','T1039','T1048'); Desc='Excessive permissions enable data collection from shared drives and data exfiltration' } 'CF05' = @{ Tactics=@('TA0009','TA0010'); Techniques=@('T1039','T1005','T1048'); Desc='Open shares expose sensitive data for collection and enable lateral data access' } 'CF06' = @{ Tactics=@('TA0008','TA0011'); Techniques=@('T1021.001','T1071'); Desc='Unrestricted remote access enables lateral movement and persistent C2 channels' } 'CF07' = @{ Tactics=@('TA0004','TA0008'); Techniques=@('T1078.001','T1021'); Desc='Excessive local admin rights enable privilege escalation and lateral movement' } 'CF08' = @{ Tactics=@('TA0001','TA0005'); Techniques=@('T1190','T1211','T1562.001'); Desc='Missing vulnerability management leaves known CVEs exploitable across the environment' } # ── Policies & Standards ── 'PS01' = @{ Tactics=@('TA0001','TA0042'); Techniques=@('T1078','T1595'); Desc='Missing security policies leave the organization without defined security posture or baselines' } 'PS02' = @{ Tactics=@('TA0042'); Techniques=@('T1595','T1589'); Desc='No AUP means no policy enforcement for acceptable behavior and security expectations' } 'PS03' = @{ Tactics=@('TA0040','TA0042'); Techniques=@('T1486','T1489','T1485'); Desc='Missing IR plan means uncoordinated, delayed response to active breaches' } 'PS04' = @{ Tactics=@('TA0042'); Techniques=@('T1595'); Desc='No compliance monitoring means security drift goes undetected over time' } 'PS05' = @{ Tactics=@('TA0042','TA0043'); Techniques=@('T1595','T1592'); Desc='Missing risk assessment leaves unknown vulnerabilities and threat vectors unaddressed' } 'PS06' = @{ Tactics=@('TA0001','TA0043'); Techniques=@('T1566','T1598','T1204'); Desc='Without ongoing training, users remain the weakest link for phishing and social engineering' } } # ATT&CK Tactic metadata for heatmap display $script:MitreTactics = [ordered]@{ 'TA0043' = @{ Name='Reconnaissance'; Short='Recon'; Color='#94a3b8' } 'TA0042' = @{ Name='Resource Development'; Short='ResDev'; Color='#a1a1aa' } 'TA0001' = @{ Name='Initial Access'; Short='InitAccess'; Color='#ef4444' } 'TA0002' = @{ Name='Execution'; Short='Execution'; Color='#f97316' } 'TA0003' = @{ Name='Persistence'; Short='Persist'; Color='#eab308' } 'TA0004' = @{ Name='Privilege Escalation'; Short='PrivEsc'; Color='#84cc16' } 'TA0005' = @{ Name='Defense Evasion'; Short='DefEvade'; Color='#22c55e' } 'TA0006' = @{ Name='Credential Access'; Short='CredAccess'; Color='#14b8a6' } 'TA0007' = @{ Name='Discovery'; Short='Discovery'; Color='#06b6d4' } 'TA0008' = @{ Name='Lateral Movement'; Short='LatMove'; Color='#3b82f6' } 'TA0009' = @{ Name='Collection'; Short='Collection'; Color='#6366f1' } 'TA0010' = @{ Name='Exfiltration'; Short='Exfil'; Color='#8b5cf6' } 'TA0011' = @{ Name='Command & Control'; Short='C2'; Color='#a855f7' } 'TA0040' = @{ Name='Impact'; Short='Impact'; Color='#ec4899' } } # Calculate ATT&CK tactic coverage from current check statuses function Get-MitreCoverage { $tacticCoverage = @{} foreach ($ta in $script:MitreTactics.Keys) { $tacticCoverage[$ta] = @{ Covered=0; Failed=0; Total=0; Checks=@() } } foreach ($id in $script:MitreMap.Keys) { $m = $script:MitreMap[$id] $sv = if ($script:StatusCombos[$id] -and $script:StatusCombos[$id].SelectedItem) { $script:StatusCombos[$id].SelectedItem.ToString() } else { 'Not Assessed' } foreach ($ta in $m.Tactics) { if (-not $tacticCoverage.Contains($ta)) { continue } $tacticCoverage[$ta].Total++ $tacticCoverage[$ta].Checks += @{ ID=$id; Status=$sv } if ($sv -eq 'Pass') { $tacticCoverage[$ta].Covered++ } elseif ($sv -eq 'Fail') { $tacticCoverage[$ta].Failed++ } elseif ($sv -eq 'Partial') { $tacticCoverage[$ta].Covered += 0.5 } } } return $tacticCoverage } # Generate attack path narratives from failed checks function Get-AttackPaths { $paths = @() $failedIds = @() foreach ($id in $script:MitreMap.Keys) { $sv = if ($script:StatusCombos[$id] -and $script:StatusCombos[$id].SelectedItem) { $script:StatusCombos[$id].SelectedItem.ToString() } else { 'Not Assessed' } if ($sv -eq 'Fail') { $failedIds += $id } } # Chain 1: Phishing -> Credential Harvest -> Domain Compromise $chain1 = @() if ('IA03' -in $failedIds) { $chain1 += @{ID='IA03';Step='Phishing bypasses MFA-less email (T1566)'} } if ('CF03' -in $failedIds) { $chain1 += @{ID='CF03';Step='Untrained users click malicious links (T1204)'} } if ('EP07' -in $failedIds) { $chain1 += @{ID='EP07';Step='Malicious macro executes payload (T1059, T1204.002)'} } if ('EP01' -in $failedIds) { $chain1 += @{ID='EP01';Step='AV fails to detect/block payload (T1562.001)'} } if ('EP08' -in $failedIds) { $chain1 += @{ID='EP08';Step='Credentials dumped from LSASS (T1003.001)'} } if ('IA01' -in $failedIds) { $chain1 += @{ID='IA01';Step='Stolen DA creds grant domain admin (T1078.002)'} } if ('CF01' -in $failedIds) { $chain1 += @{ID='CF01';Step='ADCS/LDAP vulns enable persistence (T1098)'} } if ($chain1.Count -ge 3) { $paths += @{ Name='Phishing to Domain Compromise'; Severity='CRITICAL'; Steps=$chain1 } } # Chain 2: Lateral Movement -> Ransomware $chain2 = @() if ('NA01' -in $failedIds) { $chain2 += @{ID='NA01';Step='Flat network enables unrestricted movement (T1021)'} } if ('EP03' -in $failedIds) { $chain2 += @{ID='EP03';Step='SMB/NTLM relay enables credential theft (T1557.001)'} } if ('CF07' -in $failedIds) { $chain2 += @{ID='CF07';Step='Excessive local admin enables lateral spread (T1078.001)'} } if ('LM02' -in $failedIds) { $chain2 += @{ID='LM02';Step='No SIEM - lateral movement goes undetected (T1562.002)'} } if ('BR01' -in $failedIds -or 'BR02' -in $failedIds) { $chain2 += @{ID=$(if('BR01' -in $failedIds){'BR01'}else{'BR02'});Step='No backup recovery path - ransomware is catastrophic (T1486)'} } if ($chain2.Count -ge 3) { $paths += @{ Name='Lateral Movement to Ransomware'; Severity='CRITICAL'; Steps=$chain2 } } # Chain 3: External Exploitation -> Data Exfiltration $chain3 = @() if ('NP02' -in $failedIds) { $chain3 += @{ID='NP02';Step='Open ports expose vulnerable services (T1190)'} } if ('EP04' -in $failedIds) { $chain3 += @{ID='EP04';Step='Unpatched software exploited (T1203)'} } if ('NP03' -in $failedIds) { $chain3 += @{ID='NP03';Step='RDP exposed - brute force or BlueKeep (T1021.001)'} } if ('CF05' -in $failedIds) { $chain3 += @{ID='CF05';Step='Open shares expose sensitive data (T1039)'} } if ('EP06' -in $failedIds) { $chain3 += @{ID='EP06';Step='Firewall gaps allow data exfiltration (T1048)'} } if ('NA07' -in $failedIds) { $chain3 += @{ID='NA07';Step='No DNS filtering - C2 via DNS tunneling (T1071)'} } if ($chain3.Count -ge 3) { $paths += @{ Name='External Exploitation to Data Exfiltration'; Severity='HIGH'; Steps=$chain3 } } # Chain 4: Insider Threat / Credential Abuse $chain4 = @() if ('IA04' -in $failedIds) { $chain4 += @{ID='IA04';Step='Terminated employee accounts still active (T1078)'} } if ('IA07' -in $failedIds) { $chain4 += @{ID='IA07';Step='Shared accounts eliminate attribution (T1078.001)'} } if ('LM01' -in $failedIds) { $chain4 += @{ID='LM01';Step='Inadequate auditing hides insider activity (T1562.002)'} } if ('CF04' -in $failedIds) { $chain4 += @{ID='CF04';Step='Excessive permissions enable data theft (T1005)'} } if ($chain4.Count -ge 3) { $paths += @{ Name='Insider Threat / Credential Abuse'; Severity='HIGH'; Steps=$chain4 } } return $paths } # ── Phase 4B: Ransomware Preparedness Score ────────────────────────────────── # Evaluates 4 domains: Prevention, Protection, Detection, Recovery # Each domain has weighted checks; overall score 0-100 with letter grade function Get-RansomwareScore { $domains = [ordered]@{ Prevention = @{ Weight = 0.30 Checks = @( @{ID='EP01'; Factor='AV/EDR active with ASR rules'; Points=15} @{ID='EP07'; Factor='AppLocker/WDAC + Office macros restricted'; Points=15} @{ID='IA03'; Factor='MFA on all remote access'; Points=12} @{ID='IA05'; Factor='Strong password policy'; Points=8} @{ID='NP07'; Factor='IDS/IPS on perimeter'; Points=8} @{ID='CF03'; Factor='Security awareness training'; Points=8} @{ID='NA07'; Factor='DNS filtering active'; Points=6} @{ID='NP08'; Factor='TLS properly configured'; Points=5} @{ID='EP04'; Factor='Patching current'; Points=10} @{ID='NP03'; Factor='RDP/remote access secured'; Points=8} @{ID='CF02'; Factor='SMBv1 disabled'; Points=5} ) } Protection = @{ Weight = 0.25 Checks = @( @{ID='EP08'; Factor='Credential Guard / LSA Protection / WDigest disabled'; Points=18} @{ID='EP02'; Factor='BitLocker / disk encryption'; Points=12} @{ID='EP03'; Factor='SMB signing + NTLM hardened'; Points=12} @{ID='EP05'; Factor='Local admin controlled / no privesc paths'; Points=10} @{ID='EP06'; Factor='Host firewall properly configured'; Points=10} @{ID='IA01'; Factor='Privileged accounts minimized'; Points=12} @{ID='IA06'; Factor='PAM / LAPS deployed'; Points=10} @{ID='NA01'; Factor='Network segmentation'; Points=10} @{ID='CF01'; Factor='Service account hygiene / ADCS secure'; Points=6} ) } Detection = @{ Weight = 0.25 Checks = @( @{ID='LM02'; Factor='SIEM deployed with log aggregation'; Points=20} @{ID='LM03'; Factor='Audit policy + PowerShell logging comprehensive'; Points=18} @{ID='LM01'; Factor='Audit configuration baseline'; Points=12} @{ID='LM08'; Factor='Alerting and monitoring active'; Points=15} @{ID='LM07'; Factor='Log sizes adequate for retention'; Points=8} @{ID='LM06'; Factor='Regular log review process'; Points=10} @{ID='LM04'; Factor='Firewall/IDS logging'; Points=8} @{ID='CF08'; Factor='Vulnerability scanning active'; Points=9} ) } Recovery = @{ Weight = 0.20 Checks = @( @{ID='BR01'; Factor='Backup solution operational'; Points=20} @{ID='BR02'; Factor='Offsite / immutable / air-gapped copies'; Points=20} @{ID='BR04'; Factor='Backup restore tested'; Points=15} @{ID='BR03'; Factor='DR plan documented'; Points=12} @{ID='BR05'; Factor='RTO/RPO defined and achievable'; Points=10} @{ID='BR06'; Factor='Backup monitoring and alerting'; Points=10} @{ID='BR08'; Factor='Critical system backup coverage'; Points=8} @{ID='BR07'; Factor='Backup encryption'; Points=5} ) } } $domainScores = [ordered]@{} $overallWeighted = 0 foreach ($dName in $domains.Keys) { $d = $domains[$dName] $maxPoints = ($d.Checks | ForEach-Object { $_.Points } | Measure-Object -Sum).Sum $earnedPoints = 0 $details = @() foreach ($ck in $d.Checks) { $sv = if ($script:StatusCombos[$ck.ID] -and $script:StatusCombos[$ck.ID].SelectedItem) { $script:StatusCombos[$ck.ID].SelectedItem.ToString() } else { 'Not Assessed' } $earned = switch ($sv) { 'Pass' { $ck.Points } 'Partial' { [math]::Round($ck.Points * 0.5) } default { 0 } } $earnedPoints += $earned $details += @{ ID=$ck.ID; Factor=$ck.Factor; MaxPoints=$ck.Points; Earned=$earned; Status=$sv } } $pct = if ($maxPoints -gt 0) { [math]::Round($earnedPoints / $maxPoints * 100) } else { 0 } $domainScores[$dName] = @{ Score=$pct; Earned=$earnedPoints; Max=$maxPoints; Weight=$d.Weight; Details=$details } $overallWeighted += $pct * $d.Weight } $overall = [math]::Round($overallWeighted) $grade = switch($true) { ($overall -ge 90){'A'} ($overall -ge 80){'B'} ($overall -ge 70){'C'} ($overall -ge 60){'D'} default{'F'} } return @{ Overall=$overall; Grade=$grade; Domains=$domainScores } } # ── End Phase 4 Data Layer ─────────────────────────────────────────────────── # Scan state $script:ScanTarget = 'localhost' $script:ScanCredential = $null $script:ScanRunning = $false $script:ScanButtons = @{} # ID -> scan button element $script:ActiveFilter = 'All' # Async scan infrastructure $script:ScanQueue = [System.Collections.Queue]::Synchronized([System.Collections.Queue]::new()) $script:CurrentPS = $null # current [PowerShell] instance $script:CurrentAsyncResult = $null # IAsyncResult handle $script:CurrentScanId = $null # ID being scanned # Cache InitialSessionState (expensive to create, reusable) for per-check runspaces $script:ScanISS = $null try { $script:ScanISS = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() } catch { $script:ScanISS = $null } $script:ScanBatchTotal = 0 $script:ScanBatchDone = 0 $script:ScanBatchMode = $null # 'Batch','Single','Preflight' or $null $script:ScanBatchStopwatch = $null # [Stopwatch] for total batch time $script:ScanBatchTally = $null # @{Pass=0;Fail=0;Partial=0;Error=0} $script:CurrentScanStopwatch = $null # [Stopwatch] for per-check time $script:CurrentScanHeartbeat = 0 # last heartbeat second logged $script:ConsoleLineCount = 0 $script:CheckStates = @{} $script:StatusCombos = @{} $script:NotesBoxes = @{} $script:FindingsBoxes = @{} $script:EvidenceBoxes = @{} $script:RemAssignBoxes = @{} $script:RemDueBoxes = @{} $script:RemStatusCombos = @{} $script:CheckBoxes = @{} $script:HintBlocks = @{} $script:TotalItems = 0 $script:CategoryProgress = @{} $script:ThemedElements = [System.Collections.ArrayList]@() $script:AllCombos = [System.Collections.ArrayList]@() # Collapse/advance tracking $script:ItemCards = @{} # ID -> outer card Border $script:ItemPanels = @{} # ID -> inner StackPanel (for collapsing children) $script:TabItemIDs = @{} # tabIndex -> [ordered list of item IDs] $script:ItemTabIndex = @{} # ID -> tabIndex $script:TabScrollViews = @{} # tabIndex -> ScrollViewer $script:TabIndex = 0 # current tab build counter $script:HighlightedCard = $null $script:SuppressAdvance = $false # ── XAML ───────────────────────────────────────────────────────────────────── [xml]$xaml = @"