From b99f8512e4239707c462564288b2aa8c736421e8 Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Fri, 17 Apr 2026 13:02:06 -0700 Subject: [PATCH] sync: auto-sync from ACG-TECH03L at 2026-04-17 13:02:04 Author: Howard Enos Machine: ACG-TECH03L Timestamp: 2026-04-17 13:02:04 --- .claude/memory/MEMORY.md | 1 + .claude/memory/project_sync_script_bug.md | 23 + .../msp-audit-scripts/workstation_audit.ps1 | 1606 +++++++++++++++++ 3 files changed, 1630 insertions(+) create mode 100644 .claude/memory/project_sync_script_bug.md diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index a78bd05..ef8a8b3 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -25,6 +25,7 @@ - [ACG-5070 Workstation Setup](reference_workstation_setup.md) - Windows 11 Pro clean install 2026-03-30, replaced CachyOS. All tools installed. ## Project +- [Sync script bug — untracked files](project_sync_script_bug.md) — Flagged for Mike. `.claude/scripts/sync.sh` line 53 misses untracked-only changes; one-line fix included. - [MasterBooter Side Project](project_masterbooter.md) — Howard's Rust+Slint Windows deployment toolkit at C:\MasterBooter, separate from client work. Do not log to clients/. - [Audio Processor Architecture](project_audio_processor_architecture.md) - Segment-first pipeline: detect breaks before transcription for complete content capture - [Neptune Email Routing Issues](project_email_routing_neptune.md) - Multiple clients (devcon, Sorensen/rieussetcorp) have email not routing properly from Neptune diff --git a/.claude/memory/project_sync_script_bug.md b/.claude/memory/project_sync_script_bug.md new file mode 100644 index 0000000..7aa5200 --- /dev/null +++ b/.claude/memory/project_sync_script_bug.md @@ -0,0 +1,23 @@ +--- +name: Sync script bug — untracked files +description: Flagged for Mike — .claude/scripts/sync.sh misses untracked-only changes +type: project +--- + +`.claude/scripts/sync.sh` line 53 uses `git diff-index --quiet HEAD --` to detect local changes. This only flags **tracked** files with modifications. Brand-new untracked files (a new report, new session log, new memory) will NOT be detected on their own — they only get swept up when a tracked file is also dirty (because `git add -A` then runs). + +Symptom seen 2026-04-17 by Howard: added a single new report file, ran /sync, script said "No local changes to commit" and did nothing. Workaround was `git add ` first, then re-run. + +**Why:** `git diff-index` ignores untracked files by design. Needs `git status --porcelain` (any output = changes) or equivalent. + +**How to apply:** Mike — small one-line fix in `.claude/scripts/sync.sh`. Suggested replacement: + +```bash +# Before (line 53): +if ! git diff-index --quiet HEAD -- 2>/dev/null; then + +# After: +if [ -n "$(git status --porcelain)" ]; then +``` + +Also applies to the Sync Summary's `git diff --stat $LOCAL_BEFORE..HEAD` — may need review to make sure the summary range still makes sense after the detection fix. diff --git a/projects/msp-tools/msp-audit-scripts/workstation_audit.ps1 b/projects/msp-tools/msp-audit-scripts/workstation_audit.ps1 index 93ca216..b19c75d 100644 --- a/projects/msp-tools/msp-audit-scripts/workstation_audit.ps1 +++ b/projects/msp-tools/msp-audit-scripts/workstation_audit.ps1 @@ -1136,6 +1136,1612 @@ if ($audit.SecuritySummary.Count -eq 0) { Write-Host " $($audit.SecuritySummary.Count) findings detected - review above" -ForegroundColor Yellow } +# ===================================================== +# 34. BOOT & RECOVERY +# ===================================================== +Write-Host "" +Write-Host "=== 34. BOOT & RECOVERY ===" -ForegroundColor Cyan +try { + $boot = [ordered]@{} + + # Boot mode (UEFI vs Legacy) + if ($env:firmware_type) { + $boot.BootMode = $env:firmware_type + } else { + try { + $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop + $boot.BootMode = if ($cs.BootupState) { $cs.BootupState } else { "Unknown" } + } catch { $boot.BootMode = "Unknown" } + } + + # Secure Boot (UEFI-only; throws on Legacy BIOS) + if (Get-Command Confirm-SecureBootUEFI -ErrorAction SilentlyContinue) { + try { $boot.SecureBootEnabled = [bool](Confirm-SecureBootUEFI -ErrorAction Stop) } + catch { $boot.SecureBootEnabled = $false; $boot.SecureBootNote = "Not available (Legacy BIOS or unsupported)" } + } else { + $boot.SecureBootEnabled = $null + } + + # TPM + if (Get-Command Get-Tpm -ErrorAction SilentlyContinue) { + try { + $tpm = Get-Tpm -ErrorAction Stop + $boot.TPM = [ordered]@{ + Present = [bool]$tpm.TpmPresent + Ready = [bool]$tpm.TpmReady + Enabled = [bool]$tpm.TpmEnabled + Activated = [bool]$tpm.TpmActivated + Owned = [bool]$tpm.TpmOwned + ManufacturerVersion = "$($tpm.ManufacturerVersion)" + ManagedAuthLevel = "$($tpm.ManagedAuthLevel)" + } + } catch { $boot.TPM = [ordered]@{ Error = $_.Exception.Message } } + } else { + try { + $tpmWmi = Get-CimInstance -Namespace "root\cimv2\security\microsofttpm" -ClassName Win32_Tpm -ErrorAction Stop + if ($tpmWmi) { + $boot.TPM = [ordered]@{ + Present = $true + Enabled = [bool]$tpmWmi.IsEnabled_InitialValue + Activated = [bool]$tpmWmi.IsActivated_InitialValue + Owned = [bool]$tpmWmi.IsOwned_InitialValue + SpecVersion = "$($tpmWmi.SpecVersion)" + ManufacturerVersion = "$($tpmWmi.ManufacturerVersion)" + } + } else { $boot.TPM = [ordered]@{ Present = $false } } + } catch { $boot.TPM = [ordered]@{ Present = $false; Note = "TPM WMI namespace not available" } } + } + + # WinRE status + try { + $reagent = (& reagentc /info 2>&1) -join "`n" + $statusMatch = [regex]::Match($reagent, 'Windows RE status:\s+(\w+)') + $locMatch = [regex]::Match($reagent, 'Windows RE location:\s+(.+)') + $bcdMatch = [regex]::Match($reagent, 'Boot Configuration Data \(BCD\) identifier:\s+(.+)') + $boot.WinRE = [ordered]@{ + Status = if ($statusMatch.Success) { $statusMatch.Groups[1].Value.Trim() } else { "Unknown" } + Location = if ($locMatch.Success) { $locMatch.Groups[1].Value.Trim() } else { "" } + BCDIdentifier = if ($bcdMatch.Success) { $bcdMatch.Groups[1].Value.Trim() } else { "" } + } + } catch { $boot.WinRE = [ordered]@{ Error = $_.Exception.Message } } + + # ESP partition health + try { + $espGuid = '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' + $espParts = @(Get-Partition -ErrorAction SilentlyContinue | Where-Object { $_.GptType -eq $espGuid }) + $boot.ESP = @($espParts | ForEach-Object { + $part = $_ + $vol = $null + try { $vol = $part | Get-Volume -ErrorAction Stop } catch {} + [ordered]@{ + DiskNumber = $part.DiskNumber + PartitionNumber = $part.PartitionNumber + SizeMB = [math]::Round($part.Size / 1MB, 0) + FreeMB = if ($vol -and $vol.SizeRemaining) { [math]::Round($vol.SizeRemaining / 1MB, 0) } else { $null } + FileSystem = if ($vol) { $vol.FileSystem } else { $null } + } + }) + } catch { $boot.ESP = @(); $boot.ESPError = $_.Exception.Message } + + # BCD snapshot (parsed) + try { + $bcdLines = & bcdedit /enum '{bootmgr}' 2>&1 + $bcdHash = [ordered]@{} + foreach ($line in $bcdLines) { + if ($line -match '^([a-zA-Z0-9_]+)\s{2,}(.+)$') { + $k = $matches[1].Trim() + $v = $matches[2].Trim() + if (-not $bcdHash.Contains($k)) { $bcdHash[$k] = $v } + } + } + $boot.BCDBootmgr = $bcdHash + } catch { $boot.BCDBootmgr = [ordered]@{ Error = $_.Exception.Message } } + + # Recent boot/crash events (last 30 days) + try { + $boot30dAgo = (Get-Date).AddDays(-30) + $bootEvents = @(Get-WinEvent -FilterHashtable @{LogName='System'; Id=41,6008,1074; StartTime=$boot30dAgo} -ErrorAction SilentlyContinue) + $boot.RecentBootEvents = [ordered]@{ + UnexpectedShutdownsKernel41 = @($bootEvents | Where-Object Id -eq 41).Count + PreviousShutdownUnexpected6008 = @($bootEvents | Where-Object Id -eq 6008).Count + PlannedShutdowns1074 = @($bootEvents | Where-Object Id -eq 1074).Count + Last5 = @($bootEvents | Sort-Object TimeCreated -Descending | Select-Object -First 5 | ForEach-Object { + [ordered]@{ + Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") + Id = $_.Id + Message = (($_.Message -split "`r?`n")[0]).Trim() + } + }) + } + } catch { $boot.RecentBootEvents = [ordered]@{ Error = $_.Exception.Message } } + + $audit.BootRecovery = $boot + + Write-Host " Boot mode: $($boot.BootMode), Secure Boot: $($boot.SecureBootEnabled)" + Write-Host " TPM present: $($boot.TPM.Present), ready: $($boot.TPM.Ready)" + Write-Host " WinRE: $($boot.WinRE.Status)" + Write-Host " ESP partitions: $($boot.ESP.Count)" + Write-Host " Unexpected shutdowns (30d): $($boot.RecentBootEvents.UnexpectedShutdownsKernel41)" + + if ($boot.SecureBootEnabled -eq $false) { $audit.SecuritySummary += "Secure Boot disabled" } + if ($boot.TPM.Present -eq $false) { $audit.SecuritySummary += "TPM not present" } + if ($boot.WinRE.Status -eq "Disabled") { $audit.SecuritySummary += "WinRE disabled (recovery limited)" } + if ($boot.RecentBootEvents.UnexpectedShutdownsKernel41 -gt 3) { + $audit.SecuritySummary += "$($boot.RecentBootEvents.UnexpectedShutdownsKernel41) unexpected kernel-power shutdowns in last 30d" + } +} catch { + Write-Host " [ERROR] Boot/Recovery section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "BootRecovery"; Error = $_.Exception.Message } +} + +# ===================================================== +# 35. DEFENDER DEEPER +# ===================================================== +Write-Host "" +Write-Host "=== 35. DEFENDER DEEPER ===" -ForegroundColor Cyan +try { + $def = [ordered]@{} + + if (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue) { + try { + $mp = Get-MpComputerStatus -ErrorAction Stop + $def.Status = [ordered]@{ + AMEngineVersion = "$($mp.AMEngineVersion)" + AMServiceVersion = "$($mp.AMServiceVersion)" + AMProductVersion = "$($mp.AMProductVersion)" + NISEngineVersion = "$($mp.NISEngineVersion)" + AntispywareSignatureLastUpdated = if ($mp.AntispywareSignatureLastUpdated) { $mp.AntispywareSignatureLastUpdated.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + AntivirusSignatureLastUpdated = if ($mp.AntivirusSignatureLastUpdated) { $mp.AntivirusSignatureLastUpdated.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + AntivirusSignatureAgeDays = $mp.AntivirusSignatureAge + NISSignatureAgeDays = $mp.NISSignatureAge + FullScanAgeDays = $mp.FullScanAge + QuickScanAgeDays = $mp.QuickScanAge + FullScanEndTime = if ($mp.FullScanEndTime) { $mp.FullScanEndTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + QuickScanEndTime = if ($mp.QuickScanEndTime) { $mp.QuickScanEndTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + IsTamperProtected = if ($mp.PSObject.Properties.Match('IsTamperProtected').Count) { [bool]$mp.IsTamperProtected } else { $null } + BehaviorMonitorEnabled = [bool]$mp.BehaviorMonitorEnabled + IoavProtectionEnabled = [bool]$mp.IoavProtectionEnabled + OnAccessProtectionEnabled = [bool]$mp.OnAccessProtectionEnabled + AntivirusEnabled = [bool]$mp.AntivirusEnabled + RealTimeProtectionEnabled = [bool]$mp.RealTimeProtectionEnabled + } + } catch { $def.Status = [ordered]@{ Error = $_.Exception.Message } } + } + + # Tamper Protection (registry fallback) + try { + $tpReg = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows Defender\Features" -Name TamperProtection -ErrorAction SilentlyContinue + if ($tpReg) { $def.TamperProtectionRegValue = $tpReg.TamperProtection } + } catch {} + + if (Get-Command Get-MpPreference -ErrorAction SilentlyContinue) { + try { + $pref = Get-MpPreference -ErrorAction Stop + + # Cloud + sample + $def.CloudProtection = [ordered]@{ + MAPSReporting = "$($pref.MAPSReporting)" # 0=Disabled, 1=Basic, 2=Advanced + SubmitSamplesConsent = "$($pref.SubmitSamplesConsent)" # 0=AlwaysPrompt, 1=AutoSafe, 2=Never, 3=AutoAll + CloudBlockLevel = "$($pref.CloudBlockLevel)" + CloudExtendedTimeout = $pref.CloudExtendedTimeout + } + + # Controlled Folder Access + $def.ControlledFolderAccess = [ordered]@{ + EnableControlledFolderAccess = "$($pref.EnableControlledFolderAccess)" # 0=Disabled, 1=Enabled, 2=AuditMode + ProtectedFolders = @($pref.ControlledFolderAccessProtectedFolders) + AllowedApplications = @($pref.ControlledFolderAccessAllowedApplications) + } + + # ASR rules + $asrNames = @{ + "56a863a9-875e-4185-98a7-b882c64b5ce5" = "Block abuse of exploited vulnerable signed drivers" + "7674ba52-37eb-4a4f-a9a1-f0f9a1619a2c" = "Block Adobe Reader from creating child processes" + "d4f940ab-401b-4efc-aadc-ad5f3c50688a" = "Block all Office applications from creating child processes" + "9e6c4e1f-7d60-472f-ba1a-a39ef669e4b2" = "Block credential stealing from LSASS" + "be9ba2d9-53ea-4cdc-84e5-9b1eeee46550" = "Block executable content from email/webmail" + "01443614-cd74-433a-b99e-2ecdc07bfc25" = "Block executable files unless meeting prevalence/age criteria" + "5beb7efe-fd9a-4556-801d-275e5ffc04cc" = "Block execution of potentially obfuscated scripts" + "d3e037e1-3eb8-44c8-a917-57927947596d" = "Block JavaScript/VBScript launching downloaded executable" + "3b576869-a4ec-4529-8536-b80a7769e899" = "Block Office applications from creating executable content" + "75668c1f-73b5-4cf0-bb93-3ecf5cb7cc84" = "Block Office applications from injecting code into other processes" + "26190899-1602-49e8-8b27-eb1d0a1ce869" = "Block Office communication app from creating child processes" + "e6db77e5-3df2-4cf1-b95a-636979351e5b" = "Block persistence through WMI event subscription" + "d1e49aac-8f56-4280-b9ba-993a6d77406c" = "Block process creations from PSExec/WMI commands" + "b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4" = "Block untrusted/unsigned processes that run from USB" + "92e97fa1-2edf-4476-bdd6-9dd0b4dddc7b" = "Block Win32 API calls from Office macros" + "c1db55ab-c21a-4637-bb3f-a12568109d35" = "Use advanced ransomware protection" + "a8f5898e-1dc8-49a9-9878-85004b8a61e6" = "Block Webshell creation for servers" + "33ddedf1-c6e0-47cb-833e-de6133960387" = "Block rebooting machine in Safe Mode" + "c0033c00-d16d-4114-a5a0-dc9b3a7d2ceb" = "Block use of copied/impersonated system tools" + } + $asrActionNames = @{ 0 = "Disabled"; 1 = "Block"; 2 = "Audit"; 6 = "Warn" } + + $rules = @() + $ids = @($pref.AttackSurfaceReductionRules_Ids) + $acts = @($pref.AttackSurfaceReductionRules_Actions) + for ($i = 0; $i -lt $ids.Count; $i++) { + $id = "$($ids[$i])" + $act = if ($i -lt $acts.Count) { [int]$acts[$i] } else { 0 } + $rules += [ordered]@{ + Id = $id + Name = if ($asrNames.ContainsKey($id)) { $asrNames[$id] } else { "(unknown)" } + Action = if ($asrActionNames.ContainsKey($act)) { $asrActionNames[$act] } else { "$act" } + ActionCode = $act + } + } + $def.ASRRules = $rules + + # Exclusions (high-value security signal) + $def.Exclusions = [ordered]@{ + Paths = @($pref.ExclusionPath) + Extensions = @($pref.ExclusionExtension) + Processes = @($pref.ExclusionProcess) + IpAddresses = @($pref.ExclusionIpAddress) + } + } catch { $def.PreferenceError = $_.Exception.Message } + } + + # Threat detection history + if (Get-Command Get-MpThreatDetection -ErrorAction SilentlyContinue) { + try { + $threats = Get-MpThreatDetection -ErrorAction SilentlyContinue + $def.ThreatHistory = @($threats | Sort-Object InitialDetectionTime -Descending | Select-Object -First 25 | ForEach-Object { + [ordered]@{ + ThreatID = "$($_.ThreatID)" + ProcessName = "$($_.ProcessName)" + Resources = @($_.Resources) + InitialDetectionTime = if ($_.InitialDetectionTime) { $_.InitialDetectionTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + LastThreatStatusChangeTime = if ($_.LastThreatStatusChangeTime) { $_.LastThreatStatusChangeTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + DetectionSourceTypeID = "$($_.DetectionSourceTypeID)" + AMProductVersion = "$($_.AMProductVersion)" + } + }) + $def.ThreatHistoryCount = @($threats).Count + } catch { $def.ThreatHistoryError = $_.Exception.Message } + } + + $audit.DefenderDeeper = $def + + Write-Host " Sig age (AV): $($def.Status.AntivirusSignatureAgeDays)d, Quick scan age: $($def.Status.QuickScanAgeDays)d, Full scan age: $($def.Status.FullScanAgeDays)d" + Write-Host " Tamper protected: $($def.Status.IsTamperProtected) (reg val: $($def.TamperProtectionRegValue))" + Write-Host " ASR rules configured: $(@($def.ASRRules).Count) (Block: $(@($def.ASRRules | Where-Object ActionCode -eq 1).Count), Audit: $(@($def.ASRRules | Where-Object ActionCode -eq 2).Count), Disabled: $(@($def.ASRRules | Where-Object ActionCode -eq 0).Count))" + Write-Host " Exclusions: paths=$(@($def.Exclusions.Paths).Count) exts=$(@($def.Exclusions.Extensions).Count) procs=$(@($def.Exclusions.Processes).Count)" + Write-Host " Threat history entries: $($def.ThreatHistoryCount)" + + # Findings + if ($def.Status.AntivirusSignatureAgeDays -ne $null -and $def.Status.AntivirusSignatureAgeDays -gt 7) { + $audit.SecuritySummary += "Defender signatures stale ($($def.Status.AntivirusSignatureAgeDays)d old)" + } + if ($def.Status.IsTamperProtected -eq $false) { + $audit.SecuritySummary += "Defender Tamper Protection disabled" + } + if ($def.Status.QuickScanAgeDays -ne $null -and $def.Status.QuickScanAgeDays -gt 14) { + $audit.SecuritySummary += "Defender quick scan stale ($($def.Status.QuickScanAgeDays)d)" + } + if ($def.ControlledFolderAccess.EnableControlledFolderAccess -eq "0") { + $audit.SecuritySummary += "Controlled Folder Access (anti-ransomware) disabled" + } + $blockedAsrCount = @($def.ASRRules | Where-Object ActionCode -eq 1).Count + if ($blockedAsrCount -lt 5) { + $audit.SecuritySummary += "Only $blockedAsrCount ASR rules in Block mode (recommend 10+)" + } + # Suspicious exclusions + $suspiciousExcl = @($def.Exclusions.Paths | Where-Object { $_ -match '(?i)\\(temp|appdata|programdata|users\\public)\\' }) + if ($suspiciousExcl.Count -gt 0) { + $audit.SecuritySummary += "Defender path exclusions in user-writable dirs: $(($suspiciousExcl -join '; '))" + } +} catch { + Write-Host " [ERROR] Defender Deeper section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "DefenderDeeper"; Error = $_.Exception.Message } +} + +# ===================================================== +# 36. EDR / SECURITY TOOL OVERLAY +# ===================================================== +Write-Host "" +Write-Host "=== 36. EDR / SECURITY TOOL OVERLAY ===" -ForegroundColor Cyan +try { + $edrCatalog = @( + @{ Name = "Bitdefender"; Services = @("VSSERV","EPSecurityService","EPProtectedService","EPIntegrationService","EPRedline"); RegPath = "HKLM:\SOFTWARE\Bitdefender" } + @{ Name = "SentinelOne"; Services = @("SentinelAgent","LogProcessorService","SentinelHelperService"); RegPath = "HKLM:\SOFTWARE\Sentinel Labs" } + @{ Name = "CrowdStrike Falcon";Services = @("CSFalconService"); RegPath = "HKLM:\SOFTWARE\CrowdStrike" } + @{ Name = "Webroot"; Services = @("WRSVC","WRCoreService","WRSkyClient"); RegPath = "HKLM:\SOFTWARE\WRData" } + @{ Name = "Carbon Black"; Services = @("CbDefense","carbonblack","CbProtection","CbComms"); RegPath = "HKLM:\SOFTWARE\CarbonBlack" } + @{ Name = "Sophos"; Services = @("Sophos Endpoint Defense Service","Sophos Health Service","Sophos MCS Agent","Sophos AutoUpdate Service"); RegPath = "HKLM:\SOFTWARE\Sophos" } + @{ Name = "ESET"; Services = @("ekrn","EraAgentSvc"); RegPath = "HKLM:\SOFTWARE\ESET" } + @{ Name = "Malwarebytes"; Services = @("MBAMService","MBAMSvc"); RegPath = "HKLM:\SOFTWARE\Malwarebytes" } + @{ Name = "Trend Micro"; Services = @("TmCCSF","TmListen","ntrtscan","tmpfw"); RegPath = "HKLM:\SOFTWARE\TrendMicro" } + @{ Name = "McAfee"; Services = @("masvc","macmnsvc","McAfeeFramework","mfevtps","mfemms"); RegPath = "HKLM:\SOFTWARE\McAfee" } + @{ Name = "Symantec"; Services = @("ccSvcHst","SmcService","SepMasterService"); RegPath = "HKLM:\SOFTWARE\Symantec" } + @{ Name = "Huntress"; Services = @("HuntressAgent","HuntressUpdater","HuntressRio"); RegPath = "HKLM:\SOFTWARE\Huntress Labs" } + @{ Name = "Cylance"; Services = @("CylanceSvc","CylanceUI"); RegPath = "HKLM:\SOFTWARE\Cylance" } + @{ Name = "ThreatLocker"; Services = @("ThreatLockerService"); RegPath = "HKLM:\SOFTWARE\ThreatLocker" } + @{ Name = "Defender for Endpoint Sense"; Services = @("Sense"); RegPath = "HKLM:\SOFTWARE\Microsoft\Windows Advanced Threat Protection" } + ) + $edrFound = @() + $allServices = @{} + Get-Service -ErrorAction SilentlyContinue | ForEach-Object { $allServices[$_.Name] = $_ } + foreach ($prod in $edrCatalog) { + $matched = @() + foreach ($svcName in $prod.Services) { + if ($allServices.ContainsKey($svcName)) { + $svc = $allServices[$svcName] + $matched += [ordered]@{ Service = $svcName; Status = "$($svc.Status)"; StartType = "$($svc.StartType)" } + } + } + $regPresent = $false + try { if (Test-Path $prod.RegPath) { $regPresent = $true } } catch {} + if ($matched.Count -gt 0 -or $regPresent) { + $edrFound += [ordered]@{ + Product = $prod.Name + ServicesPresent = $matched + RegistryPresent = $regPresent + } + } + } + $audit.EDRTools = $edrFound + Write-Host " EDR/AV products detected: $($edrFound.Count)" + foreach ($p in $edrFound) { + $running = @($p.ServicesPresent | Where-Object Status -eq "Running").Count + Write-Host " $($p.Product) -- services: $($p.ServicesPresent.Count) ($running running)" + } + if ($edrFound.Count -eq 0) { + Write-Host " [INFO] No third-party EDR/AV detected (Defender only)" -ForegroundColor Yellow + } +} catch { + Write-Host " [ERROR] EDR overlay section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "EDRTools"; Error = $_.Exception.Message } +} + +# ===================================================== +# 37. SCHEDULED TASKS (with suspicious flags) +# ===================================================== +Write-Host "" +Write-Host "=== 37. SCHEDULED TASKS ===" -ForegroundColor Cyan +try { + $tasks = @() + $suspiciousTasks = @() + $created30dAgo = (Get-Date).AddDays(-30) + $allTasks = @(Get-ScheduledTask -ErrorAction SilentlyContinue) + foreach ($t in $allTasks) { + $info = $null + try { $info = $t | Get-ScheduledTaskInfo -ErrorAction Stop } catch {} + $actions = @($t.Actions | ForEach-Object { + [ordered]@{ + Type = $_.GetType().Name + Execute = "$($_.Execute)" + Arguments = "$($_.Arguments)" + WorkingDirectory = "$($_.WorkingDirectory)" + } + }) + # Suspicious patterns + $isSuspicious = $false + $suspReasons = @() + foreach ($a in $actions) { + $cmd = "$($a.Execute) $($a.Arguments)" + if ($cmd -match '(?i)\\(temp|appdata\\local\\temp|programdata\\temp|users\\public)\\') { $isSuspicious=$true; $suspReasons += "Runs from user-writable temp" } + if ($cmd -match '(?i)powershell.*\s+-(e|en|enc|encod|encode|encodedcommand)\b') { $isSuspicious=$true; $suspReasons += "PowerShell -EncodedCommand" } + if ($cmd -match '(?i)\bmshta\.exe') { $isSuspicious=$true; $suspReasons += "mshta.exe (HTA execution)" } + if ($cmd -match '(?i)\brundll32\.exe.*,([A-Z][a-z]+)') { $suspReasons += "rundll32 with custom ordinal" } + if ($cmd -match '(?i)\bcertutil\.exe.*-(decode|urlcache|f)\b') { $isSuspicious=$true; $suspReasons += "certutil decode/download" } + if ($cmd -match '(?i)\bbitsadmin\.exe.*\/transfer') { $isSuspicious=$true; $suspReasons += "bitsadmin transfer" } + if ($cmd -match '(?i)\b(curl|wget|iwr|invoke-webrequest|invoke-restmethod)\b.*\bhttps?://') { $isSuspicious=$true; $suspReasons += "Inline HTTP download" } + } + $authorOk = $true + try { + if ($t.Author -and $t.Author -notmatch '^(Microsoft|Windows|\\?\\?\\?|NT AUTHORITY|SYSTEM|S-1-5-)') { + $authorOk = $true # custom author -- may be normal + } + } catch {} + $createdRecent = $false + if ($info -and $info.LastRunTime -and ($info.LastRunTime -gt $created30dAgo)) { $createdRecent = $true } + + $task = [ordered]@{ + TaskName = $t.TaskName + TaskPath = $t.TaskPath + State = "$($t.State)" + Author = $t.Author + Description = $t.Description + Actions = $actions + LastRunTime = if ($info -and $info.LastRunTime) { $info.LastRunTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + NextRunTime = if ($info -and $info.NextRunTime) { $info.NextRunTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + LastTaskResult = if ($info) { "$($info.LastTaskResult)" } else { $null } + } + if ($isSuspicious) { + $task.SuspiciousReasons = $suspReasons + $suspiciousTasks += $task + } + # Keep all non-Microsoft tasks; Microsoft tasks only if suspicious + if ($t.TaskPath -notmatch '^\\Microsoft\\' -or $isSuspicious) { + $tasks += $task + } + } + $audit.ScheduledTasks = [ordered]@{ + TotalCount = $allTasks.Count + ReturnedCount = $tasks.Count + SuspiciousCount = $suspiciousTasks.Count + SuspiciousTasks = $suspiciousTasks + AllNonMicrosoft = $tasks + } + Write-Host " Total tasks: $($allTasks.Count), Non-Microsoft: $($tasks.Count), Suspicious: $($suspiciousTasks.Count)" + if ($suspiciousTasks.Count -gt 0) { + $audit.SecuritySummary += "$($suspiciousTasks.Count) suspicious scheduled task(s) flagged" + foreach ($st in $suspiciousTasks) { + Write-Host " [SUSP] $($st.TaskPath)$($st.TaskName) -- $($st.SuspiciousReasons -join '; ')" -ForegroundColor Red + } + } +} catch { + Write-Host " [ERROR] Scheduled tasks section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "ScheduledTasks"; Error = $_.Exception.Message } +} + +# ===================================================== +# 38. SERVICES FULL INVENTORY (with suspicious flags) +# ===================================================== +Write-Host "" +Write-Host "=== 38. SERVICES INVENTORY ===" -ForegroundColor Cyan +try { + $allSvc = @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue) + $suspSvc = @() + foreach ($s in $allSvc) { + $reasons = @() + $path = "$($s.PathName)" + # Extract just the executable from the PathName + $exe = "" + if ($path -match '^"([^"]+)"') { $exe = $matches[1] } + elseif ($path -match '^(\S+\.exe)') { $exe = $matches[1] } + else { $exe = ($path -split '\s')[0] } + + # Path with spaces but not quoted (privilege-escalation classic) + if ($path -and $path -notmatch '^"' -and $path -match '\s' -and $path -match '^([A-Z]:\\[^"]+\s+[^"]+\.exe)') { + $reasons += "Unquoted path with spaces" + } + # Binary in user-writable dir + if ($exe -match '(?i)\\(users|programdata|temp|public|appdata)\\') { + $reasons += "Binary in user-writable directory: $exe" + } + # Not in standard system paths + if ($exe -and $exe -notmatch '(?i)^[A-Z]:\\(Windows|Program Files|Program Files \(x86\))') { + if ($exe -notmatch '(?i)^[A-Z]:\\Users') { + $reasons += "Binary outside standard system paths: $exe" + } + } + # StartName not standard + $startName = "$($s.StartName)" + if ($startName -and $startName -notmatch '^(LocalSystem|NT AUTHORITY|NT Service|.*\\LocalService|.*\\NetworkService)$' -and $startName -ne '') { + # Custom user account -- worth noting + } + + if ($reasons.Count -gt 0) { + $suspSvc += [ordered]@{ + Name = $s.Name + DisplayName = $s.DisplayName + State = "$($s.State)" + StartMode = "$($s.StartMode)" + StartName = $startName + PathName = $path + ProcessId = $s.ProcessId + SuspiciousReasons = $reasons + } + } + } + $audit.ServicesInventory = [ordered]@{ + TotalServices = $allSvc.Count + SuspiciousCount = $suspSvc.Count + SuspiciousServices = $suspSvc + } + Write-Host " Total services: $($allSvc.Count), Suspicious: $($suspSvc.Count)" + if ($suspSvc.Count -gt 0) { + $audit.SecuritySummary += "$($suspSvc.Count) suspicious service(s) flagged (path/binary anomalies)" + foreach ($s in $suspSvc) { + Write-Host " [SUSP] $($s.Name) ($($s.DisplayName)) -- $($s.SuspiciousReasons -join '; ')" -ForegroundColor Red + } + } +} catch { + Write-Host " [ERROR] Services inventory section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "ServicesInventory"; Error = $_.Exception.Message } +} + +# ===================================================== +# 39. PERSISTENCE - REGISTRY RUN/IFEO/WINLOGON + WMI SUBSCRIPTIONS +# ===================================================== +Write-Host "" +Write-Host "=== 39. REGISTRY PERSISTENCE + WMI SUBSCRIPTIONS ===" -ForegroundColor Cyan +try { + $persistence = [ordered]@{} + + $runKeyPaths = @( + "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run", + "HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce", + "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run", + "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce", + "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run", + "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" + ) + $runEntries = @() + foreach ($p in $runKeyPaths) { + try { + if (Test-Path $p) { + $vals = Get-ItemProperty -Path $p -ErrorAction SilentlyContinue + if ($vals) { + $vals.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object { + $runEntries += [ordered]@{ + Hive = $p + ValueName = $_.Name + Command = "$($_.Value)" + } + } + } + } + } catch {} + } + $persistence.RunKeys = $runEntries + + # Winlogon Userinit + Shell + try { + $wl = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" -ErrorAction SilentlyContinue + $persistence.Winlogon = [ordered]@{ + Userinit = "$($wl.Userinit)" + Shell = "$($wl.Shell)" + } + if ($wl.Userinit -and $wl.Userinit -notmatch '(?i)^[A-Z]:\\Windows\\system32\\userinit\.exe,?\s*$') { + $audit.SecuritySummary += "Winlogon Userinit modified: $($wl.Userinit)" + } + if ($wl.Shell -and $wl.Shell -notmatch '(?i)^explorer\.exe$') { + $audit.SecuritySummary += "Winlogon Shell modified: $($wl.Shell)" + } + } catch { $persistence.Winlogon = [ordered]@{ Error = $_.Exception.Message } } + + # Image File Execution Options - look for Debugger value (debugger hijack) + try { + $ifeoBase = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options" + $ifeoDebuggers = @() + if (Test-Path $ifeoBase) { + Get-ChildItem -Path $ifeoBase -ErrorAction SilentlyContinue | ForEach-Object { + $sub = Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue + if ($sub.Debugger) { + $ifeoDebuggers += [ordered]@{ + TargetExecutable = $_.PSChildName + Debugger = "$($sub.Debugger)" + } + } + } + } + $persistence.IFEODebuggers = $ifeoDebuggers + if ($ifeoDebuggers.Count -gt 0) { + $audit.SecuritySummary += "IFEO Debugger hijack(s) found: $($ifeoDebuggers.Count)" + } + } catch { $persistence.IFEODebuggers = @() } + + # WMI subscriptions (classic APT persistence) + $wmiSubs = [ordered]@{} + try { + $filters = @(Get-CimInstance -Namespace root\subscription -ClassName __EventFilter -ErrorAction SilentlyContinue) + $consumers = @(Get-CimInstance -Namespace root\subscription -ClassName __EventConsumer -ErrorAction SilentlyContinue) + $bindings = @(Get-CimInstance -Namespace root\subscription -ClassName __FilterToConsumerBinding -ErrorAction SilentlyContinue) + $wmiSubs.EventFilters = @($filters | ForEach-Object { [ordered]@{ Name = $_.Name; Query = "$($_.Query)"; QueryLanguage = "$($_.QueryLanguage)"; EventNamespace = "$($_.EventNamespace)" } }) + $wmiSubs.EventConsumers = @($consumers | ForEach-Object { + [ordered]@{ + Name = $_.Name + Class = $_.CimClass.CimClassName + CommandLineTemplate = "$($_.CommandLineTemplate)" + ScriptText = "$($_.ScriptText)" + ExecutablePath = "$($_.ExecutablePath)" + } + }) + $wmiSubs.Bindings = @($bindings | ForEach-Object { [ordered]@{ Filter = "$($_.Filter)"; Consumer = "$($_.Consumer)" } }) + # Anything user-defined here is suspicious + $userFilters = @($filters | Where-Object { $_.Name -notmatch '^(BVTFilter|SCM Event Log Filter|NTEventLogProvider)$' }) + if ($userFilters.Count -gt 0) { + $audit.SecuritySummary += "$($userFilters.Count) user-defined WMI event filter(s) -- review for persistence" + } + } catch { $wmiSubs.Error = $_.Exception.Message } + $persistence.WMISubscriptions = $wmiSubs + + $audit.RegistryPersistence = $persistence + Write-Host " Run-key entries: $($runEntries.Count)" + Write-Host " IFEO debugger hijacks: $($persistence.IFEODebuggers.Count)" + Write-Host " WMI EventFilters: $(@($wmiSubs.EventFilters).Count), Consumers: $(@($wmiSubs.EventConsumers).Count), Bindings: $(@($wmiSubs.Bindings).Count)" +} catch { + Write-Host " [ERROR] Registry/WMI persistence section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "RegistryPersistence"; Error = $_.Exception.Message } +} + +# ===================================================== +# 40. RECENTLY MODIFIED FILES (suspicious paths, last 7d) +# ===================================================== +Write-Host "" +Write-Host "=== 40. RECENTLY MODIFIED FILES ===" -ForegroundColor Cyan +try { + $scanDirs = @() + foreach ($d in @($env:TEMP, "$env:LOCALAPPDATA\Temp", $env:APPDATA, $env:LOCALAPPDATA, $env:PROGRAMDATA, $env:PUBLIC, "$env:SystemRoot\Temp")) { + if ($d -and (Test-Path $d)) { $scanDirs += $d } + } + $scanDirs = $scanDirs | Select-Object -Unique + $sevenDaysAgo = (Get-Date).AddDays(-7) + $execExtensions = '\.(exe|dll|ps1|vbs|vbe|js|jse|hta|jar|bat|cmd|scr|msi|cpl|wsh|wsf|lnk|pif)$' + $excludePathPatterns = @( + '(?i)\\(BraveSoftware|Google\\Chrome|Microsoft\\Edge|Mozilla\\Firefox|Microsoft\\Teams|GitHub Desktop|Slack|Discord|Spotify|Zoom)\\', + '(?i)\\(packages|cache|crashpad|GPUCache|ShaderCache|Code Cache|Cache_Data|webrtc|service_worker|IndexedDB|Local Storage)\\', + '(?i)\\(Microsoft\\Windows\\(WebCache|INetCache|Network|Notifications|Cookies|Explorer\\thumbcache))\\', + '(?i)\\(NuGet|pip|npm-cache|yarn-cache|.gradle|.m2|.cargo|.rustup)\\' + ) + $allFiles = @() + foreach ($d in $scanDirs) { + try { + $found = @(Get-ChildItem -Path $d -File -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -gt $sevenDaysAgo }) + $allFiles += $found + } catch {} + } + # De-noise + $filtered = $allFiles | Where-Object { + $f = $_.FullName + $exclude = $false + foreach ($pat in $excludePathPatterns) { if ($f -match $pat) { $exclude = $true; break } } + -not $exclude + } + $top50 = $filtered | Sort-Object LastWriteTime -Descending | Select-Object -First 50 + $execFiles = @($top50 | Where-Object { $_.Name -match $execExtensions }) + $audit.RecentlyModifiedFiles = [ordered]@{ + ScanDirectories = $scanDirs + TotalScanned = $allFiles.Count + AfterFilter = $filtered.Count + Top50 = @($top50 | ForEach-Object { + [ordered]@{ + Path = $_.FullName + SizeBytes = $_.Length + LastWriteTime = $_.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss") + Extension = $_.Extension + } + }) + ExecutableInUserDirs = @($execFiles | ForEach-Object { + [ordered]@{ + Path = $_.FullName + SizeBytes = $_.Length + LastWriteTime = $_.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss") + Extension = $_.Extension + } + }) + } + Write-Host " Scanned: $($allFiles.Count) files (after filter: $($filtered.Count)) across $($scanDirs.Count) dirs" + Write-Host " Executable-extension files in user-writable dirs (7d): $($execFiles.Count)" + if ($execFiles.Count -gt 0) { + $audit.SecuritySummary += "$($execFiles.Count) executable file(s) recently modified in user-writable dirs" + foreach ($f in ($execFiles | Select-Object -First 5)) { + Write-Host " [SUSP] $($f.FullName) -- $($f.LastWriteTime)" -ForegroundColor Yellow + } + } +} catch { + Write-Host " [ERROR] Recently modified files section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "RecentlyModifiedFiles"; Error = $_.Exception.Message } +} + +# ===================================================== +# 41. REMOTE ACCESS TOOLS INVENTORY +# ===================================================== +Write-Host "" +Write-Host "=== 41. REMOTE ACCESS TOOLS ===" -ForegroundColor Cyan +try { + $ratCatalog = @( + @{ Name = "ScreenConnect/ConnectWise Control"; ServicePattern = '^ScreenConnect Client'; Paths = @("C:\Program Files (x86)\ScreenConnect Client*", "C:\Program Files\ScreenConnect Client*") } + @{ Name = "TeamViewer"; ServicePattern = '^TeamViewer'; Paths = @("C:\Program Files\TeamViewer", "C:\Program Files (x86)\TeamViewer") } + @{ Name = "AnyDesk"; ServicePattern = '^AnyDesk'; Paths = @("C:\Program Files\AnyDesk", "C:\Program Files (x86)\AnyDesk", "C:\ProgramData\AnyDesk") } + @{ Name = "Splashtop"; ServicePattern = '^Splashtop'; Paths = @("C:\Program Files (x86)\Splashtop", "C:\Program Files\Splashtop") } + @{ Name = "LogMeIn"; ServicePattern = '^(LMI|LogMeIn)'; Paths = @("C:\Program Files (x86)\LogMeIn", "C:\Program Files\LogMeIn") } + @{ Name = "GoToAssist/GoToMyPC"; ServicePattern = '^(g2m|GoTo)'; Paths = @("C:\Program Files (x86)\Citrix\GoToAssist Expert*", "C:\Program Files (x86)\GoToMyPC") } + @{ Name = "Supremo"; ServicePattern = '^SupremoService'; Paths = @("C:\Program Files (x86)\SupremoRemoteDesktop", "C:\Program Files\SupremoRemoteDesktop") } + @{ Name = "RustDesk"; ServicePattern = '^RustDesk'; Paths = @("C:\Program Files\RustDesk") } + @{ Name = "Atera"; ServicePattern = '^AteraAgent'; Paths = @("C:\Program Files\ATERA Networks") } + @{ Name = "NinjaOne/NinjaRMM"; ServicePattern = '^NinjaRMMAgent'; Paths = @("C:\Program Files (x86)\NinjaRMMAgent", "C:\Program Files\NinjaRMMAgent") } + @{ Name = "Action1"; ServicePattern = '^Action1'; Paths = @("C:\Program Files (x86)\Action1") } + @{ Name = "Tailscale"; ServicePattern = '^Tailscale'; Paths = @("C:\Program Files\Tailscale") } + @{ Name = "ZeroTier"; ServicePattern = '^ZeroTierOneService'; Paths = @("C:\ProgramData\ZeroTier") } + @{ Name = "RemotePC"; ServicePattern = '^RemotePC'; Paths = @("C:\Program Files\RemotePC") } + @{ Name = "Kaseya VSA"; ServicePattern = '^KaseyaAgent'; Paths = @("C:\Program Files (x86)\Kaseya") } + @{ Name = "Datto RMM"; ServicePattern = '^CagService'; Paths = @("C:\Program Files (x86)\CentraStage") } + @{ Name = "N-able N-central"; ServicePattern = '^Windows Agent'; Paths = @("C:\Program Files (x86)\N-able Technologies") } + @{ Name = "Chrome Remote Desktop"; ServicePattern = '^chromoting'; Paths = @("C:\Program Files (x86)\Google\Chrome Remote Desktop") } + @{ Name = "GuruRMM"; ServicePattern = '^GuruRMM'; Paths = @("C:\Program Files\GuruRMM", "C:\ProgramData\GuruRMM") } + ) + $allSvcRat = @{} + Get-Service -ErrorAction SilentlyContinue | ForEach-Object { $allSvcRat[$_.Name] = $_ } + $rats = @() + foreach ($r in $ratCatalog) { + $matchedSvcs = @($allSvcRat.Values | Where-Object { $_.Name -match $r.ServicePattern -or $_.DisplayName -match $r.ServicePattern }) + $matchedPaths = @() + foreach ($p in $r.Paths) { + try { if (Get-ChildItem -Path $p -ErrorAction SilentlyContinue) { $matchedPaths += $p } } catch {} + } + if ($matchedSvcs.Count -gt 0 -or $matchedPaths.Count -gt 0) { + $rats += [ordered]@{ + Product = $r.Name + Services = @($matchedSvcs | ForEach-Object { [ordered]@{ Name = $_.Name; DisplayName = $_.DisplayName; Status = "$($_.Status)" } }) + InstallPaths = $matchedPaths + } + } + } + $audit.RemoteAccessTools = $rats + Write-Host " Remote-access tools detected: $($rats.Count)" + foreach ($rat in $rats) { + $running = @($rat.Services | Where-Object Status -eq "Running").Count + Write-Host " $($rat.Product) -- services: $($rat.Services.Count) ($running running) -- paths: $($rat.InstallPaths.Count)" + } + if ($rats.Count -gt 4) { + $audit.SecuritySummary += "$($rats.Count) remote-access tools installed -- review whether all are sanctioned" + } +} catch { + Write-Host " [ERROR] Remote access tools section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "RemoteAccessTools"; Error = $_.Exception.Message } +} + +# ===================================================== +# 42. NETWORK DEEPER (listening ports, established outbound, hosts, proxy, DNS) +# ===================================================== +Write-Host "" +Write-Host "=== 42. NETWORK DEEPER ===" -ForegroundColor Cyan +try { + $net = [ordered]@{} + + # Listening TCP with owning process + try { + $procIndex = @{} + Get-Process -ErrorAction SilentlyContinue | ForEach-Object { $procIndex[$_.Id] = $_ } + $listening = @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ForEach-Object { + $proc = $procIndex[[int]$_.OwningProcess] + [ordered]@{ + LocalAddress = $_.LocalAddress + LocalPort = $_.LocalPort + OwningProcessId = $_.OwningProcess + ProcessName = if ($proc) { $proc.ProcessName } else { "" } + ProcessPath = if ($proc -and $proc.Path) { $proc.Path } else { "" } + } + } | Sort-Object LocalPort) + $net.ListeningTCP = $listening + } catch { $net.ListeningTCPError = $_.Exception.Message; $net.ListeningTCP = @() } + + # Established outbound to public IPs + try { + $procIndex2 = @{} + Get-Process -ErrorAction SilentlyContinue | ForEach-Object { $procIndex2[$_.Id] = $_ } + $established = @(Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | Where-Object { + $r = $_.RemoteAddress + -not ($r -match '^(127\.|10\.|192\.168\.|169\.254\.|::1$|fe80:|0\.0\.0\.0$)' -or + $r -match '^172\.(1[6-9]|2[0-9]|3[01])\.') + } | ForEach-Object { + $proc = $procIndex2[[int]$_.OwningProcess] + [ordered]@{ + LocalPort = $_.LocalPort + RemoteAddress = $_.RemoteAddress + RemotePort = $_.RemotePort + ProcessName = if ($proc) { $proc.ProcessName } else { "" } + ProcessPath = if ($proc -and $proc.Path) { $proc.Path } else { "" } + } + } | Sort-Object ProcessName, RemoteAddress | Select-Object -First 100) + $net.EstablishedOutbound = $established + } catch { $net.EstablishedOutboundError = $_.Exception.Message; $net.EstablishedOutbound = @() } + + # HOSTS file + try { + $hostsPath = "$env:windir\System32\drivers\etc\hosts" + $hostsContent = Get-Content $hostsPath -ErrorAction SilentlyContinue + $hostsActive = @($hostsContent | Where-Object { $_ -and ($_ -notmatch '^\s*#') -and ($_.Trim() -ne '') }) + $net.HostsFile = [ordered]@{ + ActiveEntryCount = $hostsActive.Count + ActiveEntries = $hostsActive + } + if ($hostsActive.Count -gt 0) { + $audit.SecuritySummary += "HOSTS file has $($hostsActive.Count) non-default active entries" + } + } catch { $net.HostsFile = [ordered]@{ Error = $_.Exception.Message } } + + # Proxy (system + per-user) + try { + $proxyHKCU = Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" -ErrorAction SilentlyContinue + $net.ProxyHKCU = [ordered]@{ + ProxyEnable = $proxyHKCU.ProxyEnable + ProxyServer = "$($proxyHKCU.ProxyServer)" + AutoConfigURL = "$($proxyHKCU.AutoConfigURL)" + ProxyOverride = "$($proxyHKCU.ProxyOverride)" + } + $winhttp = (& netsh winhttp show proxy 2>&1) -join "`n" + $net.WinHTTPProxy = $winhttp.Trim() + if ($proxyHKCU.ProxyEnable -eq 1 -and $proxyHKCU.ProxyServer) { + $audit.SecuritySummary += "User proxy configured: $($proxyHKCU.ProxyServer) -- verify it's expected" + } + if ($proxyHKCU.AutoConfigURL) { + $audit.SecuritySummary += "Proxy auto-config URL set: $($proxyHKCU.AutoConfigURL)" + } + } catch { $net.ProxyError = $_.Exception.Message } + + # DNS servers per active interface + try { + $dns = @(Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.ServerAddresses.Count -gt 0 } | ForEach-Object { + [ordered]@{ + InterfaceAlias = $_.InterfaceAlias + InterfaceIndex = $_.InterfaceIndex + Servers = @($_.ServerAddresses) + } + }) + $net.DNSServers = $dns + } catch { $net.DNSServers = @() } + + # Network connection profiles per interface + try { + $net.ConnectionProfiles = @(Get-NetConnectionProfile -ErrorAction SilentlyContinue | ForEach-Object { + [ordered]@{ + InterfaceAlias = $_.InterfaceAlias + NetworkCategory = "$($_.NetworkCategory)" + IPv4Connectivity = "$($_.IPv4Connectivity)" + Name = $_.Name + } + }) + # Flag domain-joined machine on Public network + $publicProfiles = @($net.ConnectionProfiles | Where-Object NetworkCategory -eq "Public") + if ($audit.DomainMembership -and $audit.DomainMembership.PartOfDomain -and $publicProfiles.Count -gt 0) { + $audit.SecuritySummary += "Domain-joined machine has interface(s) on Public network profile" + } + } catch { $net.ConnectionProfiles = @() } + + $audit.NetworkDeeper = $net + Write-Host " Listening TCP: $(@($net.ListeningTCP).Count), Outbound (public): $(@($net.EstablishedOutbound).Count)" + Write-Host " HOSTS active entries: $($net.HostsFile.ActiveEntryCount)" + Write-Host " HKCU proxy enabled: $($net.ProxyHKCU.ProxyEnable)" + Write-Host " DNS interfaces with servers: $(@($net.DNSServers).Count)" +} catch { + Write-Host " [ERROR] Network Deeper section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "NetworkDeeper"; Error = $_.Exception.Message } +} + +# ===================================================== +# 43. BROWSER HYGIENE (Edge, Chrome, Brave, Firefox) +# ===================================================== +Write-Host "" +Write-Host "=== 43. BROWSER HYGIENE ===" -ForegroundColor Cyan +try { + $browsers = @() + + function Get-ChromiumExtensions { + param($ProfileDir, $BrowserName) + $extDir = Join-Path $ProfileDir "Extensions" + if (-not (Test-Path $extDir)) { return @() } + $exts = @() + Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue | ForEach-Object { + $extId = $_.Name + $verDir = Get-ChildItem -Path $_.FullName -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 + if ($verDir) { + $manifestPath = Join-Path $verDir.FullName "manifest.json" + if (Test-Path $manifestPath) { + try { + $manifest = Get-Content $manifestPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop + $name = "$($manifest.name)" + # Resolve __MSG_ name from default_locale messages.json if needed + if ($name -match '^__MSG_(.+)__$') { + $msgKey = $matches[1] + $defLocale = if ($manifest.default_locale) { $manifest.default_locale } else { "en" } + $msgPath = Join-Path $verDir.FullName "_locales\$defLocale\messages.json" + if (Test-Path $msgPath) { + try { + $messages = Get-Content $msgPath -Raw | ConvertFrom-Json + $msgEntry = $messages.PSObject.Properties | Where-Object { $_.Name -ieq $msgKey } | Select-Object -First 1 + if ($msgEntry) { $name = "$($msgEntry.Value.message)" } + } catch {} + } + } + $perms = @() + if ($manifest.permissions) { $perms += @($manifest.permissions) } + if ($manifest.host_permissions) { $perms += @($manifest.host_permissions) } + $exts += [ordered]@{ + Browser = $BrowserName + Id = $extId + Name = $name + Version = "$($manifest.version)" + Permissions = $perms + UpdateURL = "$($manifest.update_url)" + } + } catch { + $exts += [ordered]@{ Browser = $BrowserName; Id = $extId; Name = "(manifest unreadable)"; Error = $_.Exception.Message } + } + } + } + } + return $exts + } + + # Edge + $edgePath = "$env:LOCALAPPDATA\Microsoft\Edge\User Data" + if (Test-Path $edgePath) { + try { + $edgeVer = "" + try { $edgeVer = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Edge\BLBeacon" -ErrorAction Stop).version } catch {} + $profiles = @(Get-ChildItem -Path $edgePath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq "Default" -or $_.Name -like "Profile *" }) + $edgeExts = @() + foreach ($prof in $profiles) { $edgeExts += Get-ChromiumExtensions -ProfileDir $prof.FullName -BrowserName "Edge" } + $browsers += [ordered]@{ + Name = "Microsoft Edge" + Version = $edgeVer + Profiles = @($profiles | ForEach-Object { $_.Name }) + ExtensionCount = $edgeExts.Count + Extensions = $edgeExts + } + } catch { $audit._errors += @{ Section = "BrowserEdge"; Error = $_.Exception.Message } } + } + + # Chrome + $chromePath = "$env:LOCALAPPDATA\Google\Chrome\User Data" + if (Test-Path $chromePath) { + try { + $chromeVer = "" + try { $chromeVer = (Get-ItemProperty "HKLM:\SOFTWARE\Google\Chrome\BLBeacon" -ErrorAction Stop).version } catch {} + $profiles = @(Get-ChildItem -Path $chromePath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq "Default" -or $_.Name -like "Profile *" }) + $chromeExts = @() + foreach ($prof in $profiles) { $chromeExts += Get-ChromiumExtensions -ProfileDir $prof.FullName -BrowserName "Chrome" } + $browsers += [ordered]@{ + Name = "Google Chrome" + Version = $chromeVer + Profiles = @($profiles | ForEach-Object { $_.Name }) + ExtensionCount = $chromeExts.Count + Extensions = $chromeExts + } + } catch { $audit._errors += @{ Section = "BrowserChrome"; Error = $_.Exception.Message } } + } + + # Brave + $bravePath = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data" + if (Test-Path $bravePath) { + try { + $profiles = @(Get-ChildItem -Path $bravePath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq "Default" -or $_.Name -like "Profile *" }) + $braveExts = @() + foreach ($prof in $profiles) { $braveExts += Get-ChromiumExtensions -ProfileDir $prof.FullName -BrowserName "Brave" } + $browsers += [ordered]@{ + Name = "Brave" + Version = "" + Profiles = @($profiles | ForEach-Object { $_.Name }) + ExtensionCount = $braveExts.Count + Extensions = $braveExts + } + } catch { $audit._errors += @{ Section = "BrowserBrave"; Error = $_.Exception.Message } } + } + + # Firefox (different format -- extensions.json) + $firefoxBase = "$env:APPDATA\Mozilla\Firefox\Profiles" + if (Test-Path $firefoxBase) { + try { + $ffVer = "" + try { $ffVer = (Get-ItemProperty "HKLM:\SOFTWARE\Mozilla\Mozilla Firefox" -ErrorAction Stop).CurrentVersion } catch {} + $profiles = @(Get-ChildItem -Path $firefoxBase -Directory -ErrorAction SilentlyContinue) + $ffExts = @() + foreach ($prof in $profiles) { + $extJson = Join-Path $prof.FullName "extensions.json" + if (Test-Path $extJson) { + try { + $data = Get-Content $extJson -Raw | ConvertFrom-Json + foreach ($a in $data.addons) { + if ($a.location -eq "app-system-defaults" -or $a.location -eq "app-builtin") { continue } + $ffExts += [ordered]@{ + Browser = "Firefox" + Id = "$($a.id)" + Name = "$($a.defaultLocale.name)" + Version = "$($a.version)" + Active = [bool]$a.active + Type = "$($a.type)" + SourceURI = "$($a.sourceURI)" + } + } + } catch {} + } + } + $browsers += [ordered]@{ + Name = "Firefox" + Version = $ffVer + Profiles = @($profiles | ForEach-Object { $_.Name }) + ExtensionCount = $ffExts.Count + Extensions = $ffExts + } + } catch { $audit._errors += @{ Section = "BrowserFirefox"; Error = $_.Exception.Message } } + } + + $audit.Browsers = $browsers + foreach ($b in $browsers) { + Write-Host " $($b.Name) $($b.Version): profiles=$($b.Profiles.Count) extensions=$($b.ExtensionCount)" + } + # Flag extensions with risky permissions + $allExts = @() + foreach ($b in $browsers) { $allExts += $b.Extensions } + $riskyExts = @($allExts | Where-Object { $_.Permissions -and ($_.Permissions -join ',') -match '(?i)|http\*://|tabs|webRequestBlocking|cookies|history|downloads|management|nativeMessaging|debugger|proxy' }) + if ($riskyExts.Count -gt 0) { + $audit.SecuritySummary += "$($riskyExts.Count) browser extension(s) with broad/risky permissions" + Write-Host " Browser extensions with broad permissions: $($riskyExts.Count)" -ForegroundColor Yellow + } +} catch { + Write-Host " [ERROR] Browser Hygiene section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "BrowserHygiene"; Error = $_.Exception.Message } +} + +# ===================================================== +# 44. AUTHENTICATION POSTURE +# ===================================================== +Write-Host "" +Write-Host "=== 44. AUTHENTICATION POSTURE ===" -ForegroundColor Cyan +try { + $authp = [ordered]@{} + + # LSA Protection (RunAsPPL) + try { + $runAsPPL = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name RunAsPPL -ErrorAction SilentlyContinue).RunAsPPL + $runAsPPLBoot = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name RunAsPPLBoot -ErrorAction SilentlyContinue).RunAsPPLBoot + $authp.LSAProtection = [ordered]@{ + RunAsPPL = $runAsPPL + RunAsPPLBoot = $runAsPPLBoot + Enabled = ($runAsPPL -ge 1) + } + } catch { $authp.LSAProtection = [ordered]@{ Error = $_.Exception.Message } } + + # Credential Guard / HVCI + try { + $dg = Get-CimInstance -ClassName Win32_DeviceGuard -Namespace "root\Microsoft\Windows\DeviceGuard" -ErrorAction SilentlyContinue + if ($dg) { + $running = @($dg.SecurityServicesRunning) + $configured = @($dg.SecurityServicesConfigured) + $authp.DeviceGuard = [ordered]@{ + CredentialGuardRunning = ($running -contains 1) + HVCIRunning = ($running -contains 2) + CredentialGuardConfigured = ($configured -contains 1) + HVCIConfigured = ($configured -contains 2) + VirtualizationBasedSecurityStatus = "$($dg.VirtualizationBasedSecurityStatus)" + CodeIntegrityPolicyEnforcementStatus = "$($dg.CodeIntegrityPolicyEnforcementStatus)" + } + } else { $authp.DeviceGuard = [ordered]@{ Available = $false } } + } catch { $authp.DeviceGuard = [ordered]@{ Error = $_.Exception.Message } } + + # WDigest + try { + $wdigest = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest" -Name UseLogonCredential -ErrorAction SilentlyContinue).UseLogonCredential + $authp.WDigest = [ordered]@{ + UseLogonCredential = $wdigest + CleartextDisabled = ($wdigest -eq $null -or $wdigest -eq 0) + } + } catch { $authp.WDigest = [ordered]@{ Error = $_.Exception.Message } } + + # NTLM / LM + try { + $lmCompat = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name LmCompatibilityLevel -ErrorAction SilentlyContinue).LmCompatibilityLevel + $noLMHash = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name NoLMHash -ErrorAction SilentlyContinue).NoLMHash + $authp.NTLM = [ordered]@{ + LmCompatibilityLevel = $lmCompat + NoLMHash = $noLMHash + LMHashStored = ($noLMHash -ne 1) + } + } catch { $authp.NTLM = [ordered]@{ Error = $_.Exception.Message } } + + # Cached creds + try { + $cached = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name CachedLogonsCount -ErrorAction SilentlyContinue).CachedLogonsCount + $authp.CachedLogonsCount = $cached + } catch { $authp.CachedLogonsCount = $null } + + # klist (only meaningful for current session, may be empty when run as SYSTEM) + try { + $klistOut = (& klist 2>&1) -join "`n" + $tickets = @([regex]::Matches($klistOut, '#\d+>')) + $authp.KerberosTicketCount = $tickets.Count + } catch { $authp.KerberosTicketCount = $null } + + # dsregcmd /status -- AzureAD/Hybrid join state + WHfB + try { + $dsreg = (& dsregcmd /status 2>&1) -join "`n" + $authp.DSRegStatus = [ordered]@{ + AzureAdJoined = ($dsreg -match 'AzureAdJoined\s*:\s*YES') + DomainJoined = ($dsreg -match 'DomainJoined\s*:\s*YES') + EnterpriseJoined = ($dsreg -match 'EnterpriseJoined\s*:\s*YES') + DeviceId = if ($dsreg -match 'DeviceId\s*:\s*([\w-]+)') { $matches[1] } else { "" } + TenantName = if ($dsreg -match 'TenantName\s*:\s*(.+)') { $matches[1].Trim() } else { "" } + TenantId = if ($dsreg -match 'TenantId\s*:\s*([\w-]+)') { $matches[1] } else { "" } + WHfBEnabled = ($dsreg -match 'WamDefaultGUID.+microsoft' -or $dsreg -match 'NgcSet\s*:\s*YES') + } + } catch { $authp.DSRegStatus = [ordered]@{ Error = $_.Exception.Message } } + + $audit.AuthenticationPosture = $authp + + Write-Host " LSA Protection (RunAsPPL): $($authp.LSAProtection.Enabled)" + Write-Host " Credential Guard running: $($authp.DeviceGuard.CredentialGuardRunning)" + Write-Host " WDigest cleartext disabled: $($authp.WDigest.CleartextDisabled)" + Write-Host " NTLM LmCompatibilityLevel: $($authp.NTLM.LmCompatibilityLevel) (5 = recommended)" + Write-Host " Cached logons count: $($authp.CachedLogonsCount)" + Write-Host " AzureAdJoined: $($authp.DSRegStatus.AzureAdJoined), DomainJoined: $($authp.DSRegStatus.DomainJoined)" + + # Findings + if ($authp.LSAProtection.Enabled -eq $false) { + $audit.SecuritySummary += "LSA Protection (RunAsPPL) not enabled" + } + if ($authp.WDigest.CleartextDisabled -eq $false) { + $audit.SecuritySummary += "WDigest cleartext credentials not disabled" + } + if ($authp.NTLM.LmCompatibilityLevel -ne $null -and $authp.NTLM.LmCompatibilityLevel -lt 5) { + $audit.SecuritySummary += "NTLM LmCompatibilityLevel = $($authp.NTLM.LmCompatibilityLevel) (recommend 5)" + } + if ($authp.NTLM.LMHashStored) { + $audit.SecuritySummary += "LM hashes still stored (NoLMHash != 1)" + } + if ($authp.CachedLogonsCount -ne $null -and $authp.CachedLogonsCount -gt 4) { + $audit.SecuritySummary += "CachedLogonsCount = $($authp.CachedLogonsCount) (recommend <= 4 for security)" + } +} catch { + Write-Host " [ERROR] Authentication Posture section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "AuthenticationPosture"; Error = $_.Exception.Message } +} + +# ===================================================== +# 45. HARDWARE DEEPER (battery, SMART, driver problems) +# ===================================================== +Write-Host "" +Write-Host "=== 45. HARDWARE DEEPER ===" -ForegroundColor Cyan +try { + $hw = [ordered]@{} + + # Battery (laptop) + try { + $bat = @(Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue) + if ($bat.Count -gt 0) { + $batStatic = @(Get-CimInstance -Namespace "root\wmi" -ClassName BatteryStaticData -ErrorAction SilentlyContinue) + $batFull = @(Get-CimInstance -Namespace "root\wmi" -ClassName BatteryFullChargedCapacity -ErrorAction SilentlyContinue) + $batCycle = @(Get-CimInstance -Namespace "root\wmi" -ClassName BatteryCycleCount -ErrorAction SilentlyContinue) + $hw.Batteries = @() + for ($i=0; $i -lt $bat.Count; $i++) { + $design = if ($i -lt $batStatic.Count) { $batStatic[$i].DesignedCapacity } else { $null } + $full = if ($i -lt $batFull.Count) { $batFull[$i].FullChargedCapacity } else { $null } + $cycles = if ($i -lt $batCycle.Count) { $batCycle[$i].CycleCount } else { $null } + $healthPct = if ($design -and $full -and $design -gt 0) { [math]::Round(($full / $design) * 100, 1) } else { $null } + $hw.Batteries += [ordered]@{ + Name = "$($bat[$i].Name)" + Status = "$($bat[$i].Status)" + BatteryStatusCode = $bat[$i].BatteryStatus + EstimatedChargeRemainingPct = $bat[$i].EstimatedChargeRemaining + DesignCapacity_mWh = $design + FullChargeCapacity_mWh = $full + CycleCount = $cycles + HealthPercent = $healthPct + } + if ($healthPct -ne $null -and $healthPct -lt 60) { + $audit.SecuritySummary += "Battery health $healthPct% (consider replacement)" + } + } + } else { + $hw.Batteries = @() + } + } catch { $hw.BatteriesError = $_.Exception.Message; $hw.Batteries = @() } + + # SMART per physical disk + try { + $physDisks = @(Get-PhysicalDisk -ErrorAction SilentlyContinue) + $hw.SMART = @($physDisks | ForEach-Object { + $d = $_ + $rel = $null + try { $rel = $d | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue } catch {} + [ordered]@{ + FriendlyName = "$($d.FriendlyName)" + MediaType = "$($d.MediaType)" + BusType = "$($d.BusType)" + HealthStatus = "$($d.HealthStatus)" + OperationalStatus = "$($d.OperationalStatus)" + SizeGB = if ($d.Size) { [math]::Round($d.Size/1GB, 1) } else { $null } + Wear = if ($rel) { $rel.Wear } else { $null } + Temperature = if ($rel) { $rel.Temperature } else { $null } + ReadErrorsTotal = if ($rel) { $rel.ReadErrorsTotal } else { $null } + WriteErrorsTotal = if ($rel) { $rel.WriteErrorsTotal } else { $null } + PowerOnHours = if ($rel) { $rel.PowerOnHours } else { $null } + StartStopCycleCount = if ($rel) { $rel.StartStopCycleCount } else { $null } + } + }) + $unhealthy = @($hw.SMART | Where-Object { $_.HealthStatus -ne "Healthy" }) + if ($unhealthy.Count -gt 0) { + foreach ($d in $unhealthy) { + $audit.SecuritySummary += "Disk SMART unhealthy: $($d.FriendlyName) ($($d.HealthStatus))" + } + } + } catch { $hw.SMARTError = $_.Exception.Message; $hw.SMART = @() } + + # Driver problems (yellow-bang devices) + try { + if (Get-Command Get-PnpDevice -ErrorAction SilentlyContinue) { + $problemDevs = @(Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | Where-Object { $_.Status -in 'Error','Degraded','Unknown' }) + $hw.ProblemDevices = @($problemDevs | ForEach-Object { + [ordered]@{ + FriendlyName = $_.FriendlyName + Class = "$($_.Class)" + Status = "$($_.Status)" + Manufacturer = "$($_.Manufacturer)" + InstanceId = $_.InstanceId + ProblemCode = "$($_.Problem)" + } + }) + if ($problemDevs.Count -gt 0) { + $audit.SecuritySummary += "$($problemDevs.Count) device(s) with driver/PNP errors" + } + } + } catch { $hw.ProblemDevicesError = $_.Exception.Message; $hw.ProblemDevices = @() } + + $audit.HardwareDeeper = $hw + Write-Host " Batteries: $(@($hw.Batteries).Count) -- worst health: $((@($hw.Batteries | ForEach-Object HealthPercent | Where-Object { $_ -ne $null } | Sort-Object | Select-Object -First 1)))%" + Write-Host " Physical disks: $(@($hw.SMART).Count) -- unhealthy: $(@($hw.SMART | Where-Object { $_.HealthStatus -ne 'Healthy' }).Count)" + Write-Host " Devices with driver errors: $(@($hw.ProblemDevices).Count)" +} catch { + Write-Host " [ERROR] Hardware Deeper section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "HardwareDeeper"; Error = $_.Exception.Message } +} + +# ===================================================== +# 46. PERFORMANCE SNAPSHOT (top procs, memory, page file, uptime) +# ===================================================== +Write-Host "" +Write-Host "=== 46. PERFORMANCE SNAPSHOT ===" -ForegroundColor Cyan +try { + $perf = [ordered]@{} + + # Top processes + try { + $procs = @(Get-Process -ErrorAction SilentlyContinue) + $perf.TopByCPU = @($procs | Where-Object { $_.CPU -ne $null } | Sort-Object CPU -Descending | Select-Object -First 10 | ForEach-Object { + [ordered]@{ + Name = $_.ProcessName + Id = $_.Id + CPUSeconds = [math]::Round($_.CPU, 1) + WorkingSetMB = [math]::Round($_.WS / 1MB, 1) + Path = if ($_.Path) { $_.Path } else { "" } + } + }) + $perf.TopByMemory = @($procs | Sort-Object WS -Descending | Select-Object -First 10 | ForEach-Object { + [ordered]@{ + Name = $_.ProcessName + Id = $_.Id + WorkingSetMB = [math]::Round($_.WS / 1MB, 1) + CPUSeconds = if ($_.CPU) { [math]::Round($_.CPU, 1) } else { 0 } + Path = if ($_.Path) { $_.Path } else { "" } + } + }) + $perf.ProcessTotalCount = $procs.Count + } catch { $perf.TopProcessError = $_.Exception.Message } + + # Memory + try { + $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + $perf.Memory = [ordered]@{ + TotalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1) + FreeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1) + UsedPct = [math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 1) + } + $perf.Uptime = [ordered]@{ + LastBootTime = $os.LastBootUpTime.ToString("yyyy-MM-dd HH:mm:ss") + UptimeDays = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 2) + } + if ($perf.Uptime.UptimeDays -gt 30) { + $audit.SecuritySummary += "System uptime $($perf.Uptime.UptimeDays) days (recommend reboot for patches)" + } + if ($perf.Memory.UsedPct -gt 90) { + $audit.SecuritySummary += "Memory used $($perf.Memory.UsedPct)% (high pressure)" + } + } catch { $perf.MemoryError = $_.Exception.Message } + + # Page file + try { + $pf = @(Get-CimInstance -ClassName Win32_PageFileUsage -ErrorAction SilentlyContinue) + $perf.PageFiles = @($pf | ForEach-Object { + [ordered]@{ + Name = $_.Name + AllocatedBaseSizeMB = $_.AllocatedBaseSize + CurrentUsageMB = $_.CurrentUsage + PeakUsageMB = $_.PeakUsage + } + }) + } catch { $perf.PageFiles = @() } + + # Recent reboots (last 10) + try { + $rebootEvents = @(Get-WinEvent -FilterHashtable @{LogName='System'; Id=1074,6005,6006,6008,41} -MaxEvents 50 -ErrorAction SilentlyContinue) + $perf.RecentReboots = @($rebootEvents | Sort-Object TimeCreated -Descending | Select-Object -First 10 | ForEach-Object { + [ordered]@{ + Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") + Id = $_.Id + ProviderName = $_.ProviderName + Message = (($_.Message -split "`r?`n")[0]).Trim() + } + }) + } catch { $perf.RecentReboots = @() } + + $audit.Performance = $perf + Write-Host " Processes: $($perf.ProcessTotalCount), Memory: $($perf.Memory.UsedPct)% used ($($perf.Memory.FreeGB)/$($perf.Memory.TotalGB) GB free)" + Write-Host " Uptime: $($perf.Uptime.UptimeDays) days (last boot: $($perf.Uptime.LastBootTime))" + Write-Host " Page files: $($perf.PageFiles.Count), Recent reboot events: $($perf.RecentReboots.Count)" +} catch { + Write-Host " [ERROR] Performance section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "Performance"; Error = $_.Exception.Message } +} + +# ===================================================== +# 47. OFFICE / OUTLOOK +# ===================================================== +Write-Host "" +Write-Host "=== 47. OFFICE / OUTLOOK ===" -ForegroundColor Cyan +try { + $office = [ordered]@{} + + # Office C2R config + try { + $c2r = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration" -ErrorAction SilentlyContinue + if ($c2r) { + $office.ClickToRun = [ordered]@{ + VersionToReport = "$($c2r.VersionToReport)" + ProductReleaseIds = "$($c2r.ProductReleaseIds)" + CDNBaseUrl = "$($c2r.CDNBaseUrl)" + UpdateChannel = "$($c2r.UpdateChannel)" + ClientCulture = "$($c2r.ClientCulture)" + Platform = "$($c2r.Platform)" + SharedComputerLicensing = $c2r.SharedComputerLicensing + } + } + } catch { $office.ClickToRunError = $_.Exception.Message } + + # Outlook profiles + accounts + try { + $outlookProfileBases = @( + "HKCU:\Software\Microsoft\Office\16.0\Outlook\Profiles", + "HKCU:\Software\Microsoft\Office\15.0\Outlook\Profiles" + ) + $office.OutlookProfiles = @() + foreach ($base in $outlookProfileBases) { + if (Test-Path $base) { + Get-ChildItem -Path $base -ErrorAction SilentlyContinue | ForEach-Object { + $office.OutlookProfiles += [ordered]@{ + Hive = $base + ProfileName = $_.PSChildName + } + } + } + } + } catch { $office.OutlookProfilesError = $_.Exception.Message } + + # OST/PST sizes + try { + $pstLocations = @( + "$env:LOCALAPPDATA\Microsoft\Outlook", + "$env:USERPROFILE\Documents\Outlook Files" + ) | Where-Object { Test-Path $_ } + $office.MailFiles = @() + foreach ($loc in $pstLocations) { + Get-ChildItem -Path $loc -Filter "*.ost" -File -ErrorAction SilentlyContinue | ForEach-Object { + $office.MailFiles += [ordered]@{ Type = "OST"; Path = $_.FullName; SizeGB = [math]::Round($_.Length / 1GB, 2); LastWrite = $_.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss") } + } + Get-ChildItem -Path $loc -Filter "*.pst" -File -ErrorAction SilentlyContinue | ForEach-Object { + $office.MailFiles += [ordered]@{ Type = "PST"; Path = $_.FullName; SizeGB = [math]::Round($_.Length / 1GB, 2); LastWrite = $_.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss") } + } + } + $largeOST = @($office.MailFiles | Where-Object { $_.SizeGB -gt 50 }) + if ($largeOST.Count -gt 0) { + $audit.SecuritySummary += "$($largeOST.Count) large Outlook data file(s) >50GB (sync/perf risk)" + } + } catch { $office.MailFilesError = $_.Exception.Message } + + # Outlook add-ins + try { + $office.Addins = @() + foreach ($base in @("HKCU:\Software\Microsoft\Office\Outlook\Addins","HKLM:\Software\Microsoft\Office\Outlook\Addins","HKLM:\Software\Wow6432Node\Microsoft\Office\Outlook\Addins")) { + if (Test-Path $base) { + Get-ChildItem -Path $base -ErrorAction SilentlyContinue | ForEach-Object { + $sub = Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue + $office.Addins += [ordered]@{ + Hive = $base + ProgId = $_.PSChildName + FriendlyName = "$($sub.FriendlyName)" + Description = "$($sub.Description)" + LoadBehavior = $sub.LoadBehavior + } + } + } + } + } catch { $office.AddinsError = $_.Exception.Message } + + $audit.OfficeOutlook = $office + Write-Host " Office C2R version: $($office.ClickToRun.VersionToReport), channel: $($office.ClickToRun.UpdateChannel)" + Write-Host " Outlook profiles: $($office.OutlookProfiles.Count), Mail files: $($office.MailFiles.Count), Add-ins: $($office.Addins.Count)" +} catch { + Write-Host " [ERROR] Office/Outlook section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "OfficeOutlook"; Error = $_.Exception.Message } +} + +# ===================================================== +# 48. TIME + UPDATE EXTENDED +# ===================================================== +Write-Host "" +Write-Host "=== 48. TIME / UPDATE EXTENDED ===" -ForegroundColor Cyan +try { + $tu = [ordered]@{} + + # Time service + try { + $w32tm = (& w32tm /query /status 2>&1) -join "`n" + $tu.W32time = [ordered]@{ + Source = if ($w32tm -match 'Source:\s+(.+)') { $matches[1].Trim() } else { "" } + LastSync = if ($w32tm -match 'Last Successful Sync Time:\s+(.+)') { $matches[1].Trim() } else { "" } + Stratum = if ($w32tm -match 'Stratum:\s+(\d+)') { $matches[1] } else { "" } + LeapIndicator = if ($w32tm -match 'Leap Indicator:\s+(.+)') { $matches[1].Trim() } else { "" } + } + } catch { $tu.W32time = [ordered]@{ Error = $_.Exception.Message } } + + # Time zone + try { + $tz = Get-TimeZone -ErrorAction SilentlyContinue + $tu.TimeZone = if ($tz) { [ordered]@{ Id = $tz.Id; DisplayName = $tz.DisplayName; BaseUtcOffset = "$($tz.BaseUtcOffset)" } } else { @{} } + } catch { $tu.TimeZone = @{} } + + # Pending Windows Updates (COM) + try { + $session = New-Object -ComObject Microsoft.Update.Session -ErrorAction Stop + $searcher = $session.CreateUpdateSearcher() + $searchResult = $searcher.Search("IsInstalled=0 and IsHidden=0 and Type='Software'") + $tu.PendingUpdates = [ordered]@{ + Count = $searchResult.Updates.Count + Updates = @() + } + for ($i=0; $i -lt [math]::Min($searchResult.Updates.Count, 25); $i++) { + $up = $searchResult.Updates.Item($i) + $tu.PendingUpdates.Updates += [ordered]@{ + Title = "$($up.Title)" + IsCritical = ($up.MsrcSeverity -eq "Critical") + Severity = "$($up.MsrcSeverity)" + KBs = @($up.KBArticleIDs) + SizeMB = if ($up.MaxDownloadSize) { [math]::Round($up.MaxDownloadSize / 1MB, 1) } else { $null } + } + } + if ($searchResult.Updates.Count -gt 0) { + $audit.SecuritySummary += "$($searchResult.Updates.Count) pending Windows Update(s)" + } + } catch { $tu.PendingUpdates = [ordered]@{ Error = $_.Exception.Message } } + + # WU history failures (last 30d) + try { + $wu30 = (Get-Date).AddDays(-30) + $failures = @(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-WindowsUpdateClient/Operational'; Id=20,25,31; StartTime=$wu30} -ErrorAction SilentlyContinue) + $tu.WUHistoryFailures30d = [ordered]@{ + Count = $failures.Count + Last10 = @($failures | Sort-Object TimeCreated -Descending | Select-Object -First 10 | ForEach-Object { + [ordered]@{ + Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") + Id = $_.Id + Message = (($_.Message -split "`r?`n")[0]).Trim() + } + }) + } + } catch { $tu.WUHistoryFailures30d = [ordered]@{ Error = $_.Exception.Message } } + + $audit.TimeUpdateExtended = $tu + Write-Host " Time source: $($tu.W32time.Source) | Last sync: $($tu.W32time.LastSync)" + Write-Host " Time zone: $($tu.TimeZone.Id)" + Write-Host " Pending updates: $($tu.PendingUpdates.Count) | WU failures (30d): $($tu.WUHistoryFailures30d.Count)" +} catch { + Write-Host " [ERROR] Time/Update extended section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "TimeUpdateExtended"; Error = $_.Exception.Message } +} + +# ===================================================== +# 49. EVENT LOG ADVANCED (crashes, lockouts, encoded PS, defender) +# ===================================================== +Write-Host "" +Write-Host "=== 49. EVENT LOG ADVANCED ===" -ForegroundColor Cyan +try { + $ev = [ordered]@{} + $thirtyDays = (Get-Date).AddDays(-30) + + # Top crashing apps (Application 1000) + try { + $crashes = @(Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='Application Error'; Id=1000; StartTime=$thirtyDays} -ErrorAction SilentlyContinue) + $crashGrouped = $crashes | ForEach-Object { + # Message format: "Faulting application name: , version..." + if ($_.Message -match 'Faulting application name:\s+(\S+)') { $matches[1] } else { "unknown" } + } | Group-Object | Sort-Object Count -Descending | Select-Object -First 10 + $ev.AppCrashesTop10 = @($crashGrouped | ForEach-Object { + [ordered]@{ Application = $_.Name; CrashCount = $_.Count } + }) + $ev.AppCrashesTotal = $crashes.Count + } catch { $ev.AppCrashesError = $_.Exception.Message } + + # Account lockouts (Security 4740) + try { + $lockouts = @(Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4740; StartTime=$thirtyDays} -ErrorAction SilentlyContinue) + $ev.AccountLockouts30d = @($lockouts | Sort-Object TimeCreated -Descending | Select-Object -First 25 | ForEach-Object { + $msg = $_.Message + $accountName = if ($msg -match 'Account That Was Locked Out:\s*\r?\n\s*Security ID:\s+\S+\s*\r?\n\s*Account Name:\s+(.+)') { $matches[1].Trim() } else { "" } + $callerComputer = if ($msg -match 'Caller Computer Name:\s+(.+)') { $matches[1].Trim() } else { "" } + [ordered]@{ + Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") + AccountName = $accountName + CallerComputer = $callerComputer + } + }) + if ($lockouts.Count -gt 0) { + $audit.SecuritySummary += "$($lockouts.Count) account lockout event(s) in last 30d" + } + } catch { $ev.AccountLockoutsError = $_.Exception.Message } + + # Audit policy changes (Security 4719) + try { + $polChanges = @(Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4719; StartTime=$thirtyDays} -ErrorAction SilentlyContinue) + $ev.AuditPolicyChanges30d = $polChanges.Count + if ($polChanges.Count -gt 0) { + $audit.SecuritySummary += "$($polChanges.Count) audit policy change event(s) in last 30d" + } + } catch { $ev.AuditPolicyChangesError = $_.Exception.Message } + + # PowerShell encoded commands (Microsoft-Windows-PowerShell/Operational, EID 4104) + try { + $encScripts = @(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104; StartTime=$thirtyDays} -MaxEvents 500 -ErrorAction SilentlyContinue | + Where-Object { + $m = $_.Message + ($m -match 'FromBase64String' -or $m -match '-EncodedCommand' -or $m -match '-enc\s+[A-Za-z0-9+/=]{50,}' -or $m -match 'IEX\s*\(' -or $m -match 'Invoke-Expression') + }) + $ev.SuspiciousPowerShell30d = [ordered]@{ + Count = $encScripts.Count + Last10 = @($encScripts | Sort-Object TimeCreated -Descending | Select-Object -First 10 | ForEach-Object { + $snippet = ($_.Message -split "`r?`n" | Select-Object -First 3) -join " " + if ($snippet.Length -gt 300) { $snippet = $snippet.Substring(0, 300) + "..." } + [ordered]@{ + Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") + Snippet = $snippet + } + }) + } + if ($encScripts.Count -gt 5) { + $audit.SecuritySummary += "$($encScripts.Count) suspicious PowerShell scripts (base64/IEX/encoded) in last 30d" + } + } catch { $ev.SuspiciousPowerShellError = $_.Exception.Message } + + # Defender detections (Microsoft-Windows-Windows Defender/Operational, EID 1116, 1117) + try { + $defDet = @(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Windows Defender/Operational'; Id=1116,1117,1118,1119; StartTime=$thirtyDays} -ErrorAction SilentlyContinue) + $ev.DefenderDetections30d = [ordered]@{ + Count = $defDet.Count + Last10 = @($defDet | Sort-Object TimeCreated -Descending | Select-Object -First 10 | ForEach-Object { + [ordered]@{ + Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") + Id = $_.Id + Message = (($_.Message -split "`r?`n")[0]).Trim() + } + }) + } + if ($defDet.Count -gt 0) { + $audit.SecuritySummary += "$($defDet.Count) Defender detection event(s) in last 30d" + } + } catch { $ev.DefenderDetectionsError = $_.Exception.Message } + + # Sysmon (if installed, EID 1 = process create) - flag unsigned in user dirs + try { + $sysmon = @(Get-WinEvent -ListLog "Microsoft-Windows-Sysmon/Operational" -ErrorAction SilentlyContinue) + if ($sysmon) { + $ev.SysmonInstalled = $true + $sysmonProc = @(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1; StartTime=$thirtyDays} -MaxEvents 200 -ErrorAction SilentlyContinue | + Where-Object { + $m = $_.Message + ($m -match 'Image:\s+(C:\\Users\\[^\r\n]+\.exe)' -or $m -match 'Image:\s+(C:\\ProgramData\\[^\r\n]+\.exe)' -or $m -match 'Image:\s+(C:\\Windows\\Temp\\[^\r\n]+\.exe)') -and + ($m -notmatch 'Signed:\s+true') + }) + $ev.SysmonUnsignedExecsInUserDirs = $sysmonProc.Count + if ($sysmonProc.Count -gt 0) { + $audit.SecuritySummary += "Sysmon: $($sysmonProc.Count) unsigned exec(s) from user-writable dirs in 30d" + } + } else { $ev.SysmonInstalled = $false } + } catch { $ev.SysmonError = $_.Exception.Message } + + $audit.EventLogAdvanced = $ev + Write-Host " App crashes (30d): $($ev.AppCrashesTotal) total, top 10 apps captured" + Write-Host " Account lockouts (30d): $(@($ev.AccountLockouts30d).Count)" + Write-Host " Suspicious PowerShell (30d): $($ev.SuspiciousPowerShell30d.Count)" + Write-Host " Defender detections (30d): $($ev.DefenderDetections30d.Count)" + Write-Host " Sysmon installed: $($ev.SysmonInstalled)" +} catch { + Write-Host " [ERROR] Event Log Advanced section failed: $($_.Exception.Message)" -ForegroundColor Red + $audit._errors += @{ Section = "EventLogAdvanced"; Error = $_.Exception.Message } +} + # ===================================================== # DONE - SAVE JSON # =====================================================