sync: auto-sync from ACG-TECH03L at 2026-04-17 14:10:20

Author: Howard Enos
Machine: ACG-TECH03L
Timestamp: 2026-04-17 14:10:20
This commit is contained in:
2026-04-17 14:10:21 -07:00
parent e695743149
commit 71c9ddce9e

View File

@@ -1,8 +1,23 @@
# ========================================== # ============================================================
# UNIVERSAL WORKSTATION AUDIT SCRIPT # UNIVERSAL WORKSTATION AUDIT SCRIPT
# Works on Windows 10 / 11 (domain or workgroup) #
# Outputs: HOSTNAME_workstation_audit_DATE.json to C:\Temp # Script Version : 2.0.0
# ========================================== # Schema Version : 2.0 (output JSON shape; bump on breaking changes)
# Build Date : 2026-04-17
# Sections : 49 (originals 1-33; security/diag additions 34-49)
# Compatibility : Windows 10 21H2 -> Windows 11 25H2 (admin required)
#
# Outputs : C:\Temp\<HOSTNAME>_workstation_audit_<DATE>.json
# (UTF-8, ConvertTo-Json depth 10)
#
# Each top-level JSON key is one of: _metadata, _errors, then one
# property per section in execution order. Section names are stable;
# never rename without bumping Schema Version.
# ============================================================
$ScriptVersion = "2.0.1"
$ScriptSchemaVersion = "2.0"
$ScriptBuildDate = "2026-04-17"
# Auto-relaunch with ExecutionPolicy Bypass if needed # Auto-relaunch with ExecutionPolicy Bypass if needed
if ($MyInvocation.MyCommand.Path) { if ($MyInvocation.MyCommand.Path) {
@@ -27,22 +42,37 @@ $JsonFile = "$OutputDir\${Name}_workstation_audit_$Date.json"
if (!(Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir | Out-Null } if (!(Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir | Out-Null }
# Self-SHA (file-on-disk runs only; iex-streamed runs report "iex-streamed")
$ScriptSelfSHA256 = if ($MyInvocation.MyCommand.Path -and (Test-Path $MyInvocation.MyCommand.Path)) {
try { (Get-FileHash -Path $MyInvocation.MyCommand.Path -Algorithm SHA256 -ErrorAction Stop).Hash }
catch { "sha-computation-failed" }
} else { "iex-streamed" }
# Structured data collector # Structured data collector
$audit = [ordered]@{ $audit = [ordered]@{
_metadata = [ordered]@{ _metadata = [ordered]@{
ScriptVersion = "1.0" ScriptVersion = $ScriptVersion
ScriptType = "Workstation" ScriptSchemaVersion = $ScriptSchemaVersion
RunDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") ScriptBuildDate = $ScriptBuildDate
RunBy = "$env:USERDOMAIN\$env:USERNAME" ScriptSelfSHA256 = $ScriptSelfSHA256
Hostname = $env:COMPUTERNAME ScriptType = "Workstation"
SectionCount = 49
RunDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
RunDateUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
RunBy = "$env:USERDOMAIN\$env:USERNAME"
Hostname = $env:COMPUTERNAME
OSCaption = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue).Caption
OSBuild = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue).BuildNumber
PowerShellVersion = "$($PSVersionTable.PSVersion)"
} }
_errors = @() _errors = @()
} }
Write-Host "=======================================" Write-Host "============================================="
Write-Host " UNIVERSAL WORKSTATION AUDIT v1.0" Write-Host " UNIVERSAL WORKSTATION AUDIT v$ScriptVersion (schema $ScriptSchemaVersion)"
Write-Host " $((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))" Write-Host " Build $ScriptBuildDate - $($audit._metadata.SectionCount) sections"
Write-Host "=======================================" Write-Host " $($audit._metadata.RunDate)"
Write-Host "============================================="
Write-Host "" Write-Host ""
# ===================================================== # =====================================================
@@ -1292,10 +1322,10 @@ try {
NISEngineVersion = "$($mp.NISEngineVersion)" NISEngineVersion = "$($mp.NISEngineVersion)"
AntispywareSignatureLastUpdated = if ($mp.AntispywareSignatureLastUpdated) { $mp.AntispywareSignatureLastUpdated.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } 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 } AntivirusSignatureLastUpdated = if ($mp.AntivirusSignatureLastUpdated) { $mp.AntivirusSignatureLastUpdated.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
AntivirusSignatureAgeDays = $mp.AntivirusSignatureAge AntivirusSignatureAgeDays = if ($mp.AntivirusSignatureAge -eq 4294967295) { $null } else { $mp.AntivirusSignatureAge }
NISSignatureAgeDays = $mp.NISSignatureAge NISSignatureAgeDays = if ($mp.NISSignatureAge -eq 4294967295) { $null } else { $mp.NISSignatureAge }
FullScanAgeDays = $mp.FullScanAge FullScanAgeDays = if ($mp.FullScanAge -eq 4294967295) { $null } else { $mp.FullScanAge }
QuickScanAgeDays = $mp.QuickScanAge QuickScanAgeDays = if ($mp.QuickScanAge -eq 4294967295) { $null } else { $mp.QuickScanAge }
FullScanEndTime = if ($mp.FullScanEndTime) { $mp.FullScanEndTime.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } 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 } 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 } IsTamperProtected = if ($mp.PSObject.Properties.Match('IsTamperProtected').Count) { [bool]$mp.IsTamperProtected } else { $null }
@@ -1419,6 +1449,12 @@ try {
if ($def.Status.QuickScanAgeDays -ne $null -and $def.Status.QuickScanAgeDays -gt 14) { if ($def.Status.QuickScanAgeDays -ne $null -and $def.Status.QuickScanAgeDays -gt 14) {
$audit.SecuritySummary += "Defender quick scan stale ($($def.Status.QuickScanAgeDays)d)" $audit.SecuritySummary += "Defender quick scan stale ($($def.Status.QuickScanAgeDays)d)"
} }
if ($def.Status.QuickScanAgeDays -eq $null) {
$audit.SecuritySummary += "Defender quick scan has never run"
}
if ($def.Status.FullScanAgeDays -eq $null) {
$audit.SecuritySummary += "Defender full scan has never run"
}
if ($def.ControlledFolderAccess.EnableControlledFolderAccess -eq "0") { if ($def.ControlledFolderAccess.EnableControlledFolderAccess -eq "0") {
$audit.SecuritySummary += "Controlled Folder Access (anti-ransomware) disabled" $audit.SecuritySummary += "Controlled Folder Access (anti-ransomware) disabled"
} }
@@ -1584,6 +1620,13 @@ Write-Host "=== 38. SERVICES INVENTORY ===" -ForegroundColor Cyan
try { try {
$allSvc = @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue) $allSvc = @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue)
$suspSvc = @() $suspSvc = @()
# Allowlist for known-good Microsoft / vendor paths under ProgramData (auto-updating engines etc.)
$svcAllowlistPatterns = @(
'(?i)^[A-Z]:\\ProgramData\\Microsoft\\Windows Defender\\Platform\\',
'(?i)^[A-Z]:\\ProgramData\\Microsoft\\Windows Defender\\Definition Updates\\',
'(?i)^[A-Z]:\\ProgramData\\Package Cache\\',
'(?i)^[A-Z]:\\ProgramData\\Microsoft\\Click[Tt]o[Rr]un\\'
)
foreach ($s in $allSvc) { foreach ($s in $allSvc) {
$reasons = @() $reasons = @()
$path = "$($s.PathName)" $path = "$($s.PathName)"
@@ -1593,16 +1636,20 @@ try {
elseif ($path -match '^(\S+\.exe)') { $exe = $matches[1] } elseif ($path -match '^(\S+\.exe)') { $exe = $matches[1] }
else { $exe = ($path -split '\s')[0] } else { $exe = ($path -split '\s')[0] }
# Check against allowlist
$isAllowed = $false
foreach ($pat in $svcAllowlistPatterns) { if ($exe -match $pat) { $isAllowed = $true; break } }
# Path with spaces but not quoted (privilege-escalation classic) # 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)') { if ($path -and $path -notmatch '^"' -and $path -match '\s' -and $path -match '^([A-Z]:\\[^"]+\s+[^"]+\.exe)') {
$reasons += "Unquoted path with spaces" $reasons += "Unquoted path with spaces"
} }
# Binary in user-writable dir # Binary in user-writable dir (skip if allowlisted)
if ($exe -match '(?i)\\(users|programdata|temp|public|appdata)\\') { if (-not $isAllowed -and $exe -match '(?i)\\(users|programdata|temp|public|appdata)\\') {
$reasons += "Binary in user-writable directory: $exe" $reasons += "Binary in user-writable directory: $exe"
} }
# Not in standard system paths # Not in standard system paths (skip if allowlisted)
if ($exe -and $exe -notmatch '(?i)^[A-Z]:\\(Windows|Program Files|Program Files \(x86\))') { if (-not $isAllowed -and $exe -and $exe -notmatch '(?i)^[A-Z]:\\(Windows|Program Files|Program Files \(x86\))') {
if ($exe -notmatch '(?i)^[A-Z]:\\Users') { if ($exe -notmatch '(?i)^[A-Z]:\\Users') {
$reasons += "Binary outside standard system paths: $exe" $reasons += "Binary outside standard system paths: $exe"
} }
@@ -2744,21 +2791,61 @@ try {
# ===================================================== # =====================================================
# DONE - SAVE JSON # DONE - SAVE JSON
# (Banner moved to AFTER save so completion message reflects truth.
# ConvertTo-Json on PS 5.1 is slow on multi-MB hashtables; we time it,
# use [IO.File]::WriteAllText for fast write, and provide a per-section
# fallback if the monolithic serialize fails.)
# ===================================================== # =====================================================
Write-Host "" Write-Host ""
Write-Host "=======================================" Write-Host "[INFO] All section data collected. Serializing to JSON (can take 30-90s on large profiles)..." -ForegroundColor Cyan
Write-Host " WORKSTATION AUDIT COMPLETE" $saveStart = Get-Date
Write-Host "=======================================" $saveOk = $false
$saveErr = ""
try {
$jsonText = $audit | ConvertTo-Json -Depth 8 -ErrorAction Stop
[System.IO.File]::WriteAllText($JsonFile, $jsonText, [System.Text.UTF8Encoding]::new($false))
$saveOk = $true
$saveDuration = ((Get-Date) - $saveStart).TotalSeconds
$jsonSizeKB = [math]::Round((Get-Item $JsonFile).Length / 1KB, 1)
Write-Host "[OK] JSON written: $JsonFile ($jsonSizeKB KB in $([math]::Round($saveDuration,1))s)" -ForegroundColor Green
} catch {
$saveErr = $_.Exception.Message
Write-Host "[ERROR] Monolithic JSON serialize/write failed: $saveErr" -ForegroundColor Red
Write-Host "[INFO] Attempting per-section fallback export..." -ForegroundColor Yellow
try {
$fallbackDir = ($JsonFile -replace '\.json$','') + "_partial"
New-Item -ItemType Directory -Path $fallbackDir -Force | Out-Null
$secOk = 0; $secFail = 0
foreach ($k in @($audit.Keys)) {
$partPath = Join-Path $fallbackDir "$k.json"
try {
$partText = $audit[$k] | ConvertTo-Json -Depth 8 -ErrorAction Stop
[System.IO.File]::WriteAllText($partPath, $partText, [System.Text.UTF8Encoding]::new($false))
$secOk++
} catch {
[System.IO.File]::WriteAllText((Join-Path $fallbackDir "$k.ERROR.txt"), "Failed: $($_.Exception.Message)", [System.Text.UTF8Encoding]::new($false))
$secFail++
}
}
Write-Host "[OK] Fallback complete: $secOk sections saved, $secFail failed. Folder: $fallbackDir" -ForegroundColor Yellow
} catch {
Write-Host "[ERROR] Per-section fallback also failed: $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "======================================="
if ($saveOk) {
Write-Host " WORKSTATION AUDIT COMPLETE"
} else {
Write-Host " WORKSTATION AUDIT FINISHED (SAVE ISSUE)"
}
Write-Host "======================================="
$errorCount = $audit._errors.Count $errorCount = $audit._errors.Count
if ($errorCount -gt 0) { if ($errorCount -gt 0) {
Write-Host "Completed with $errorCount section errors (see _errors in JSON)" -ForegroundColor Yellow Write-Host "Section errors: $errorCount (see _errors in JSON)" -ForegroundColor Yellow
} else { } else {
Write-Host "All sections completed successfully" -ForegroundColor Green Write-Host "All sections completed successfully" -ForegroundColor Green
} }
Write-Host "JSON data: $JsonFile" Write-Host "JSON data: $JsonFile"
Write-Host "=======================================" Write-Host "======================================="
# Save JSON
$audit | ConvertTo-Json -Depth 10 | Out-File $JsonFile -Encoding UTF8