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:
@@ -1,8 +1,23 @@
|
||||
# ==========================================
|
||||
# ============================================================
|
||||
# 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
|
||||
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 }
|
||||
|
||||
# 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
|
||||
$audit = [ordered]@{
|
||||
_metadata = [ordered]@{
|
||||
ScriptVersion = "1.0"
|
||||
ScriptVersion = $ScriptVersion
|
||||
ScriptSchemaVersion = $ScriptSchemaVersion
|
||||
ScriptBuildDate = $ScriptBuildDate
|
||||
ScriptSelfSHA256 = $ScriptSelfSHA256
|
||||
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 = @()
|
||||
}
|
||||
|
||||
Write-Host "======================================="
|
||||
Write-Host " UNIVERSAL WORKSTATION AUDIT v1.0"
|
||||
Write-Host " $((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))"
|
||||
Write-Host "======================================="
|
||||
Write-Host "============================================="
|
||||
Write-Host " UNIVERSAL WORKSTATION AUDIT v$ScriptVersion (schema $ScriptSchemaVersion)"
|
||||
Write-Host " Build $ScriptBuildDate - $($audit._metadata.SectionCount) sections"
|
||||
Write-Host " $($audit._metadata.RunDate)"
|
||||
Write-Host "============================================="
|
||||
Write-Host ""
|
||||
|
||||
# =====================================================
|
||||
@@ -1292,10 +1322,10 @@ try {
|
||||
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
|
||||
AntivirusSignatureAgeDays = if ($mp.AntivirusSignatureAge -eq 4294967295) { $null } else { $mp.AntivirusSignatureAge }
|
||||
NISSignatureAgeDays = if ($mp.NISSignatureAge -eq 4294967295) { $null } else { $mp.NISSignatureAge }
|
||||
FullScanAgeDays = if ($mp.FullScanAge -eq 4294967295) { $null } else { $mp.FullScanAge }
|
||||
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 }
|
||||
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 }
|
||||
@@ -1419,6 +1449,12 @@ try {
|
||||
if ($def.Status.QuickScanAgeDays -ne $null -and $def.Status.QuickScanAgeDays -gt 14) {
|
||||
$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") {
|
||||
$audit.SecuritySummary += "Controlled Folder Access (anti-ransomware) disabled"
|
||||
}
|
||||
@@ -1584,6 +1620,13 @@ Write-Host "=== 38. SERVICES INVENTORY ===" -ForegroundColor Cyan
|
||||
try {
|
||||
$allSvc = @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue)
|
||||
$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) {
|
||||
$reasons = @()
|
||||
$path = "$($s.PathName)"
|
||||
@@ -1593,16 +1636,20 @@ try {
|
||||
elseif ($path -match '^(\S+\.exe)') { $exe = $matches[1] }
|
||||
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)
|
||||
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)\\') {
|
||||
# Binary in user-writable dir (skip if allowlisted)
|
||||
if (-not $isAllowed -and $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\))') {
|
||||
# Not in standard system paths (skip if allowlisted)
|
||||
if (-not $isAllowed -and $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"
|
||||
}
|
||||
@@ -2744,21 +2791,61 @@ try {
|
||||
|
||||
# =====================================================
|
||||
# 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 " WORKSTATION AUDIT COMPLETE"
|
||||
Write-Host "======================================="
|
||||
Write-Host "[INFO] All section data collected. Serializing to JSON (can take 30-90s on large profiles)..." -ForegroundColor Cyan
|
||||
$saveStart = Get-Date
|
||||
$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
|
||||
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 {
|
||||
Write-Host "All sections completed successfully" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "JSON data: $JsonFile"
|
||||
Write-Host "======================================="
|
||||
|
||||
# Save JSON
|
||||
$audit | ConvertTo-Json -Depth 10 | Out-File $JsonFile -Encoding UTF8
|
||||
|
||||
Reference in New Issue
Block a user