2856 lines
138 KiB
PowerShell
2856 lines
138 KiB
PowerShell
# ============================================================
|
|
# UNIVERSAL WORKSTATION AUDIT SCRIPT
|
|
#
|
|
# Script Version : 2.0.2
|
|
# 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 8, .NET WriteAllText)
|
|
#
|
|
# 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.
|
|
#
|
|
# Authoritative version is $ScriptVersion below; this comment is
|
|
# documentation only. CI/runtime reads $ScriptVersion + $ScriptSchemaVersion.
|
|
# ============================================================
|
|
|
|
$ScriptVersion = "2.0.2"
|
|
$ScriptSchemaVersion = "2.0"
|
|
$ScriptBuildDate = "2026-04-17"
|
|
|
|
# Auto-relaunch with ExecutionPolicy Bypass if needed
|
|
if ($MyInvocation.MyCommand.Path) {
|
|
$currentPolicy = Get-ExecutionPolicy -Scope Process
|
|
if ($currentPolicy -eq 'Restricted' -or $currentPolicy -eq 'AllSigned') {
|
|
Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File `"$($MyInvocation.MyCommand.Path)`"" -Wait -NoNewWindow
|
|
exit
|
|
}
|
|
}
|
|
|
|
# Verify running as admin
|
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
if (-not $isAdmin) {
|
|
Write-Host "ERROR: This script must be run as Administrator." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
$Date = Get-Date -Format 'yyyy-MM-dd'
|
|
$OutputDir = "C:\Temp"
|
|
$Name = $env:COMPUTERNAME
|
|
$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 = $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 v$ScriptVersion (schema $ScriptSchemaVersion)"
|
|
Write-Host " Build $ScriptBuildDate - $($audit._metadata.SectionCount) sections"
|
|
Write-Host " $($audit._metadata.RunDate)"
|
|
Write-Host "============================================="
|
|
Write-Host ""
|
|
|
|
# =====================================================
|
|
# 1. SYSTEM INFO
|
|
# =====================================================
|
|
Write-Host "=== 1. SYSTEM INFO ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-CimInstance Win32_OperatingSystem, Win32_ComputerSystem"
|
|
try {
|
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
$cs = Get-CimInstance Win32_ComputerSystem
|
|
$uptime = (Get-Date) - $os.LastBootUpTime
|
|
|
|
# Friendly version (e.g., 23H2)
|
|
$displayVersion = "N/A"
|
|
try {
|
|
$displayVersion = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop).DisplayVersion
|
|
} catch {}
|
|
|
|
$sysInfo = [ordered]@{
|
|
Hostname = $env:COMPUTERNAME
|
|
OS = $os.Caption
|
|
OSVersion = $os.Version
|
|
DisplayVersion = $displayVersion
|
|
BuildNumber = $os.BuildNumber
|
|
Architecture = $os.OSArchitecture
|
|
InstallDate = $os.InstallDate.ToString("yyyy-MM-dd")
|
|
LastBoot = $os.LastBootUpTime.ToString("yyyy-MM-dd HH:mm:ss")
|
|
UptimeDays = [math]::Round($uptime.TotalDays, 1)
|
|
Domain = $cs.Domain
|
|
DomainJoined = $cs.PartOfDomain
|
|
CurrentUser = $cs.UserName
|
|
TotalRAM_GB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
|
|
}
|
|
|
|
$sysInfo.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
|
|
$audit.SystemInfo = $sysInfo
|
|
Write-Host " [OK] System info collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "SystemInfo"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 2. HARDWARE
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 2. HARDWARE ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-CimInstance Win32_ComputerSystem, Win32_Processor, Win32_BIOS, Win32_SystemEnclosure"
|
|
try {
|
|
$hw = Get-CimInstance Win32_ComputerSystem
|
|
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
$bios = Get-CimInstance Win32_BIOS
|
|
$encl = Get-CimInstance Win32_SystemEnclosure | Select-Object -First 1
|
|
$battery = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue
|
|
|
|
$hardware = [ordered]@{
|
|
Manufacturer = $hw.Manufacturer
|
|
Model = $hw.Model
|
|
SerialNumber = if ($bios.SerialNumber) { $bios.SerialNumber } else { $encl.SerialNumber }
|
|
CPU = $cpu.Name
|
|
CPUCores = $cpu.NumberOfCores
|
|
CPULogical = $cpu.NumberOfLogicalProcessors
|
|
RAM_GB = [math]::Round($hw.TotalPhysicalMemory / 1GB, 1)
|
|
BIOSVersion = $bios.SMBIOSBIOSVersion
|
|
ChassisType = switch ($encl.ChassisTypes[0]) {
|
|
3 {"Desktop"} 4 {"Low Profile Desktop"} 5 {"Pizza Box"} 6 {"Mini Tower"}
|
|
7 {"Tower"} 8 {"Portable"} 9 {"Laptop"} 10 {"Notebook"} 11 {"Hand Held"}
|
|
12 {"Docking Station"} 13 {"All in One"} 14 {"Sub Notebook"} 15 {"Space-Saving"}
|
|
default {"Other ($($encl.ChassisTypes[0]))"}
|
|
}
|
|
HasBattery = ($null -ne $battery)
|
|
}
|
|
|
|
$hardware.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
|
|
$audit.Hardware = $hardware
|
|
Write-Host " [OK] Hardware info collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "Hardware"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 3. OS ACTIVATION STATUS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 3. OS ACTIVATION STATUS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-CimInstance SoftwareLicensingProduct"
|
|
try {
|
|
$lic = Get-CimInstance SoftwareLicensingProduct |
|
|
Where-Object { $_.PartialProductKey -and $_.Name -like '*Windows*' } |
|
|
Select-Object -First 1
|
|
|
|
$statusText = switch ($lic.LicenseStatus) {
|
|
0 {"Unlicensed"} 1 {"Licensed"} 2 {"OOB Grace"} 3 {"OOT Grace"}
|
|
4 {"Non-Genuine"} 5 {"Notification"} 6 {"Extended Grace"} default {"Unknown"}
|
|
}
|
|
|
|
$activation = [ordered]@{
|
|
ProductName = $lic.Name
|
|
LicenseStatus = $statusText
|
|
StatusCode = $lic.LicenseStatus
|
|
PartialKey = $lic.PartialProductKey
|
|
LicenseFamily = $lic.LicenseFamily
|
|
}
|
|
|
|
$activation.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
|
|
|
|
if ($lic.LicenseStatus -ne 1) {
|
|
Write-Host " WARNING: This machine is NOT fully licensed!" -ForegroundColor Red
|
|
}
|
|
|
|
$audit.Activation = $activation
|
|
Write-Host " [OK] Activation status collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "Activation"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 4. STORAGE
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 4. STORAGE ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-Volume, Get-PhysicalDisk"
|
|
try {
|
|
$volumes = Get-Volume | Where-Object { $_.DriveLetter } | Select-Object DriveLetter,
|
|
FileSystemLabel, FileSystem,
|
|
@{N='SizeGB';E={[math]::Round($_.Size/1GB,1)}},
|
|
@{N='FreeGB';E={[math]::Round($_.SizeRemaining/1GB,1)}},
|
|
@{N='UsedPct';E={if($_.Size -gt 0){[math]::Round(($_.Size - $_.SizeRemaining)/$_.Size * 100,0)}else{0}}},
|
|
HealthStatus
|
|
$volumes | Format-Table -AutoSize
|
|
$audit.Volumes = @($volumes | ForEach-Object {
|
|
[ordered]@{
|
|
Drive = "$($_.DriveLetter):"; Label = $_.FileSystemLabel; FileSystem = $_.FileSystem
|
|
SizeGB = $_.SizeGB; FreeGB = $_.FreeGB; UsedPct = $_.UsedPct; Health = "$($_.HealthStatus)"
|
|
}
|
|
})
|
|
Write-Host " [OK] Volumes collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "Volumes"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
Write-Host " Running: Get-PhysicalDisk"
|
|
try {
|
|
$disks = Get-PhysicalDisk | Select-Object DeviceId, FriendlyName, MediaType,
|
|
@{N='SizeGB';E={[math]::Round($_.Size/1GB,1)}}, HealthStatus, OperationalStatus
|
|
$disks | Format-Table -AutoSize
|
|
$audit.PhysicalDisks = @($disks | ForEach-Object {
|
|
[ordered]@{
|
|
DeviceId = "$($_.DeviceId)"; Name = $_.FriendlyName; MediaType = "$($_.MediaType)"
|
|
SizeGB = $_.SizeGB; Health = "$($_.HealthStatus)"; Status = "$($_.OperationalStatus)"
|
|
}
|
|
})
|
|
Write-Host " [OK] Physical disks collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] PhysicalDisk: $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "PhysicalDisks"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 5. BITLOCKER STATUS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 5. BITLOCKER STATUS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-BitLockerVolume"
|
|
try {
|
|
$bl = Get-BitLockerVolume -ErrorAction Stop
|
|
$bl | ForEach-Object {
|
|
Write-Host " $($_.MountPoint): $($_.VolumeStatus), Protection: $($_.ProtectionStatus), Method: $($_.EncryptionMethod)"
|
|
}
|
|
$audit.BitLocker = @($bl | ForEach-Object {
|
|
[ordered]@{
|
|
MountPoint = "$($_.MountPoint)"; VolumeStatus = "$($_.VolumeStatus)"
|
|
Protection = "$($_.ProtectionStatus)"; Method = "$($_.EncryptionMethod)"
|
|
KeyProtectors = @($_.KeyProtector | ForEach-Object { "$($_.KeyProtectorType)" })
|
|
}
|
|
})
|
|
Write-Host " [OK] BitLocker status collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
Write-Host " Trying manage-bde fallback..." -ForegroundColor Yellow
|
|
try {
|
|
$bde = manage-bde -status 2>&1
|
|
$bde | ForEach-Object { Write-Host " $_" }
|
|
}
|
|
catch {
|
|
Write-Host " BitLocker not available on this edition" -ForegroundColor Yellow
|
|
}
|
|
$audit._errors += @{ Section = "BitLocker"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 6. NETWORK CONFIG
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 6. NETWORK CONFIG ===" -ForegroundColor Cyan
|
|
|
|
Write-Host " Running: Get-NetAdapter"
|
|
try {
|
|
$adapters = Get-NetAdapter | Select-Object Name, InterfaceDescription, MacAddress, Status, LinkSpeed
|
|
$adapters | Format-Table -AutoSize
|
|
$audit.NetworkAdapters = @($adapters | ForEach-Object {
|
|
[ordered]@{ Name = $_.Name; Description = $_.InterfaceDescription; MAC = $_.MacAddress; Status = "$($_.Status)"; LinkSpeed = $_.LinkSpeed }
|
|
})
|
|
Write-Host " [OK] Adapters collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "NetworkAdapters"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
Write-Host " Running: Get-NetIPConfiguration"
|
|
try {
|
|
$ipConfigs = Get-NetIPConfiguration | Where-Object { $_.IPv4Address }
|
|
$audit.IPConfig = @($ipConfigs | ForEach-Object {
|
|
[ordered]@{
|
|
Interface = $_.InterfaceAlias
|
|
IPv4 = "$($_.IPv4Address.IPAddress)"
|
|
PrefixLength = $_.IPv4Address.PrefixLength
|
|
Gateway = "$($_.IPv4DefaultGateway.NextHop)"
|
|
DNS = @($_.DNSServer | Where-Object { $_.AddressFamily -eq 2 } | ForEach-Object { $_.ServerAddresses }) | Select-Object -Unique
|
|
DHCPEnabled = (Get-NetIPInterface -InterfaceAlias $_.InterfaceAlias -AddressFamily IPv4 -ErrorAction SilentlyContinue).Dhcp
|
|
}
|
|
})
|
|
$ipConfigs | Format-List InterfaceAlias, IPv4Address, IPv4DefaultGateway, DnsServer
|
|
Write-Host " [OK] IP config collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "IPConfig"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 7. DOMAIN MEMBERSHIP
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 7. DOMAIN MEMBERSHIP ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Win32_ComputerSystem + nltest + DirectorySearcher"
|
|
try {
|
|
$cs = Get-CimInstance Win32_ComputerSystem
|
|
$domainInfo = [ordered]@{
|
|
DomainJoined = $cs.PartOfDomain
|
|
Domain = $cs.Domain
|
|
Workgroup = if (-not $cs.PartOfDomain) { $cs.Workgroup } else { $null }
|
|
}
|
|
|
|
if ($cs.PartOfDomain) {
|
|
# Get site
|
|
try {
|
|
$site = nltest /dsgetsite 2>&1 | Select-Object -First 1
|
|
$domainInfo.ADSite = $site.Trim()
|
|
} catch {}
|
|
|
|
# Get DC
|
|
try {
|
|
$dcInfo = nltest /dsgetdc:$($cs.Domain) 2>&1
|
|
$dcLine = ($dcInfo | Select-String "DC: \\\\").Line
|
|
if ($dcLine) { $domainInfo.DC = $dcLine.Trim() }
|
|
} catch {}
|
|
|
|
# Get computer OU via DirectorySearcher (no RSAT needed)
|
|
try {
|
|
$searcher = New-Object System.DirectoryServices.DirectorySearcher
|
|
$searcher.Filter = "(&(objectClass=computer)(cn=$env:COMPUTERNAME))"
|
|
$result = $searcher.FindOne()
|
|
if ($result) { $domainInfo.ComputerDN = "$($result.Properties['distinguishedname'][0])" }
|
|
} catch {}
|
|
}
|
|
|
|
$domainInfo.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
|
|
$audit.DomainMembership = $domainInfo
|
|
Write-Host " [OK] Domain membership collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "DomainMembership"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 8. CURRENT USER INFO
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 8. CURRENT USER INFO ===" -ForegroundColor Cyan
|
|
Write-Host " Running: query user, whoami /groups"
|
|
try {
|
|
$currentUser = [ordered]@{
|
|
Username = $env:USERNAME
|
|
Domain = $env:USERDOMAIN
|
|
}
|
|
|
|
# Logged-in sessions
|
|
Write-Host " Active sessions:" -ForegroundColor Yellow
|
|
try {
|
|
$sessions = query user 2>&1
|
|
$sessions | ForEach-Object { Write-Host " $_" }
|
|
} catch { Write-Host " Unable to query sessions" }
|
|
|
|
# Group memberships (of the account running the script)
|
|
Write-Host " Group memberships:" -ForegroundColor Yellow
|
|
try {
|
|
$groups = whoami /groups /fo csv 2>&1 | ConvertFrom-Csv -ErrorAction Stop
|
|
$currentUser.Groups = @($groups | ForEach-Object { $_.'Group Name' })
|
|
$groups | ForEach-Object { Write-Host " $($_.'Group Name')" }
|
|
} catch {
|
|
Write-Host " Unable to enumerate groups" -ForegroundColor Yellow
|
|
}
|
|
|
|
$audit.CurrentUser = $currentUser
|
|
Write-Host " [OK] User info collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "CurrentUser"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 9. LOCAL USERS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 9. LOCAL USERS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-LocalUser"
|
|
try {
|
|
$localUsers = Get-LocalUser | Select-Object Name, Enabled, LastLogon, PasswordRequired, PasswordLastSet
|
|
$localUsers | Format-Table -AutoSize
|
|
$audit.LocalUsers = @($localUsers | ForEach-Object {
|
|
[ordered]@{
|
|
Name = $_.Name; Enabled = $_.Enabled
|
|
LastLogon = if ($_.LastLogon) { $_.LastLogon.ToString("yyyy-MM-dd") } else { $null }
|
|
PasswordRequired = $_.PasswordRequired
|
|
PasswordLastSet = if ($_.PasswordLastSet) { $_.PasswordLastSet.ToString("yyyy-MM-dd") } else { $null }
|
|
}
|
|
})
|
|
Write-Host " [OK] Local users collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message) - trying net user fallback" -ForegroundColor Red
|
|
try { net user } catch {}
|
|
$audit._errors += @{ Section = "LocalUsers"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 10. LOCAL ADMINISTRATORS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 10. LOCAL ADMINISTRATORS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-LocalGroupMember Administrators"
|
|
try {
|
|
$localAdmins = Get-LocalGroupMember Administrators | Select-Object Name, PrincipalSource, ObjectClass
|
|
$localAdmins | Format-Table -AutoSize
|
|
$audit.LocalAdmins = @($localAdmins | ForEach-Object {
|
|
[ordered]@{ Name = $_.Name; Source = "$($_.PrincipalSource)"; Type = "$($_.ObjectClass)" }
|
|
})
|
|
Write-Host " [OK] Local admins collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message) - trying net localgroup fallback" -ForegroundColor Red
|
|
try { net localgroup Administrators } catch {}
|
|
$audit._errors += @{ Section = "LocalAdmins"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 11. MAPPED DRIVES
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 11. MAPPED DRIVES ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-PSDrive, net use, HKU registry"
|
|
try {
|
|
# Current session drives
|
|
$mappedDrives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.DisplayRoot } |
|
|
Select-Object Name, @{N='UNC';E={$_.DisplayRoot}}
|
|
if ($mappedDrives) {
|
|
Write-Host " Current session drives:" -ForegroundColor Yellow
|
|
$mappedDrives | Format-Table -AutoSize
|
|
}
|
|
|
|
# net use
|
|
Write-Host " net use output:" -ForegroundColor Yellow
|
|
net use 2>&1 | ForEach-Object { Write-Host " $_" }
|
|
|
|
# Try to get logged-in user's persistent drives via HKU
|
|
$audit.MappedDrives = @()
|
|
try {
|
|
$loggedOnUser = (Get-CimInstance Win32_ComputerSystem).UserName
|
|
if ($loggedOnUser) {
|
|
$sid = (New-Object System.Security.Principal.NTAccount($loggedOnUser)).Translate(
|
|
[System.Security.Principal.SecurityIdentifier]).Value
|
|
try {
|
|
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS -ErrorAction Stop | Out-Null
|
|
$userDrives = Get-ItemProperty "HKU:\$sid\Network\*" -ErrorAction SilentlyContinue
|
|
if ($userDrives) {
|
|
Write-Host " Persistent drives for $loggedOnUser`:" -ForegroundColor Yellow
|
|
$userDrives | ForEach-Object {
|
|
Write-Host " $($_.PSChildName): -> $($_.RemotePath)"
|
|
$audit.MappedDrives += [ordered]@{ Drive = "$($_.PSChildName):"; UNC = $_.RemotePath; User = $loggedOnUser }
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
Remove-PSDrive HKU -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Host " Unable to read user registry for mapped drives: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
}
|
|
|
|
# Also add from PSDrive
|
|
if ($mappedDrives) {
|
|
foreach ($d in $mappedDrives) {
|
|
if (-not ($audit.MappedDrives | Where-Object { $_.Drive -eq "$($d.Name):" })) {
|
|
$audit.MappedDrives += [ordered]@{ Drive = "$($d.Name):"; UNC = $d.UNC; User = "CurrentSession" }
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host " [OK] Mapped drives collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "MappedDrives"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 12. WIFI PROFILES
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 12. WIFI PROFILES ===" -ForegroundColor Cyan
|
|
Write-Host " Running: netsh wlan show profiles, netsh wlan show interfaces"
|
|
try {
|
|
Write-Host " Saved WiFi profiles:" -ForegroundColor Yellow
|
|
$profiles = netsh wlan show profiles 2>&1
|
|
$profiles | ForEach-Object { Write-Host " $_" }
|
|
|
|
$profileNames = @($profiles | Select-String "All User Profile\s+:\s+(.+)" | ForEach-Object { $_.Matches.Groups[1].Value.Trim() })
|
|
|
|
Write-Host ""
|
|
Write-Host " Current connection:" -ForegroundColor Yellow
|
|
$interfaces = netsh wlan show interfaces 2>&1
|
|
$interfaces | ForEach-Object { Write-Host " $_" }
|
|
|
|
$connectedSSID = ($interfaces | Select-String "SSID\s+:\s+(.+)" | Select-Object -First 1)
|
|
if ($connectedSSID) { $connectedSSID = $connectedSSID.Matches.Groups[1].Value.Trim() }
|
|
|
|
$audit.WiFi = [ordered]@{
|
|
SavedProfiles = $profileNames
|
|
ConnectedSSID = $connectedSSID
|
|
}
|
|
Write-Host " [OK] WiFi profiles collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
Write-Host " (No WiFi adapter or service not running)" -ForegroundColor Yellow
|
|
$audit._errors += @{ Section = "WiFi"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 13. ANTIVIRUS / SECURITY CENTER
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 13. ANTIVIRUS / SECURITY CENTER ===" -ForegroundColor Cyan
|
|
|
|
Write-Host " Running: Get-CimInstance root/SecurityCenter2 AntiVirusProduct"
|
|
try {
|
|
$avProducts = Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct -ErrorAction Stop
|
|
$audit.Antivirus = @($avProducts | ForEach-Object {
|
|
$state = '{0:X6}' -f $_.productState
|
|
$enabled = if ($state.Substring(2,2) -eq '10') { $true } else { $false }
|
|
$upToDate = if ($state.Substring(4,2) -eq '00') { $true } else { $false }
|
|
|
|
Write-Host " $($_.displayName): Enabled=$enabled, UpToDate=$upToDate"
|
|
|
|
[ordered]@{
|
|
Name = $_.displayName; Enabled = $enabled; UpToDate = $upToDate
|
|
Path = $_.pathToSignedProductExe
|
|
}
|
|
})
|
|
Write-Host " [OK] AV products collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "Antivirus"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
Write-Host " Running: Get-MpComputerStatus (Windows Defender)"
|
|
try {
|
|
$defender = Get-MpComputerStatus -ErrorAction Stop
|
|
$defenderInfo = [ordered]@{
|
|
AMRunningMode = "$($defender.AMRunningMode)"
|
|
AntivirusEnabled = $defender.AntivirusEnabled
|
|
RealTimeProtection = $defender.RealTimeProtectionEnabled
|
|
BehaviorMonitor = $defender.BehaviorMonitorEnabled
|
|
SignatureLastUpdated = if ($defender.AntivirusSignatureLastUpdated) { $defender.AntivirusSignatureLastUpdated.ToString("yyyy-MM-dd HH:mm") } else { $null }
|
|
LastQuickScan = if ($defender.QuickScanEndTime) { $defender.QuickScanEndTime.ToString("yyyy-MM-dd HH:mm") } else { $null }
|
|
LastFullScan = if ($defender.FullScanEndTime) { $defender.FullScanEndTime.ToString("yyyy-MM-dd HH:mm") } else { $null }
|
|
}
|
|
|
|
$defenderInfo.GetEnumerator() | ForEach-Object { Write-Host " Defender $($_.Key): $($_.Value)" }
|
|
$audit.WindowsDefender = $defenderInfo
|
|
Write-Host " [OK] Defender status collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "WindowsDefender"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 14. PRINTERS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 14. PRINTERS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-Printer, Get-PrinterPort"
|
|
try {
|
|
$printers = Get-Printer | Select-Object Name, DriverName, PortName, Type, Shared
|
|
$printers | Format-Table -AutoSize
|
|
|
|
$ports = Get-PrinterPort | Where-Object { $_.Name -like 'TCP_*' -or $_.Name -like 'IP_*' -or $_.PrinterHostAddress } |
|
|
Select-Object Name, PrinterHostAddress, PortNumber
|
|
if ($ports) {
|
|
Write-Host " Network printer ports:" -ForegroundColor DarkGray
|
|
$ports | Format-Table -AutoSize
|
|
}
|
|
|
|
$audit.Printers = @($printers | ForEach-Object {
|
|
[ordered]@{ Name = $_.Name; Driver = $_.DriverName; Port = $_.PortName; Type = "$($_.Type)"; Shared = $_.Shared }
|
|
})
|
|
$audit.PrinterPorts = @($ports | ForEach-Object {
|
|
[ordered]@{ Name = $_.Name; IP = $_.PrinterHostAddress; Port = $_.PortNumber }
|
|
})
|
|
Write-Host " [OK] Printers collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "Printers"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 15. INSTALLED SOFTWARE (registry-based)
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 15. INSTALLED SOFTWARE ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry query (HKLM Uninstall keys)"
|
|
try {
|
|
$regPaths = @(
|
|
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
|
|
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
)
|
|
$software = Get-ItemProperty $regPaths -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.DisplayName } |
|
|
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate |
|
|
Sort-Object DisplayName -Unique
|
|
|
|
$software | Format-Table -AutoSize
|
|
|
|
$audit.InstalledSoftware = @($software | ForEach-Object {
|
|
[ordered]@{ Name = $_.DisplayName; Version = $_.DisplayVersion; Publisher = $_.Publisher; InstallDate = $_.InstallDate }
|
|
})
|
|
Write-Host " [OK] Software collected - $($software.Count) packages" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "InstalledSoftware"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 16. STARTUP PROGRAMS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 16. STARTUP PROGRAMS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-CimInstance Win32_StartupCommand"
|
|
try {
|
|
$startup = Get-CimInstance Win32_StartupCommand | Select-Object Name, Command, Location, User
|
|
$startup | Format-Table -AutoSize
|
|
|
|
$audit.StartupPrograms = @($startup | ForEach-Object {
|
|
[ordered]@{ Name = $_.Name; Command = $_.Command; Location = $_.Location; User = $_.User }
|
|
})
|
|
Write-Host " [OK] Startup programs collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "StartupPrograms"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 17. WINDOWS UPDATES (last 15)
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 17. WINDOWS UPDATES (last 15) ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-HotFix"
|
|
try {
|
|
$updates = Get-HotFix | Sort-Object InstalledOn -Descending -ErrorAction SilentlyContinue | Select-Object -First 15 |
|
|
Select-Object HotFixID, Description, InstalledOn, InstalledBy
|
|
$updates | Format-Table -AutoSize
|
|
|
|
$audit.WindowsUpdates = @($updates | ForEach-Object {
|
|
[ordered]@{
|
|
KB = $_.HotFixID; Description = $_.Description
|
|
InstalledOn = if ($_.InstalledOn) { $_.InstalledOn.ToString("yyyy-MM-dd") } else { $null }
|
|
}
|
|
})
|
|
Write-Host " [OK] Updates collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "WindowsUpdates"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 18. WINDOWS UPDATE SETTINGS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 18. WINDOWS UPDATE SETTINGS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry check for WSUS/AU settings"
|
|
try {
|
|
$wuSettings = [ordered]@{}
|
|
|
|
$wu = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -ErrorAction SilentlyContinue
|
|
if ($wu) {
|
|
$wuSettings.WUServer = $wu.WUServer
|
|
$wuSettings.WUStatusServer = $wu.WUStatusServer
|
|
Write-Host " WSUS Server: $($wu.WUServer)"
|
|
} else {
|
|
Write-Host " No WSUS configured (using Windows Update directly)"
|
|
$wuSettings.WUServer = "None (Windows Update)"
|
|
}
|
|
|
|
$au = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -ErrorAction SilentlyContinue
|
|
if ($au) {
|
|
$auOption = switch ($au.AUOptions) {
|
|
2 {"Notify before download"} 3 {"Auto download, notify install"}
|
|
4 {"Auto download, auto install"} 5 {"Allow local admin to choose"} default {"$($au.AUOptions)"}
|
|
}
|
|
$wuSettings.AutoUpdateOption = $auOption
|
|
Write-Host " Auto Update: $auOption"
|
|
}
|
|
|
|
$audit.WindowsUpdateSettings = $wuSettings
|
|
Write-Host " [OK] Update settings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "WindowsUpdateSettings"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 19. NETWORK SHARES
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 19. NETWORK SHARES (on this machine) ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-SmbShare"
|
|
try {
|
|
$shares = Get-SmbShare | Where-Object { $_.Name -notlike '*$' }
|
|
if ($shares) {
|
|
$shares | Format-Table Name, Path, Description -AutoSize
|
|
$audit.LocalShares = @($shares | ForEach-Object {
|
|
[ordered]@{ Name = $_.Name; Path = $_.Path; Description = $_.Description }
|
|
})
|
|
} else {
|
|
Write-Host " No user shares on this machine"
|
|
$audit.LocalShares = @()
|
|
}
|
|
Write-Host " [OK] Shares collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "LocalShares"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 20. POWER SETTINGS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 20. POWER SETTINGS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: powercfg /getactivescheme"
|
|
try {
|
|
$powerScheme = powercfg /getactivescheme 2>&1
|
|
Write-Host " $powerScheme"
|
|
$audit.PowerScheme = "$powerScheme".Trim()
|
|
Write-Host " [OK] Power settings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "PowerSettings"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 21. EVENT LOG ERRORS (last 14 days)
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 21. EVENT LOG ERRORS (last 14 days) ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-WinEvent - Critical/Error, last 14 days"
|
|
try {
|
|
$errSince = (Get-Date).AddDays(-14)
|
|
$errors = Get-WinEvent -FilterHashtable @{LogName='System'; Level=1,2; StartTime=$errSince} -MaxEvents 30 -ErrorAction Stop |
|
|
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, @{N='Message';E={$_.Message.Substring(0, [Math]::Min(200, $_.Message.Length))}}
|
|
Write-Host " $($errors.Count) critical/error events found"
|
|
$errors | Format-Table TimeCreated, Id, ProviderName -AutoSize
|
|
$audit.EventLogErrors = @($errors | ForEach-Object {
|
|
[ordered]@{ Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss"); EventId = $_.Id; Level = $_.LevelDisplayName; Source = $_.ProviderName; Message = $_.Message }
|
|
})
|
|
Write-Host " [OK] Event log errors collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " No critical/error events in System log (last 14 days)" -ForegroundColor Green
|
|
$audit.EventLogErrors = @()
|
|
}
|
|
|
|
# =====================================================
|
|
# 22. EVENT LOG WARNINGS (last 14 days)
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 22. EVENT LOG WARNINGS (last 14 days) ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-WinEvent - Warning level, last 14 days"
|
|
try {
|
|
$warnSince = (Get-Date).AddDays(-14)
|
|
$warnings = Get-WinEvent -FilterHashtable @{LogName='System'; Level=3; StartTime=$warnSince} -MaxEvents 50 -ErrorAction Stop |
|
|
Select-Object TimeCreated, Id, ProviderName, @{N='Message';E={$_.Message.Substring(0, [Math]::Min(200, $_.Message.Length))}}
|
|
Write-Host " $($warnings.Count) warning events found"
|
|
$warnings | Format-Table TimeCreated, Id, ProviderName -AutoSize
|
|
$audit.EventLogWarnings = @($warnings | ForEach-Object {
|
|
[ordered]@{ Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss"); EventId = $_.Id; Source = $_.ProviderName; Message = $_.Message }
|
|
})
|
|
Write-Host " [OK] Event log warnings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " No warning events in System log (last 14 days)" -ForegroundColor Green
|
|
$audit.EventLogWarnings = @()
|
|
}
|
|
|
|
# =====================================================
|
|
# 23. EVENT LOG SETTINGS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 23. EVENT LOG SETTINGS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-WinEvent -ListLog"
|
|
try {
|
|
$importantLogs = @('Application', 'Security', 'System', 'Setup', 'Microsoft-Windows-PowerShell/Operational')
|
|
$audit.EventLogSettings = @()
|
|
foreach ($logName in $importantLogs) {
|
|
try {
|
|
$log = Get-WinEvent -ListLog $logName -ErrorAction Stop
|
|
$logInfo = [ordered]@{
|
|
LogName = $log.LogName
|
|
MaxSizeKB = [math]::Round($log.MaximumSizeInBytes / 1KB)
|
|
CurrentSizeKB = [math]::Round($log.FileSize / 1KB)
|
|
RecordCount = $log.RecordCount
|
|
LogMode = "$($log.LogMode)"
|
|
IsEnabled = $log.IsEnabled
|
|
}
|
|
Write-Host " $($log.LogName): Max=$($logInfo.MaxSizeKB)KB, Mode=$($log.LogMode), Records=$($log.RecordCount)"
|
|
$audit.EventLogSettings += $logInfo
|
|
}
|
|
catch { Write-Host " $logName`: not available" -ForegroundColor DarkGray }
|
|
}
|
|
Write-Host " [OK] Event log settings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "EventLogSettings"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 24. WINDOWS FIREWALL PROFILES
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 24. WINDOWS FIREWALL PROFILES ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-NetFirewallProfile"
|
|
try {
|
|
$fwProfiles = Get-NetFirewallProfile -ErrorAction Stop
|
|
$audit.FirewallProfiles = @($fwProfiles | ForEach-Object {
|
|
$p = [ordered]@{
|
|
Profile = "$($_.Name)"; Enabled = $_.Enabled
|
|
DefaultInboundAction = "$($_.DefaultInboundAction)"
|
|
DefaultOutboundAction = "$($_.DefaultOutboundAction)"
|
|
}
|
|
Write-Host " $($_.Name): Enabled=$($_.Enabled), Inbound=$($_.DefaultInboundAction), Outbound=$($_.DefaultOutboundAction)"
|
|
if (-not $_.Enabled) {
|
|
Write-Host " [WARN] $($_.Name) firewall profile is DISABLED" -ForegroundColor Red
|
|
}
|
|
$p
|
|
})
|
|
Write-Host " [OK] Firewall profiles collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "FirewallProfiles"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 25. RDP SECURITY SETTINGS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 25. RDP SECURITY SETTINGS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry check for RDP settings"
|
|
try {
|
|
$tsReg = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
|
|
$rdpReg = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
|
|
|
|
$rdpEnabled = (Get-ItemProperty $tsReg -ErrorAction Stop).fDenyTSConnections
|
|
$nla = (Get-ItemProperty $rdpReg -ErrorAction SilentlyContinue).UserAuthentication
|
|
|
|
$rdpSettings = [ordered]@{
|
|
RDPEnabled = ($rdpEnabled -eq 0)
|
|
NLARequired = ($nla -eq 1)
|
|
}
|
|
$rdpSettings.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
|
|
|
|
if ($rdpSettings.RDPEnabled -and -not $rdpSettings.NLARequired) {
|
|
Write-Host " [WARN] RDP is enabled but NLA is NOT required" -ForegroundColor Yellow
|
|
}
|
|
|
|
$audit.RDPSettings = $rdpSettings
|
|
Write-Host " [OK] RDP settings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "RDPSettings"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 26. UAC SETTINGS
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 26. UAC SETTINGS ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry check for UAC"
|
|
try {
|
|
$uacReg = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction Stop
|
|
$uacSettings = [ordered]@{
|
|
EnableLUA = $uacReg.EnableLUA
|
|
ConsentPromptBehaviorAdmin = switch ($uacReg.ConsentPromptBehaviorAdmin) {
|
|
0 {"Elevate without prompting"} 1 {"Prompt for credentials on secure desktop"}
|
|
2 {"Prompt for consent on secure desktop"} 3 {"Prompt for credentials"}
|
|
4 {"Prompt for consent"} 5 {"Prompt for consent for non-Windows binaries"} default {"Unknown"}
|
|
}
|
|
}
|
|
$uacSettings.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
|
|
|
|
if ($uacReg.EnableLUA -ne 1) {
|
|
Write-Host " [WARN] UAC is DISABLED" -ForegroundColor Red
|
|
}
|
|
$audit.UACSettings = $uacSettings
|
|
Write-Host " [OK] UAC settings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "UACSettings"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 27. SCREEN LOCK / INACTIVITY TIMEOUT
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 27. SCREEN LOCK / INACTIVITY TIMEOUT ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry check for screen lock policy"
|
|
try {
|
|
$screenLock = [ordered]@{}
|
|
|
|
$inactivity = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction SilentlyContinue).InactivityTimeoutSecs
|
|
$screenLock.InactivityTimeoutSecs = $inactivity
|
|
if ($inactivity) {
|
|
Write-Host " Machine inactivity timeout: $inactivity seconds"
|
|
} else {
|
|
Write-Host " Machine inactivity timeout: Not configured"
|
|
Write-Host " [WARN] No machine inactivity timeout - HIPAA requires automatic session lock" -ForegroundColor Yellow
|
|
}
|
|
|
|
# Screensaver (may be SYSTEM context - try HKLM policy too)
|
|
$ssPolicy = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Control Panel\Desktop' -ErrorAction SilentlyContinue
|
|
if ($ssPolicy) {
|
|
$screenLock.ScreenSaverGPO = $true
|
|
$screenLock.ScreenSaveTimeout = $ssPolicy.ScreenSaveTimeOut
|
|
$screenLock.ScreenSaverSecure = $ssPolicy.ScreenSaverIsSecure
|
|
Write-Host " Screensaver GPO: Timeout=$($ssPolicy.ScreenSaveTimeOut)sec, Password=$($ssPolicy.ScreenSaverIsSecure)"
|
|
} else {
|
|
$screenLock.ScreenSaverGPO = $false
|
|
Write-Host " No screensaver GPO detected"
|
|
}
|
|
|
|
$audit.ScreenLockPolicy = $screenLock
|
|
Write-Host " [OK] Screen lock settings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "ScreenLock"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 28. USB STORAGE POLICY
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 28. USB STORAGE POLICY ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry check for USB storage restrictions"
|
|
try {
|
|
$usbStorage = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\USBSTOR' -ErrorAction Stop).Start
|
|
$usbStatus = switch ($usbStorage) { 3 { "Enabled" } 4 { "Disabled" } default { "Unknown ($usbStorage)" } }
|
|
Write-Host " USB Storage: $usbStatus"
|
|
|
|
$usbGPO = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices' -ErrorAction SilentlyContinue
|
|
if ($usbGPO) { Write-Host " GPO removable storage restrictions detected" }
|
|
|
|
if ($usbStorage -eq 3 -and -not $usbGPO) {
|
|
Write-Host " [WARN] USB storage is unrestricted - data exfiltration risk" -ForegroundColor Yellow
|
|
}
|
|
$audit.USBStoragePolicy = [ordered]@{ Status = $usbStatus; GPORestrictions = ($null -ne $usbGPO) }
|
|
Write-Host " [OK] USB storage policy collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "USBStorage"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 29. TLS/SSL CONFIGURATION
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 29. TLS/SSL CONFIGURATION ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry check for TLS protocol versions"
|
|
try {
|
|
$tlsVersions = @('SSL 2.0', 'SSL 3.0', 'TLS 1.0', 'TLS 1.1', 'TLS 1.2', 'TLS 1.3')
|
|
$audit.TLSConfig = [ordered]@{}
|
|
foreach ($ver in $tlsVersions) {
|
|
$clientPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$ver\Client"
|
|
$enabled = $null
|
|
if (Test-Path $clientPath) {
|
|
$enabled = (Get-ItemProperty $clientPath -ErrorAction SilentlyContinue).Enabled
|
|
}
|
|
$status = if ($null -eq $enabled) { "OS Default" } elseif ($enabled -eq 0) { "Disabled" } else { "Enabled" }
|
|
Write-Host " $ver`: $status"
|
|
$audit.TLSConfig[$ver] = $status
|
|
}
|
|
Write-Host " [OK] TLS config collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "TLSConfig"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 30. AUTOPLAY / AUTORUN
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 30. AUTOPLAY / AUTORUN ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Registry check for AutoPlay/AutoRun"
|
|
try {
|
|
$autoplay = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer' -ErrorAction SilentlyContinue).NoDriveTypeAutoRun
|
|
$autorun = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer' -ErrorAction SilentlyContinue).NoAutorun
|
|
|
|
if ($autoplay -eq 255) {
|
|
Write-Host " AutoPlay: Disabled for all drives" -ForegroundColor Green
|
|
} elseif ($autoplay) {
|
|
Write-Host " AutoPlay: Partially restricted (value: $autoplay)" -ForegroundColor Yellow
|
|
} else {
|
|
Write-Host " AutoPlay: Not restricted by policy"
|
|
Write-Host " [WARN] AutoPlay is not disabled - malware risk from USB/optical media" -ForegroundColor Yellow
|
|
}
|
|
|
|
$audit.AutoPlay = [ordered]@{ NoDriveTypeAutoRun = $autoplay; NoAutorun = $autorun }
|
|
Write-Host " [OK] AutoPlay settings collected" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "AutoPlay"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 31. SERVICES (with expected state check)
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 31. SERVICES (Auto start - stopped unexpectedly) ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-Service - Auto start services that are stopped"
|
|
try {
|
|
$stoppedAuto = Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' } |
|
|
Select-Object Name, DisplayName, Status, StartType
|
|
if ($stoppedAuto) {
|
|
Write-Host " [WARN] $($stoppedAuto.Count) auto-start services are NOT running:" -ForegroundColor Yellow
|
|
$stoppedAuto | Format-Table -AutoSize
|
|
} else {
|
|
Write-Host " All auto-start services are running" -ForegroundColor Green
|
|
}
|
|
$audit.StoppedAutoServices = @($stoppedAuto | ForEach-Object {
|
|
[ordered]@{ Name = $_.Name; DisplayName = $_.DisplayName; Status = "$($_.Status)" }
|
|
})
|
|
Write-Host " [OK] Service state check complete" -ForegroundColor Green
|
|
}
|
|
catch {
|
|
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
|
|
$audit._errors += @{ Section = "StoppedAutoServices"; Error = $_.Exception.Message }
|
|
}
|
|
|
|
# =====================================================
|
|
# 32. FAILED LOGON ATTEMPTS (last 7 days)
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 32. FAILED LOGON ATTEMPTS (last 7 days) ===" -ForegroundColor Cyan
|
|
Write-Host " Running: Get-WinEvent Security log - Event ID 4625"
|
|
$failedSince = (Get-Date).AddDays(-7)
|
|
$failedLogons = @(Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=$failedSince} -MaxEvents 50 -ErrorAction SilentlyContinue)
|
|
if ($failedLogons.Count -gt 0) {
|
|
$grouped = $failedLogons | ForEach-Object {
|
|
$xml = [xml]$_.ToXml()
|
|
$targetUser = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
|
|
$sourceIP = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'
|
|
[PSCustomObject]@{ Time = $_.TimeCreated; User = $targetUser; SourceIP = $sourceIP }
|
|
}
|
|
$summary = $grouped | Group-Object User | Sort-Object Count -Descending
|
|
Write-Host " $($failedLogons.Count) failed logon attempts:" -ForegroundColor Yellow
|
|
$summary | ForEach-Object { Write-Host " $($_.Name): $($_.Count) failures" -ForegroundColor Yellow }
|
|
$audit.FailedLogons = [ordered]@{
|
|
TotalCount = $failedLogons.Count
|
|
ByUser = @($summary | ForEach-Object { [ordered]@{ User = $_.Name; Count = $_.Count } })
|
|
}
|
|
} else {
|
|
Write-Host " No failed logon attempts" -ForegroundColor Green
|
|
$audit.FailedLogons = [ordered]@{ TotalCount = 0 }
|
|
}
|
|
Write-Host " [OK] Failed logons collected" -ForegroundColor Green
|
|
|
|
# =====================================================
|
|
# 33. SECURITY SUMMARY
|
|
# =====================================================
|
|
Write-Host ""
|
|
Write-Host "=== 33. SECURITY SUMMARY ===" -ForegroundColor Cyan
|
|
$audit.SecuritySummary = @()
|
|
|
|
# Check each finding
|
|
if ($audit.Activation -and $audit.Activation.StatusCode -ne 1) {
|
|
$audit.SecuritySummary += "OS is NOT fully licensed"
|
|
Write-Host " [WARN] OS is NOT fully licensed" -ForegroundColor Red
|
|
}
|
|
if ($audit.FirewallProfiles) {
|
|
$disabledFW = $audit.FirewallProfiles | Where-Object { -not $_.Enabled }
|
|
if ($disabledFW) {
|
|
$audit.SecuritySummary += "Windows Firewall disabled on: $($disabledFW.Profile -join ', ')"
|
|
Write-Host " [WARN] Windows Firewall disabled on: $($disabledFW.Profile -join ', ')" -ForegroundColor Red
|
|
}
|
|
}
|
|
if ($audit.BitLocker) {
|
|
$unencrypted = $audit.BitLocker | Where-Object { $_.Protection -eq 'Off' -or $_.VolumeStatus -eq 'FullyDecrypted' }
|
|
if ($unencrypted) {
|
|
$audit.SecuritySummary += "BitLocker not enabled on: $($unencrypted.MountPoint -join ', ')"
|
|
Write-Host " [WARN] BitLocker not enabled - HIPAA requires encryption at rest" -ForegroundColor Red
|
|
}
|
|
}
|
|
if (-not $audit.ScreenLockPolicy.InactivityTimeoutSecs -and -not $audit.ScreenLockPolicy.ScreenSaverGPO) {
|
|
$audit.SecuritySummary += "No screen lock / inactivity timeout configured"
|
|
Write-Host " [WARN] No screen lock configured - HIPAA requires automatic session lock" -ForegroundColor Yellow
|
|
}
|
|
if ($audit.USBStoragePolicy -and $audit.USBStoragePolicy.Status -eq 'Enabled' -and -not $audit.USBStoragePolicy.GPORestrictions) {
|
|
$audit.SecuritySummary += "USB storage unrestricted"
|
|
Write-Host " [WARN] USB storage unrestricted - data exfiltration risk" -ForegroundColor Yellow
|
|
}
|
|
if ($audit.UACSettings -and $audit.UACSettings.EnableLUA -ne 1) {
|
|
$audit.SecuritySummary += "UAC is disabled"
|
|
Write-Host " [WARN] UAC is disabled" -ForegroundColor Red
|
|
}
|
|
if ($audit.RDPSettings -and $audit.RDPSettings.RDPEnabled -and -not $audit.RDPSettings.NLARequired) {
|
|
$audit.SecuritySummary += "RDP enabled without NLA"
|
|
Write-Host " [WARN] RDP enabled without NLA" -ForegroundColor Yellow
|
|
}
|
|
if ($audit.Antivirus) {
|
|
$noAV = $audit.Antivirus | Where-Object { -not $_.Enabled }
|
|
if ($noAV) {
|
|
# Cross-reference with Get-MpComputerStatus - SecurityCenter2 is unreliable for Defender
|
|
$defenderActive = $audit.WindowsDefender -and $audit.WindowsDefender.AntivirusEnabled -and $audit.WindowsDefender.RealTimeProtection
|
|
$nonDefenderDown = $noAV | Where-Object { $_.Name -ne 'Windows Defender' }
|
|
if ($defenderActive -and -not $nonDefenderDown) {
|
|
# SecurityCenter2 false positive - Defender is actually running
|
|
} else {
|
|
$names = if ($nonDefenderDown) { $nonDefenderDown.Name -join ', ' } else { $noAV.Name -join ', ' }
|
|
$audit.SecuritySummary += "Antivirus not active: $names"
|
|
Write-Host " [WARN] Antivirus not active" -ForegroundColor Red
|
|
}
|
|
}
|
|
}
|
|
if ($audit.StoppedAutoServices.Count -gt 0) {
|
|
$audit.SecuritySummary += "$($audit.StoppedAutoServices.Count) auto-start services stopped"
|
|
Write-Host " [WARN] $($audit.StoppedAutoServices.Count) auto-start services stopped unexpectedly" -ForegroundColor Yellow
|
|
}
|
|
|
|
if ($audit.SecuritySummary.Count -eq 0) {
|
|
Write-Host " No critical security findings" -ForegroundColor Green
|
|
} else {
|
|
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 = 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 }
|
|
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.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"
|
|
}
|
|
$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 = @()
|
|
# 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)"
|
|
# 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] }
|
|
|
|
# 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 (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 (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"
|
|
}
|
|
}
|
|
# 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 (read via .NET to avoid PSObject metadata baggage in Get-Content output;
|
|
# Get-Content lines carry PSDrive/Provider reflection refs that ConvertTo-Json explodes into 40+ MB)
|
|
try {
|
|
$hostsPath = "$env:windir\System32\drivers\etc\hosts"
|
|
$hostsLines = if (Test-Path $hostsPath) { [System.IO.File]::ReadAllLines($hostsPath) } else { @() }
|
|
$hostsActive = @($hostsLines | Where-Object { $_ -and ($_ -notmatch '^\s*#') -and ($_.Trim() -ne '') } | ForEach-Object { [string]$_ })
|
|
$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)<all_urls>|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: <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
|
|
# (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 "[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 "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 "======================================="
|