Files
claudetools/projects/msp-tools/msp-audit-scripts/server_audit.ps1
Howard Enos 8d975c1b44 import: ingested 160 files from C:\Users\howar\Clients
Howard's personal MSP client documentation folder imported into shared
ClaudeTools repo via /import command. Scope:

Clients (structured MSP docs under clients/<name>/docs/):
- anaise       (NEW)  - 13 files
- cascades-tucson     - 47 files merged (existing had only reports/)
- dataforth           - 18 files merged (alongside incident reports)
- instrumental-music-center - 14 files merged
- khalsa       (NEW)  - 22 files, multi-site (camden, river)
- kittle       (NEW)  - 16 files incl. fix-pdf-preview, gpo-intranet-zone
- lens-auto-brokerage (NEW) - 3 files (name matches SOPS vault)
- _client_template    - 13-file scaffold for new clients

MSP tooling (projects/msp-tools/):
- msp-audit-scripts/ - server_audit.ps1, workstation_audit.ps1, README
- utilities/         - clean_printer_ports, win11_upgrade,
                       screenconnect-toolbox-commands

Credential handling:
- Extracted 1 inline password (Anaise DESKTOP-O8GF4SD / david)
  to SOPS vault: clients/anaise/desktop-o8gf4sd.sops.yaml
- Redacted overview.md with vault reference pattern
- Scanned all 160 files for keys/tokens/connection strings -
  no other credentials found

Skipped:
- Cascades/.claude/settings.local.json (per-machine config)
- Source-root CLAUDE.md (personal, claudetools has its own)
- scripts/server_audit.ps1 and workstation_audit.ps1 at source root
  (identical duplicates of msp-audit-scripts versions)

Memory updates:
- reference_client_docs_structure.md (layout, conventions, active list)
- reference_msp_audit_scripts.md (locations, ScreenConnect 80-char rule)

Session log: session-logs/2026-04-16-howard-client-docs-import.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 19:43:58 -07:00

2272 lines
100 KiB
PowerShell

# ==========================================
# UNIVERSAL WINDOWS SERVER AUDIT SCRIPT
# Works on Windows Server 2012 -> 2025
# Outputs: HOSTNAME_server_audit_DATE.json to C:\Temp
# ==========================================
# 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}_server_audit_$Date.json"
if (!(Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir | Out-Null }
# Structured data collector
$audit = [ordered]@{
_metadata = [ordered]@{
ScriptVersion = "2.1"
ScriptType = "Server"
RunDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
RunBy = "$env:USERDOMAIN\$env:USERNAME"
Hostname = $env:COMPUTERNAME
}
_errors = @()
}
Write-Host "======================================="
Write-Host " UNIVERSAL SERVER AUDIT v2.0"
Write-Host " $((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))"
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
$sysInfo = [ordered]@{
Hostname = $env:COMPUTERNAME
OS = $os.Caption
OSVersion = $os.Version
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
DomainRole = switch ($cs.DomainRole) {
0 {"Standalone Workstation"} 1 {"Member Workstation"} 2 {"Standalone Server"}
3 {"Member Server"} 4 {"Backup DC"} 5 {"Primary DC"} default {"Unknown ($($cs.DomainRole))"}
}
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
$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
BIOSDate = if ($bios.ReleaseDate) { $bios.ReleaseDate.ToString("yyyy-MM-dd") } else { "N/A" }
}
$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. STORAGE
# =====================================================
Write-Host ""
Write-Host "=== 3. STORAGE ===" -ForegroundColor Cyan
Write-Host " Running: Get-Volume"
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] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "PhysicalDisks"; Error = $_.Exception.Message }
}
# =====================================================
# 4. NETWORK CONFIG
# =====================================================
Write-Host ""
Write-Host "=== 4. NETWORK CONFIG ===" -ForegroundColor Cyan
Write-Host " Running: Get-NetAdapter"
try {
$adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } |
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)"
Gateway = "$($_.IPv4DefaultGateway.NextHop)"
DNS = @($_.DNSServer | Where-Object { $_.AddressFamily -eq 2 } | ForEach-Object { $_.ServerAddresses }) | Select-Object -Unique
}
})
$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 }
}
Write-Host " Running: Get-DnsClientServerAddress"
try {
Get-DnsClientServerAddress -AddressFamily IPv4 | Where-Object { $_.ServerAddresses.Count -gt 0 } |
Format-Table InterfaceAlias, ServerAddresses -AutoSize
Write-Host " [OK] DNS client config collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "DnsClient"; Error = $_.Exception.Message }
}
# =====================================================
# 5. ROUTING TABLE
# =====================================================
Write-Host ""
Write-Host "=== 5. ROUTING TABLE ===" -ForegroundColor Cyan
Write-Host " Running: Get-NetRoute"
try {
Get-NetRoute -AddressFamily IPv4 | Where-Object { $_.DestinationPrefix -ne '255.255.255.255/32' } |
Sort-Object DestinationPrefix |
Format-Table DestinationPrefix, NextHop, InterfaceAlias, RouteMetric -AutoSize
Write-Host " [OK] Routes collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "Routes"; Error = $_.Exception.Message }
}
# =====================================================
# 6. ARP TABLE
# =====================================================
Write-Host ""
Write-Host "=== 6. ARP TABLE ===" -ForegroundColor Cyan
Write-Host " Running: Get-NetNeighbor"
try {
$arp = Get-NetNeighbor -AddressFamily IPv4 | Where-Object { $_.State -ne 'Unreachable' -and $_.State -ne 'Permanent' } |
Select-Object IPAddress, LinkLayerAddress, State, InterfaceAlias
$arp | Format-Table -AutoSize
$audit.ARPTable = @($arp | ForEach-Object {
[ordered]@{ IP = $_.IPAddress; MAC = $_.LinkLayerAddress; State = "$($_.State)"; Interface = $_.InterfaceAlias }
})
Write-Host " [OK] ARP table collected - $($arp.Count) entries" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ARP"; Error = $_.Exception.Message }
}
# =====================================================
# 7. LISTENING PORTS
# =====================================================
Write-Host ""
Write-Host "=== 7. LISTENING PORTS ===" -ForegroundColor Cyan
Write-Host " Running: Get-NetTCPConnection -State Listen"
try {
$listeners = Get-NetTCPConnection -State Listen | ForEach-Object {
$proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
[PSCustomObject]@{
LocalAddress = $_.LocalAddress
LocalPort = $_.LocalPort
PID = $_.OwningProcess
ProcessName = $proc.ProcessName
}
} | Sort-Object LocalPort
$listeners | Format-Table -AutoSize
$audit.ListeningPorts = @($listeners | ForEach-Object {
[ordered]@{ Address = $_.LocalAddress; Port = $_.LocalPort; PID = $_.PID; Process = $_.ProcessName }
})
Write-Host " [OK] Listening ports collected - $($listeners.Count) ports" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ListeningPorts"; Error = $_.Exception.Message }
}
# =====================================================
# 8. WINDOWS FIREWALL RULES
# =====================================================
Write-Host ""
Write-Host "=== 8. WINDOWS FIREWALL RULES (Enabled) ===" -ForegroundColor Cyan
Write-Host " Running: Get-NetFirewallRule | Get-NetFirewallPortFilter"
try {
$fwRules = Get-NetFirewallRule | Where-Object { $_.Enabled -eq 'True' } | ForEach-Object {
$portFilter = $_ | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue
[PSCustomObject]@{
DisplayName = $_.DisplayName
Direction = "$($_.Direction)"
Action = "$($_.Action)"
Protocol = $portFilter.Protocol
LocalPort = $portFilter.LocalPort
RemotePort = $portFilter.RemotePort
}
}
$fwRules | Format-Table DisplayName, Direction, Action, Protocol, LocalPort -AutoSize
$audit.FirewallRules = @($fwRules | ForEach-Object {
[ordered]@{
Name = $_.DisplayName; Direction = $_.Direction; Action = $_.Action
Protocol = $_.Protocol; LocalPort = $_.LocalPort; RemotePort = $_.RemotePort
}
})
Write-Host " [OK] Firewall rules collected - $($fwRules.Count) enabled rules" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "FirewallRules"; Error = $_.Exception.Message }
}
# =====================================================
# 9. AD DOMAIN INFO
# =====================================================
Write-Host ""
Write-Host "=== 9. AD DOMAIN INFO ===" -ForegroundColor Cyan
Write-Host " Running: Get-ADDomain, Get-ADForest"
try {
Import-Module ActiveDirectory -ErrorAction Stop
$script:domain = Get-ADDomain
$script:forest = Get-ADForest
$domain = $script:domain
$forest = $script:forest
$adInfo = [ordered]@{
DomainName = $domain.DNSRoot
NetBIOSName = $domain.NetBIOSName
DomainMode = "$($domain.DomainMode)"
ForestName = $forest.Name
ForestMode = "$($forest.ForestMode)"
PDCEmulator = $domain.PDCEmulator
SchemaMaster = $forest.SchemaMaster
Sites = @($forest.Sites)
GlobalCatalogs = @($forest.GlobalCatalogs)
}
$adInfo.GetEnumerator() | ForEach-Object {
$val = if ($_.Value -is [array]) { $_.Value -join ", " } else { $_.Value }
Write-Host " $($_.Key): $val"
}
$audit.ADDomain = $adInfo
# Trusts
Write-Host " Running: Get-ADTrust"
try {
$trusts = Get-ADTrust -Filter *
if ($trusts) {
$trusts | Format-Table Name, Direction, TrustType, IntraForest -AutoSize
$audit.ADTrusts = @($trusts | ForEach-Object {
[ordered]@{ Name = $_.Name; Direction = "$($_.Direction)"; Type = "$($_.TrustType)"; IntraForest = $_.IntraForest }
})
} else {
Write-Host " No trusts configured"
$audit.ADTrusts = @()
}
}
catch {
Write-Host " [FAIL] Trusts: $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ADTrusts"; Error = $_.Exception.Message }
}
Write-Host " [OK] AD domain info collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host " (AD module not available - this may not be a domain controller)" -ForegroundColor Yellow
$audit._errors += @{ Section = "ADDomain"; Error = $_.Exception.Message }
}
# =====================================================
# 10. DOMAIN CONTROLLERS
# =====================================================
Write-Host ""
Write-Host "=== 10. DOMAIN CONTROLLERS ===" -ForegroundColor Cyan
Write-Host " Running: Get-ADDomainController -Filter *"
try {
$dcs = Get-ADDomainController -Filter *
$dcs | Format-Table HostName, IPv4Address, Site, IsGlobalCatalog, OperatingSystem -AutoSize
$audit.DomainControllers = @($dcs | ForEach-Object {
[ordered]@{
Hostname = $_.HostName; IP = $_.IPv4Address; Site = $_.Site
IsGC = $_.IsGlobalCatalog; OS = $_.OperatingSystem
}
})
Write-Host " [OK] Domain controllers collected - $($dcs.Count) DCs" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "DomainControllers"; Error = $_.Exception.Message }
}
# =====================================================
# 11. AD OU STRUCTURE
# =====================================================
Write-Host ""
Write-Host "=== 11. AD OU STRUCTURE ===" -ForegroundColor Cyan
Write-Host " Running: Get-ADOrganizationalUnit -Filter *"
try {
$ous = Get-ADOrganizationalUnit -Filter * -Properties Description, ProtectedFromAccidentalDeletion |
Sort-Object DistinguishedName
$ous | Format-Table Name, DistinguishedName, Description, ProtectedFromAccidentalDeletion -AutoSize -Wrap
$audit.ADOUs = @($ous | ForEach-Object {
[ordered]@{
Name = $_.Name; DN = $_.DistinguishedName; Description = $_.Description
Protected = $_.ProtectedFromAccidentalDeletion
}
})
Write-Host " [OK] OU structure collected - $($ous.Count) OUs" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ADOUs"; Error = $_.Exception.Message }
}
# =====================================================
# 12. AD USERS
# =====================================================
Write-Host ""
Write-Host "=== 12. AD USERS ===" -ForegroundColor Cyan
Write-Host " Running: Get-ADUser -Filter * -Properties ..."
try {
$users = Get-ADUser -Filter * -Properties LastLogonDate, Enabled, Description,
PasswordLastSet, PasswordNeverExpires, DistinguishedName, EmailAddress, WhenCreated |
Select-Object Name, SamAccountName, Enabled, LastLogonDate, PasswordLastSet,
PasswordNeverExpires, EmailAddress, Description, DistinguishedName, WhenCreated
$users | Format-Table Name, SamAccountName, Enabled, LastLogonDate, EmailAddress -AutoSize
$audit.ADUsers = @($users | ForEach-Object {
[ordered]@{
Name = $_.Name; SAM = $_.SamAccountName; Enabled = $_.Enabled
LastLogon = if ($_.LastLogonDate) { $_.LastLogonDate.ToString("yyyy-MM-dd") } else { $null }
PasswordLastSet = if ($_.PasswordLastSet) { $_.PasswordLastSet.ToString("yyyy-MM-dd") } else { $null }
PasswordNeverExpires = $_.PasswordNeverExpires
Email = $_.EmailAddress; Description = $_.Description
OU = $_.DistinguishedName; Created = if ($_.WhenCreated) { $_.WhenCreated.ToString("yyyy-MM-dd") } else { $null }
}
})
Write-Host " [OK] AD users collected - $($users.Count) users" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ADUsers"; Error = $_.Exception.Message }
}
# =====================================================
# 13. AD COMPUTERS
# =====================================================
Write-Host ""
Write-Host "=== 13. AD COMPUTERS ===" -ForegroundColor Cyan
Write-Host " Running: Get-ADComputer -Filter * -Properties ..."
try {
$computers = Get-ADComputer -Filter * -Properties LastLogonDate, OperatingSystem,
OperatingSystemVersion, IPv4Address, DistinguishedName, Description, WhenCreated |
Select-Object Name, OperatingSystem, OperatingSystemVersion, IPv4Address,
LastLogonDate, Description, DistinguishedName, WhenCreated
$computers | Format-Table Name, OperatingSystem, IPv4Address, LastLogonDate -AutoSize
$audit.ADComputers = @($computers | ForEach-Object {
[ordered]@{
Name = $_.Name; OS = $_.OperatingSystem; OSVersion = $_.OperatingSystemVersion
IP = $_.IPv4Address
LastLogon = if ($_.LastLogonDate) { $_.LastLogonDate.ToString("yyyy-MM-dd") } else { $null }
Description = $_.Description; OU = $_.DistinguishedName
Created = if ($_.WhenCreated) { $_.WhenCreated.ToString("yyyy-MM-dd") } else { $null }
}
})
Write-Host " [OK] AD computers collected - $($computers.Count) computers" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ADComputers"; Error = $_.Exception.Message }
}
# =====================================================
# 14. AD GROUPS WITH MEMBERS
# =====================================================
Write-Host ""
Write-Host "=== 14. AD GROUPS WITH MEMBERS ===" -ForegroundColor Cyan
Write-Host " Running: Get-ADGroup -Filter * + Get-ADGroupMember"
try {
$groups = Get-ADGroup -Filter * -Properties Description, Members |
Where-Object { $_.Members.Count -gt 0 -and $_.GroupCategory -eq 'Security' } |
Sort-Object Name
$audit.ADGroups = @()
foreach ($g in $groups) {
Write-Host " Group: $($g.Name) [$($g.GroupScope)]" -ForegroundColor Yellow
try {
$members = Get-ADGroupMember $g.DistinguishedName -ErrorAction Stop |
Select-Object Name, SamAccountName, objectClass
$members | Format-Table -AutoSize
$audit.ADGroups += [ordered]@{
Name = $g.Name; Scope = "$($g.GroupScope)"; Category = "$($g.GroupCategory)"
Description = $g.Description
Members = @($members | ForEach-Object { [ordered]@{ Name = $_.Name; SAM = $_.SamAccountName; Type = $_.objectClass } })
}
}
catch {
Write-Host " [FAIL] Could not enumerate members: $($_.Exception.Message)" -ForegroundColor Red
$audit.ADGroups += [ordered]@{
Name = $g.Name; Scope = "$($g.GroupScope)"; Error = $_.Exception.Message
}
}
}
Write-Host " [OK] AD groups collected - $($groups.Count) groups with members" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ADGroups"; Error = $_.Exception.Message }
}
# =====================================================
# 15. PRIVILEGED GROUPS
# =====================================================
Write-Host ""
Write-Host "=== 15. PRIVILEGED GROUPS ===" -ForegroundColor Cyan
Write-Host " Running: Get-ADGroupMember for privileged groups"
$privGroups = @("Domain Admins", "Enterprise Admins", "Schema Admins", "Administrators",
"Account Operators", "Backup Operators", "Server Operators")
$audit.PrivilegedGroups = @()
foreach ($pg in $privGroups) {
try {
$members = Get-ADGroupMember $pg -ErrorAction Stop | Select-Object Name, SamAccountName, objectClass
Write-Host " $pg`:" -ForegroundColor Yellow
if ($members) {
$members | ForEach-Object { Write-Host " $($_.Name) ($($_.SamAccountName)) [$($_.objectClass)]" }
$audit.PrivilegedGroups += [ordered]@{
Group = $pg
Members = @($members | ForEach-Object { [ordered]@{ Name = $_.Name; SAM = $_.SamAccountName; Type = $_.objectClass } })
}
} else {
Write-Host " (empty)"
$audit.PrivilegedGroups += [ordered]@{ Group = $pg; Members = @() }
}
}
catch {
Write-Host " $pg`: [FAIL] $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host " [OK] Privileged groups checked" -ForegroundColor Green
# =====================================================
# 16. GROUP POLICY
# =====================================================
Write-Host ""
Write-Host "=== 16. GROUP POLICY ===" -ForegroundColor Cyan
Write-Host " Running: Get-GPO -All + Get-GPOReport (for links)"
try {
Import-Module GroupPolicy -ErrorAction Stop
$gpos = Get-GPO -All
$audit.GPOs = @()
foreach ($gpo in $gpos) {
Write-Host " GPO: $($gpo.DisplayName)" -ForegroundColor Yellow
Write-Host " Status: $($gpo.GpoStatus) Modified: $($gpo.ModificationTime)"
$links = @()
try {
[xml]$report = Get-GPOReport -Guid $gpo.Id -ReportType Xml -ErrorAction Stop
if ($report.GPO.LinksTo) {
$report.GPO.LinksTo | ForEach-Object {
Write-Host " Link: $($_.SOMPath) [Enabled: $($_.Enabled)]"
$links += [ordered]@{ Path = $_.SOMPath; Enabled = $_.Enabled }
}
} else {
Write-Host " Link: (not linked)"
}
}
catch {
Write-Host " [FAIL] Could not get links: $($_.Exception.Message)" -ForegroundColor Red
}
$audit.GPOs += [ordered]@{
Name = $gpo.DisplayName; Status = "$($gpo.GpoStatus)"
Created = $gpo.CreationTime.ToString("yyyy-MM-dd")
Modified = $gpo.ModificationTime.ToString("yyyy-MM-dd")
Links = $links
}
}
Write-Host " [OK] GPOs collected - $($gpos.Count) GPOs" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "GPOs"; Error = $_.Exception.Message }
}
# =====================================================
# 17. LOCAL USERS
# =====================================================
Write-Host ""
Write-Host "=== 17. 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 }
}
# =====================================================
# 18. LOCAL ADMINISTRATORS
# =====================================================
Write-Host ""
Write-Host "=== 18. 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 }
}
# =====================================================
# 19. SHARES WITH PERMISSIONS
# =====================================================
Write-Host ""
Write-Host "=== 19. SHARES WITH PERMISSIONS ===" -ForegroundColor Cyan
Write-Host " Running: Get-SmbShare + Get-SmbShareAccess + Get-Acl"
try {
$shares = Get-SmbShare | Where-Object { $_.Name -notlike '*$' }
$audit.Shares = @()
foreach ($s in $shares) {
Write-Host " Share: \\$env:COMPUTERNAME\$($s.Name)" -ForegroundColor Yellow
Write-Host " Path: $($s.Path)"
Write-Host " Description: $($s.Description)"
$shareData = [ordered]@{
Name = $s.Name; Path = $s.Path; Description = $s.Description
SMBPermissions = @(); NTFSPermissions = @()
}
# SMB permissions
Write-Host " SMB Permissions:" -ForegroundColor DarkGray
try {
$smbPerms = Get-SmbShareAccess -Name $s.Name
$smbPerms | ForEach-Object {
Write-Host " $($_.AccountName): $($_.AccessRight) ($($_.AccessControlType))"
$shareData.SMBPermissions += [ordered]@{
Account = $_.AccountName; Right = "$($_.AccessRight)"; Type = "$($_.AccessControlType)"
}
}
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
}
# NTFS permissions
Write-Host " NTFS Permissions:" -ForegroundColor DarkGray
try {
if (Test-Path $s.Path) {
$acl = Get-Acl $s.Path
$acl.Access | ForEach-Object {
Write-Host " $($_.IdentityReference): $($_.FileSystemRights) ($($_.AccessControlType))"
$shareData.NTFSPermissions += [ordered]@{
Identity = "$($_.IdentityReference)"; Rights = "$($_.FileSystemRights)"
Type = "$($_.AccessControlType)"; Inherited = $_.IsInherited
}
}
}
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
}
$audit.Shares += $shareData
}
Write-Host " [OK] Shares collected - $($shares.Count) shares" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "Shares"; Error = $_.Exception.Message }
}
# =====================================================
# 20. INSTALLED ROLES/FEATURES
# =====================================================
Write-Host ""
Write-Host "=== 20. INSTALLED ROLES/FEATURES ===" -ForegroundColor Cyan
Write-Host " Running: Get-WindowsFeature | Where Installed"
try {
$roles = Get-WindowsFeature | Where-Object { $_.Installed } | Select-Object Name, DisplayName
$roles | Format-Table -AutoSize
$audit.InstalledRoles = @($roles | ForEach-Object {
[ordered]@{ Name = $_.Name; DisplayName = $_.DisplayName }
})
Write-Host " [OK] Roles collected - $($roles.Count) installed" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "InstalledRoles"; Error = $_.Exception.Message }
}
# =====================================================
# 21. DHCP
# =====================================================
Write-Host ""
Write-Host "=== 21. DHCP ===" -ForegroundColor Cyan
Write-Host " Running: Get-DhcpServerv4Scope + options + reservations + leases"
try {
Import-Module DhcpServer -ErrorAction Stop
$scopes = Get-DhcpServerv4Scope
$audit.DHCP = [ordered]@{ Scopes = @() }
# Server-level options
Write-Host " Server-level DHCP options:" -ForegroundColor Yellow
try {
$serverOpts = Get-DhcpServerv4OptionValue -ErrorAction Stop
$serverOpts | Format-Table OptionId, Name, Value -AutoSize
$audit.DHCP.ServerOptions = @($serverOpts | ForEach-Object {
[ordered]@{ OptionId = $_.OptionId; Name = $_.Name; Value = @($_.Value) }
})
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
}
# Failover
try {
$failover = Get-DhcpServerv4Failover -ErrorAction Stop
if ($failover) {
$failover | Format-Table Name, PartnerServer, Mode, State -AutoSize
$audit.DHCP.Failover = @($failover | ForEach-Object {
[ordered]@{ Name = $_.Name; Partner = $_.PartnerServer; Mode = "$($_.Mode)"; State = "$($_.State)" }
})
}
}
catch { Write-Host " No DHCP failover configured" -ForegroundColor DarkGray }
foreach ($scope in $scopes) {
$scopeData = [ordered]@{
ScopeId = "$($scope.ScopeId)"; Name = $scope.Name
SubnetMask = "$($scope.SubnetMask)"; StartRange = "$($scope.StartRange)"
EndRange = "$($scope.EndRange)"; State = "$($scope.State)"
LeaseDuration = "$($scope.LeaseDuration)"
Options = @(); Exclusions = @(); Reservations = @(); Leases = @()
}
Write-Host " Scope: $($scope.ScopeId) ($($scope.Name))" -ForegroundColor Yellow
Write-Host " Range: $($scope.StartRange) - $($scope.EndRange), Mask: $($scope.SubnetMask), State: $($scope.State)"
# Scope options
try {
$scopeOpts = Get-DhcpServerv4OptionValue -ScopeId $scope.ScopeId -ErrorAction Stop
Write-Host " Options:"
$scopeOpts | ForEach-Object { Write-Host " $($_.OptionId) $($_.Name): $($_.Value -join ', ')" }
$scopeData.Options = @($scopeOpts | ForEach-Object {
[ordered]@{ OptionId = $_.OptionId; Name = $_.Name; Value = @($_.Value) }
})
}
catch { Write-Host " Options: [FAIL] $($_.Exception.Message)" -ForegroundColor Red }
# Exclusions
try {
$exclusions = Get-DhcpServerv4ExclusionRange -ScopeId $scope.ScopeId -ErrorAction Stop
if ($exclusions) {
Write-Host " Exclusions:"
$exclusions | ForEach-Object { Write-Host " $($_.StartRange) - $($_.EndRange)" }
$scopeData.Exclusions = @($exclusions | ForEach-Object {
[ordered]@{ Start = "$($_.StartRange)"; End = "$($_.EndRange)" }
})
}
}
catch {}
# Reservations
try {
$reservations = Get-DhcpServerv4Reservation -ScopeId $scope.ScopeId -ErrorAction Stop
if ($reservations) {
Write-Host " Reservations:"
$reservations | Format-Table IPAddress, Name, ClientId, Description -AutoSize
$scopeData.Reservations = @($reservations | ForEach-Object {
[ordered]@{ IP = "$($_.IPAddress)"; Name = $_.Name; MAC = $_.ClientId; Description = $_.Description }
})
}
}
catch { Write-Host " Reservations: [FAIL] $($_.Exception.Message)" -ForegroundColor Red }
# Leases
try {
$leases = Get-DhcpServerv4Lease -ScopeId $scope.ScopeId -ErrorAction Stop
if ($leases) {
Write-Host " Active Leases:"
$leases | Format-Table IPAddress, HostName, ClientId, AddressState -AutoSize
$scopeData.Leases = @($leases | ForEach-Object {
[ordered]@{ IP = "$($_.IPAddress)"; Hostname = $_.HostName; MAC = $_.ClientId; State = "$($_.AddressState)" }
})
}
}
catch { Write-Host " Leases: [FAIL] $($_.Exception.Message)" -ForegroundColor Red }
$audit.DHCP.Scopes += $scopeData
}
Write-Host " [OK] DHCP collected - $($scopes.Count) scopes" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host " (DHCP role not installed or not running)" -ForegroundColor Yellow
$audit._errors += @{ Section = "DHCP"; Error = $_.Exception.Message }
}
# =====================================================
# 22. DNS SERVER
# =====================================================
Write-Host ""
Write-Host "=== 22. DNS SERVER ===" -ForegroundColor Cyan
Write-Host " Running: Get-DnsServerZone, Get-DnsServerForwarder, Get-DnsServerResourceRecord"
try {
Import-Module DnsServer -ErrorAction Stop
# Forwarders
Write-Host " Forwarders:" -ForegroundColor Yellow
$fwd = Get-DnsServerForwarder
$fwd.IPAddress | ForEach-Object { Write-Host " $_" }
$audit.DNS = [ordered]@{
Forwarders = @($fwd.IPAddress | ForEach-Object { "$_" })
ConditionalForwarders = @()
Zones = @()
}
# Conditional forwarders
Write-Host " Conditional Forwarders:" -ForegroundColor Yellow
$condFwd = Get-DnsServerZone | Where-Object { $_.ZoneType -eq 'Forwarder' }
if ($condFwd) {
$condFwd | ForEach-Object {
Write-Host " $($_.ZoneName) -> $($_.MasterServers -join ', ')"
$audit.DNS.ConditionalForwarders += [ordered]@{ Zone = $_.ZoneName; ForwardTo = @($_.MasterServers | ForEach-Object { "$_" }) }
}
} else {
Write-Host " (none)"
}
# Zones and records
Write-Host " Zones:" -ForegroundColor Yellow
$zones = Get-DnsServerZone | Where-Object { $_.ZoneType -ne 'Forwarder' }
foreach ($z in $zones) {
$zoneData = [ordered]@{
Name = $z.ZoneName; Type = "$($z.ZoneType)"; IsReverse = $z.IsReverseLookupZone
DynamicUpdate = "$($z.DynamicUpdate)"; Records = @()
}
Write-Host " $($z.ZoneName) [$($z.ZoneType)] Reverse=$($z.IsReverseLookupZone)" -ForegroundColor DarkGray
# Get records for primary zones (skip if >500 records to avoid flooding)
if ($z.ZoneType -eq 'Primary' -and -not $z.IsAutoCreated) {
try {
$records = Get-DnsServerResourceRecord -ZoneName $z.ZoneName -ErrorAction Stop
if ($records.Count -le 500) {
$zoneData.Records = @($records | ForEach-Object {
[ordered]@{
Name = $_.HostName; Type = "$($_.RecordType)"; TTL = "$($_.TimeToLive)"
Data = "$($_.RecordData.IPv4Address)$($_.RecordData.HostNameAlias)$($_.RecordData.NameServer)$($_.RecordData.DescriptiveText)$($_.RecordData.DomainName)"
}
})
Write-Host " $($records.Count) records"
} else {
Write-Host " $($records.Count) records (too many to list, count only)" -ForegroundColor Yellow
$zoneData.RecordCount = $records.Count
}
}
catch {
Write-Host " [FAIL] Records: $($_.Exception.Message)" -ForegroundColor Red
}
}
$audit.DNS.Zones += $zoneData
}
Write-Host " [OK] DNS collected - $($zones.Count) zones" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host " (DNS Server role not installed)" -ForegroundColor Yellow
$audit._errors += @{ Section = "DNS"; Error = $_.Exception.Message }
}
# =====================================================
# 23. HYPER-V VMs
# =====================================================
Write-Host ""
Write-Host "=== 23. HYPER-V VMs ===" -ForegroundColor Cyan
Write-Host " Running: Get-VM"
try {
$vms = Get-VM -ErrorAction Stop
if ($vms) {
$audit.HyperV = @($vms | ForEach-Object {
$vm = $_
Write-Host " VM: $($vm.Name) [State: $($vm.State)]" -ForegroundColor Yellow
Write-Host " CPU: $($vm.ProcessorCount), Memory: $([math]::Round($vm.MemoryAssigned/1MB))MB, Gen: $($vm.Generation)"
$netAdapters = Get-VMNetworkAdapter -VMName $vm.Name -ErrorAction SilentlyContinue
$disks = Get-VMHardDiskDrive -VMName $vm.Name -ErrorAction SilentlyContinue
[ordered]@{
Name = $vm.Name; State = "$($vm.State)"; CPUs = $vm.ProcessorCount
MemoryMB = [math]::Round($vm.MemoryAssigned/1MB); Generation = $vm.Generation
Networks = @($netAdapters | ForEach-Object { [ordered]@{ Switch = $_.SwitchName; MAC = $_.MacAddress; IPs = @($_.IPAddresses) } })
Disks = @($disks | ForEach-Object { $_.Path })
}
})
Write-Host " [OK] Hyper-V VMs collected - $($vms.Count) VMs" -ForegroundColor Green
} else {
Write-Host " No VMs found" -ForegroundColor DarkGray
$audit.HyperV = @()
}
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host " (Hyper-V role not installed)" -ForegroundColor Yellow
$audit._errors += @{ Section = "HyperV"; Error = $_.Exception.Message }
}
# =====================================================
# 24. IIS SITES
# =====================================================
Write-Host ""
Write-Host "=== 24. IIS SITES ===" -ForegroundColor Cyan
Write-Host " Running: Get-Website"
try {
Import-Module WebAdministration -ErrorAction Stop
$sites = Get-Website
if ($sites) {
$audit.IIS = @($sites | ForEach-Object {
Write-Host " Site: $($_.Name) [State: $($_.State)]" -ForegroundColor Yellow
Write-Host " Path: $($_.PhysicalPath)"
Write-Host " Bindings: $(($_.Bindings.Collection | ForEach-Object { $_.bindingInformation }) -join ', ')"
[ordered]@{
Name = $_.Name; State = "$($_.State)"; Path = $_.PhysicalPath
AppPool = $_.applicationPool
Bindings = @($_.Bindings.Collection | ForEach-Object { $_.bindingInformation })
}
})
Write-Host " [OK] IIS sites collected" -ForegroundColor Green
} else {
Write-Host " No websites configured" -ForegroundColor DarkGray
}
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host " (IIS not installed)" -ForegroundColor Yellow
$audit._errors += @{ Section = "IIS"; Error = $_.Exception.Message }
}
# =====================================================
# 25. NPS / RADIUS
# =====================================================
Write-Host ""
Write-Host "=== 25. NPS / RADIUS ===" -ForegroundColor Cyan
Write-Host " Running: Get-NpsRadiusClient, Get-NpsNetworkPolicy"
try {
Import-Module NPS -ErrorAction Stop
$clients = Get-NpsRadiusClient -ErrorAction Stop
$policies = Get-NpsNetworkPolicy -ErrorAction Stop
$audit.NPS = [ordered]@{
Clients = @($clients | ForEach-Object {
Write-Host " RADIUS Client: $($_.Name) ($($_.Address))" -ForegroundColor Yellow
[ordered]@{ Name = $_.Name; Address = $_.Address }
})
Policies = @($policies | ForEach-Object {
Write-Host " Policy: $($_.Name) [Enabled: $($_.Enabled)]" -ForegroundColor Yellow
[ordered]@{ Name = $_.Name; Enabled = $_.Enabled; Order = $_.ProcessingOrder }
})
}
Write-Host " [OK] NPS collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host " (NPS/RADIUS not installed)" -ForegroundColor Yellow
$audit._errors += @{ Section = "NPS"; Error = $_.Exception.Message }
}
# =====================================================
# 26. WINDOWS SERVER BACKUP
# =====================================================
Write-Host ""
Write-Host "=== 26. WINDOWS SERVER BACKUP ===" -ForegroundColor Cyan
Write-Host " Running: Get-WBPolicy, Get-WBSummary"
try {
Add-PSSnapin Windows.ServerBackup -ErrorAction Stop
$policy = Get-WBPolicy -ErrorAction Stop
$wsbData = [ordered]@{
Schedule = @(Get-WBSchedule -Policy $policy)
Volumes = @((Get-WBVolume -Policy $policy).MountPoint)
SystemState = (Get-WBSystemState -Policy $policy)
}
try {
$target = Get-WBBackupTarget -Policy $policy
$wsbData.Target = "$($target.Label) ($($target.TargetPath))"
}
catch { $wsbData.Target = "Unknown" }
try {
$summary = Get-WBSummary
$wsbData.LastSuccess = if ($summary.LastSuccessfulBackupTime) { $summary.LastSuccessfulBackupTime.ToString("yyyy-MM-dd HH:mm") } else { $null }
$wsbData.LastResult = "$($summary.LastBackupResultHR)"
$wsbData.NextRun = if ($summary.NextBackupTime) { $summary.NextBackupTime.ToString("yyyy-MM-dd HH:mm") } else { $null }
}
catch {}
$wsbData.GetEnumerator() | ForEach-Object {
$val = if ($_.Value -is [array]) { $_.Value -join ", " } else { $_.Value }
Write-Host " $($_.Key): $val"
}
$audit.WindowsServerBackup = $wsbData
Write-Host " [OK] WSB info collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host " (Windows Server Backup not installed or no policy configured)" -ForegroundColor Yellow
$audit._errors += @{ Section = "WindowsServerBackup"; Error = $_.Exception.Message }
}
# =====================================================
# 27. PRINTERS
# =====================================================
Write-Host ""
Write-Host "=== 27. PRINTERS ===" -ForegroundColor Cyan
Write-Host " Running: Get-Printer, Get-PrinterPort"
try {
$printers = Get-Printer | Select-Object Name, DriverName, PortName, Shared, ShareName, Published
$printers | Format-Table -AutoSize
$ports = Get-PrinterPort | Where-Object { $_.Name -like 'TCP_*' -or $_.Name -like 'IP_*' } |
Select-Object Name, PrinterHostAddress, PortNumber
if ($ports) {
Write-Host " Printer Ports:" -ForegroundColor DarkGray
$ports | Format-Table -AutoSize
}
$audit.Printers = @($printers | ForEach-Object {
[ordered]@{
Name = $_.Name; Driver = $_.DriverName; Port = $_.PortName
Shared = $_.Shared; ShareName = $_.ShareName
}
})
$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 }
}
# =====================================================
# 28. CERTIFICATES
# =====================================================
Write-Host ""
Write-Host "=== 28. CERTIFICATES ===" -ForegroundColor Cyan
Write-Host " Running: Get-ChildItem Cert:\LocalMachine\My"
try {
$certs = Get-ChildItem Cert:\LocalMachine\My | Select-Object Subject, NotAfter, NotBefore, Issuer, Thumbprint
$certs | Format-Table Subject, NotAfter, Issuer -AutoSize
$expiringSoon = $certs | Where-Object { $_.NotAfter -lt (Get-Date).AddDays(90) }
if ($expiringSoon) {
Write-Host " WARNING: Certificates expiring within 90 days:" -ForegroundColor Yellow
$expiringSoon | ForEach-Object { Write-Host " $($_.Subject) expires $($_.NotAfter)" -ForegroundColor Yellow }
}
$audit.Certificates = @($certs | ForEach-Object {
[ordered]@{
Subject = $_.Subject; Issuer = $_.Issuer; Thumbprint = $_.Thumbprint
NotBefore = $_.NotBefore.ToString("yyyy-MM-dd"); NotAfter = $_.NotAfter.ToString("yyyy-MM-dd")
ExpiringSoon = ($_.NotAfter -lt (Get-Date).AddDays(90))
}
})
Write-Host " [OK] Certificates collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "Certificates"; Error = $_.Exception.Message }
}
# =====================================================
# 29. INSTALLED SOFTWARE (registry-based)
# =====================================================
Write-Host ""
Write-Host "=== 29. 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 }
}
# =====================================================
# 30. SERVICES (non-default running)
# =====================================================
Write-Host ""
Write-Host "=== 30. SERVICES (non-default, running) ===" -ForegroundColor Cyan
Write-Host " Running: Get-Service | Where Running + Automatic"
try {
$services = Get-Service | Where-Object { $_.Status -eq 'Running' -and $_.StartType -eq 'Automatic' } |
Where-Object { $_.DisplayName -notmatch '^(Windows|Microsoft|Net\.|COM\+|DCOM|WMI|Plug|Task|CNG|Base|Crypto|Security Center|DHCP Client|DNS Client|Group Policy|IP Helper|Server$|Workstation$|TCP/IP|Remote Procedure|User Profile|Background|Connected|CoreMessaging|State Repository|Storage|System Events|Time Broker|User Manager|WinHTTP|Diagnostic)' } |
Select-Object Name, DisplayName, StartType |
Sort-Object DisplayName
$services | Format-Table -AutoSize
$audit.Services = @($services | ForEach-Object {
[ordered]@{ Name = $_.Name; DisplayName = $_.DisplayName; StartType = "$($_.StartType)" }
})
Write-Host " [OK] Services collected - $($services.Count) non-default" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "Services"; Error = $_.Exception.Message }
}
# =====================================================
# 31. SCHEDULED TASKS (non-Microsoft)
# =====================================================
Write-Host ""
Write-Host "=== 31. SCHEDULED TASKS (non-Microsoft) ===" -ForegroundColor Cyan
Write-Host " Running: Get-ScheduledTask"
try {
$tasks = Get-ScheduledTask |
Where-Object { $_.Author -notlike 'Microsoft*' -and $_.State -ne 'Disabled' } |
Select-Object TaskName, State, Author, TaskPath
$tasks | Format-Table -AutoSize
$audit.ScheduledTasks = @($tasks | ForEach-Object {
[ordered]@{ Name = $_.TaskName; State = "$($_.State)"; Author = $_.Author; Path = $_.TaskPath }
})
Write-Host " [OK] Scheduled tasks collected - $($tasks.Count) non-Microsoft" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ScheduledTasks"; Error = $_.Exception.Message }
}
# =====================================================
# 32. WINDOWS UPDATES
# =====================================================
Write-Host ""
Write-Host "=== 32. WINDOWS UPDATES (last 20) ===" -ForegroundColor Cyan
Write-Host " Running: Get-HotFix"
try {
$updates = Get-HotFix | Sort-Object InstalledOn -Descending -ErrorAction SilentlyContinue | Select-Object -First 20 |
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 }
InstalledBy = $_.InstalledBy
}
})
Write-Host " [OK] Updates collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "WindowsUpdates"; Error = $_.Exception.Message }
}
# =====================================================
# 33. TIME CONFIG
# =====================================================
Write-Host ""
Write-Host "=== 33. TIME CONFIG ===" -ForegroundColor Cyan
Write-Host " Running: w32tm /query /configuration"
try {
$timeConfig = w32tm /query /configuration 2>&1
$timeConfig | ForEach-Object { Write-Host " $_" }
Write-Host " [OK] Time config collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "TimeConfig"; Error = $_.Exception.Message }
}
# =====================================================
# 34. EVENT LOG ERRORS (last 14 days)
# =====================================================
Write-Host ""
Write-Host "=== 34. EVENT LOG ERRORS (Critical/Error, last 14 days) ===" -ForegroundColor Cyan
Write-Host " Running: Get-WinEvent -FilterHashtable System log"
try {
$since = (Get-Date).AddDays(-14)
$events = Get-WinEvent -FilterHashtable @{LogName='System'; Level=1,2; StartTime=$since} -MaxEvents 30 -ErrorAction Stop |
Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, @{N='Message';E={$_.Message.Substring(0, [Math]::Min(200, $_.Message.Length))}}
$events | Format-Table TimeCreated, Id, ProviderName -AutoSize
$audit.EventLogErrors = @($events | ForEach-Object {
[ordered]@{
Time = $_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss")
EventId = $_.Id; Level = $_.LevelDisplayName; Source = $_.ProviderName
Message = $_.Message
}
})
Write-Host " [OK] Events collected - $($events.Count) critical/error events" -ForegroundColor Green
}
catch {
Write-Host " No critical/error events in System log (last 14 days)" -ForegroundColor Green
$audit.EventLogErrors = @()
}
# =====================================================
# 35. EVENT LOG WARNINGS (last 14 days)
# =====================================================
Write-Host ""
Write-Host "=== 35. 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 = @()
}
# =====================================================
# 36. EVENT LOG SETTINGS
# =====================================================
Write-Host ""
Write-Host "=== 36. 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, Current=$($logInfo.CurrentSizeKB)KB, Mode=$($log.LogMode), Records=$($log.RecordCount)"
$audit.EventLogSettings += $logInfo
}
catch { Write-Host " $logName`: not available" -ForegroundColor DarkGray }
}
# HIPAA requires adequate log retention
$securityLog = $audit.EventLogSettings | Where-Object { $_.LogName -eq 'Security' }
if ($securityLog -and $securityLog.MaxSizeKB -lt 131072) {
Write-Host " [WARN] Security log max size is $($securityLog.MaxSizeKB)KB - recommend at least 128MB for compliance" -ForegroundColor Yellow
}
if ($securityLog -and $securityLog.LogMode -eq 'Circular') {
Write-Host " [WARN] Security log is in Circular mode - old events will be overwritten. Consider archiving for HIPAA" -ForegroundColor Yellow
}
Write-Host " [OK] Event log settings collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "EventLogSettings"; Error = $_.Exception.Message }
}
# =====================================================
# 37. WINDOWS FIREWALL PROFILES
# =====================================================
Write-Host ""
Write-Host "=== 37. 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)"
LogAllowed = $_.LogAllowed; LogBlocked = $_.LogBlocked
LogFileName = $_.LogFileName; LogMaxSizeKilobytes = $_.LogMaxSizeKilobytes
}
Write-Host " $($_.Name): Enabled=$($_.Enabled), Inbound=$($_.DefaultInboundAction), Outbound=$($_.DefaultOutboundAction)"
if (-not $_.Enabled) {
Write-Host " [WARN] $($_.Name) firewall profile is DISABLED" -ForegroundColor Red
}
if (-not $_.LogBlocked) {
Write-Host " [WARN] $($_.Name) blocked connections are NOT being logged" -ForegroundColor Yellow
}
$p
})
Write-Host " [OK] Firewall profiles collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "FirewallProfiles"; Error = $_.Exception.Message }
}
# =====================================================
# 38. RDP SECURITY SETTINGS
# =====================================================
Write-Host ""
Write-Host "=== 38. 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
$secLayer = (Get-ItemProperty $rdpReg -ErrorAction SilentlyContinue).SecurityLayer
$minEncrypt = (Get-ItemProperty $rdpReg -ErrorAction SilentlyContinue).MinEncryptionLevel
$rdpSettings = [ordered]@{
RDPEnabled = ($rdpEnabled -eq 0)
NLARequired = ($nla -eq 1)
SecurityLayer = switch ($secLayer) { 0 {"RDP Security"} 1 {"Negotiate"} 2 {"SSL/TLS"} default {"Unknown ($secLayer)"} }
MinEncryptionLevel = switch ($minEncrypt) { 1 {"Low"} 2 {"Client Compatible"} 3 {"High"} 4 {"FIPS"} default {"Unknown ($minEncrypt)"} }
}
$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 - credential theft risk" -ForegroundColor Red
}
if ($rdpSettings.RDPEnabled -and $secLayer -lt 2) {
Write-Host " [WARN] RDP security layer is not SSL/TLS - downgrade attack risk" -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 }
}
# =====================================================
# 39. TLS/SSL CONFIGURATION
# =====================================================
Write-Host ""
Write-Host "=== 39. 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) {
$serverPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$ver\Server"
$enabled = $null
if (Test-Path $serverPath) {
$enabled = (Get-ItemProperty $serverPath -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
if ($ver -in @('SSL 2.0', 'SSL 3.0', 'TLS 1.0', 'TLS 1.1') -and $status -ne 'Disabled') {
Write-Host " [WARN] $ver should be explicitly DISABLED for compliance" -ForegroundColor Yellow
}
}
Write-Host " [OK] TLS config collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "TLSConfig"; Error = $_.Exception.Message }
}
# =====================================================
# 40. CREDENTIAL GUARD / DEVICE GUARD
# =====================================================
Write-Host ""
Write-Host "=== 40. CREDENTIAL GUARD / DEVICE GUARD ===" -ForegroundColor Cyan
Write-Host " Running: Get-CimInstance Win32_DeviceGuard"
try {
$dg = Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard -ErrorAction Stop
$cgStatus = switch ($dg.SecurityServicesRunning) {
{ 1 -in $_ } { "Running" }
default { "Not running" }
}
$vbsStatus = switch ($dg.VirtualizationBasedSecurityStatus) {
0 { "Not enabled" } 1 { "Enabled but not running" } 2 { "Running" } default { "Unknown" }
}
$guardInfo = [ordered]@{
VBSStatus = $vbsStatus
CredentialGuard = $cgStatus
SecurityServicesConfigured = @($dg.SecurityServicesConfigured)
SecurityServicesRunning = @($dg.SecurityServicesRunning)
}
$guardInfo.GetEnumerator() | ForEach-Object {
$val = if ($_.Value -is [array]) { $_.Value -join ", " } else { $_.Value }
Write-Host " $($_.Key): $val"
}
if ($cgStatus -ne "Running") {
Write-Host " [WARN] Credential Guard is not running - pass-the-hash risk" -ForegroundColor Yellow
}
$audit.CredentialGuard = $guardInfo
Write-Host " [OK] Credential/Device Guard checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "CredentialGuard"; Error = $_.Exception.Message }
}
# =====================================================
# 41. UAC SETTINGS
# =====================================================
Write-Host ""
Write-Host "=== 41. 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"}
}
PromptOnSecureDesktop = $uacReg.PromptOnSecureDesktop
FilterAdministratorToken = $uacReg.FilterAdministratorToken
}
$uacSettings.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
if ($uacReg.EnableLUA -ne 1) {
Write-Host " [WARN] UAC is DISABLED - all processes run with full admin rights" -ForegroundColor Red
}
if ($uacReg.ConsentPromptBehaviorAdmin -eq 0) {
Write-Host " [WARN] Admin elevation without prompting - malware risk" -ForegroundColor Yellow
}
$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 }
}
# =====================================================
# 42. SCREEN LOCK / INACTIVITY TIMEOUT
# =====================================================
Write-Host ""
Write-Host "=== 42. SCREEN LOCK / INACTIVITY TIMEOUT ===" -ForegroundColor Cyan
Write-Host " Running: Registry check for screen lock policy"
try {
$screenLock = [ordered]@{}
# Machine inactivity limit
$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 ($([math]::Round($inactivity/60)) minutes)"
} else {
Write-Host " Machine inactivity timeout: Not configured"
Write-Host " [WARN] No machine inactivity timeout - HIPAA requires automatic session lock" -ForegroundColor Yellow
}
# Screensaver settings (applied via GPO)
$ssActive = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -ErrorAction SilentlyContinue).ScreenSaveActive
$ssTimeout = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -ErrorAction SilentlyContinue).ScreenSaveTimeOut
$ssSecure = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -ErrorAction SilentlyContinue).ScreenSaverIsSecure
$screenLock.ScreenSaverActive = $ssActive
$screenLock.ScreenSaverTimeout = $ssTimeout
$screenLock.ScreenSaverSecure = $ssSecure
Write-Host " Screensaver active: $ssActive, Timeout: $ssTimeout sec, Require password: $ssSecure"
$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 }
}
# =====================================================
# 43. USB STORAGE POLICY
# =====================================================
Write-Host ""
Write-Host "=== 43. 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 (normal)" } 4 { "Disabled" } default { "Unknown ($usbStorage)" }
}
Write-Host " USB Storage: $usbStatus"
# Check for GPO-based USB restrictions
$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 / HIPAA concern" -ForegroundColor Yellow
}
$audit.USBStoragePolicy = [ordered]@{ USBSTORStart = $usbStorage; 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 }
}
# =====================================================
# 44. FAILED LOGON ATTEMPTS (last 7 days)
# =====================================================
Write-Host ""
Write-Host "=== 44. 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 100 -ErrorAction SilentlyContinue)
if ($failedLogons.Count -gt 0) {
# Group by target account
$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 | Select-Object Count, Name
Write-Host " $($failedLogons.Count) failed logon attempts in last 7 days:" -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 } })
Recent = @($grouped | Select-Object -First 20 | ForEach-Object {
[ordered]@{ Time = $_.Time.ToString("yyyy-MM-dd HH:mm:ss"); User = $_.User; SourceIP = $_.SourceIP }
})
}
} else {
Write-Host " No failed logon attempts" -ForegroundColor Green
$audit.FailedLogons = [ordered]@{ TotalCount = 0 }
}
Write-Host " [OK] Failed logons collected" -ForegroundColor Green
# =====================================================
# 45. SECURITY QUICK CHECKS
# =====================================================
Write-Host ""
Write-Host "=== 45. SECURITY QUICK CHECKS ===" -ForegroundColor Cyan
$audit.SecurityChecks = [ordered]@{}
# --- Password Policy ---
Write-Host " Running: Get-ADDefaultDomainPasswordPolicy"
try {
$pp = Get-ADDefaultDomainPasswordPolicy -ErrorAction Stop
$passPolicy = [ordered]@{
MinLength = $pp.MinPasswordLength
ComplexityEnabled = $pp.ComplexityEnabled
MaxAge = "$($pp.MaxPasswordAge)"
MinAge = "$($pp.MinPasswordAge)"
HistoryCount = $pp.PasswordHistoryCount
LockoutThreshold = $pp.LockoutThreshold
LockoutDuration = "$($pp.LockoutDuration)"
LockoutWindow = "$($pp.LockoutObservationWindow)"
ReversibleEncryption = $pp.ReversibleEncryptionEnabled
}
$passPolicy.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
# Flag weak settings
$passIssues = @()
if ($pp.MinPasswordLength -lt 12) { $passIssues += "Min password length is $($pp.MinPasswordLength) - recommend 12+" }
if (-not $pp.ComplexityEnabled) { $passIssues += "Complexity is DISABLED" }
if ($pp.LockoutThreshold -eq 0) { $passIssues += "Account lockout is DISABLED - brute force risk" }
if ($pp.ReversibleEncryptionEnabled) { $passIssues += "Reversible encryption is ENABLED - critical risk" }
if ($pp.PasswordHistoryCount -lt 12) { $passIssues += "Password history is only $($pp.PasswordHistoryCount) - recommend 12+" }
$passPolicy.Issues = $passIssues
if ($passIssues) {
$passIssues | ForEach-Object { Write-Host " [WARN] $_" -ForegroundColor Yellow }
}
$audit.SecurityChecks.PasswordPolicy = $passPolicy
Write-Host " [OK] Password policy checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "PasswordPolicy"; Error = $_.Exception.Message }
}
# --- Fine-Grained Password Policies ---
Write-Host " Running: Get-ADFineGrainedPasswordPolicy"
try {
$fgpp = Get-ADFineGrainedPasswordPolicy -Filter * -ErrorAction Stop
if ($fgpp) {
$audit.SecurityChecks.FineGrainedPolicies = @($fgpp | ForEach-Object {
Write-Host " FGPP: $($_.Name) - MinLen=$($_.MinPasswordLength), MaxAge=$($_.MaxPasswordAge), AppliesTo=$($_.AppliesTo -join ', ')"
[ordered]@{
Name = $_.Name; Precedence = $_.Precedence; MinLength = $_.MinPasswordLength
MaxAge = "$($_.MaxPasswordAge)"; LockoutThreshold = $_.LockoutThreshold
AppliesTo = @($_.AppliesTo)
}
})
Write-Host " [OK] Fine-grained policies checked" -ForegroundColor Green
} else {
Write-Host " No fine-grained password policies configured"
$audit.SecurityChecks.FineGrainedPolicies = @()
}
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "FineGrainedPolicies"; Error = $_.Exception.Message }
}
# --- LAPS Status ---
Write-Host " Running: LAPS check"
try {
$lapsCheck = [ordered]@{ LegacyLAPS = $false; WindowsLAPS = $false }
# Check for Legacy LAPS schema extension
$schemaNC = (Get-ADRootDSE).schemaNamingContext
$lapsSchema = Get-ADObject -SearchBase $schemaNC -Filter "Name -eq 'ms-Mcs-AdmPwd'" -ErrorAction SilentlyContinue
if ($lapsSchema) {
$lapsCheck.LegacyLAPS = $true
Write-Host " Legacy LAPS (ms-Mcs-AdmPwd): INSTALLED" -ForegroundColor Green
# Count computers with LAPS passwords
$lapsComputers = (Get-ADComputer -Filter * -Properties ms-Mcs-AdmPwd | Where-Object { $_.'ms-Mcs-AdmPwd' }).Count
$totalComputers = (Get-ADComputer -Filter *).Count
$lapsCheck.LegacyLAPSCoverage = "$lapsComputers / $totalComputers computers"
Write-Host " Coverage: $lapsComputers / $totalComputers computers have LAPS passwords"
} else {
Write-Host " Legacy LAPS: NOT INSTALLED" -ForegroundColor Yellow
}
# Check for Windows LAPS schema
$winLapsSchema = Get-ADObject -SearchBase $schemaNC -Filter "Name -eq 'ms-LAPS-Password'" -ErrorAction SilentlyContinue
if ($winLapsSchema) {
$lapsCheck.WindowsLAPS = $true
Write-Host " Windows LAPS (ms-LAPS-Password): INSTALLED" -ForegroundColor Green
} else {
Write-Host " Windows LAPS: NOT INSTALLED" -ForegroundColor Yellow
}
if (-not $lapsCheck.LegacyLAPS -and -not $lapsCheck.WindowsLAPS) {
Write-Host " [WARN] No LAPS deployment detected - local admin passwords are likely shared/static" -ForegroundColor Red
}
$audit.SecurityChecks.LAPS = $lapsCheck
Write-Host " [OK] LAPS checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "LAPS"; Error = $_.Exception.Message }
}
# --- SMBv1 + SMB Signing ---
Write-Host " Running: SMB configuration check"
try {
$smbConfig = Get-SmbServerConfiguration -ErrorAction Stop
# SMBv1
if ($smbConfig.EnableSMB1Protocol) {
Write-Host " [WARN] SMBv1 is ENABLED - known vulnerability, should be disabled" -ForegroundColor Red
} else {
Write-Host " SMBv1: Disabled" -ForegroundColor Green
}
$audit.SecurityChecks.SMBv1Enabled = $smbConfig.EnableSMB1Protocol
# SMB Signing
$smbSigning = [ordered]@{
RequireSecuritySignature = $smbConfig.RequireSecuritySignature
EnableSecuritySignature = $smbConfig.EnableSecuritySignature
EncryptData = $smbConfig.EncryptData
}
$smbSigning.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
if (-not $smbConfig.RequireSecuritySignature) {
Write-Host " [WARN] SMB signing is NOT required - relay attack risk" -ForegroundColor Yellow
}
$audit.SecurityChecks.SMBSigning = $smbSigning
Write-Host " [OK] SMB config checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "SMBConfig"; Error = $_.Exception.Message }
}
# --- Machine Account Quota ---
Write-Host " Running: Machine Account Quota check"
try {
$maq = (Get-ADObject -Identity $script:domain.DistinguishedName -Properties ms-DS-MachineAccountQuota).'ms-DS-MachineAccountQuota'
Write-Host " Machine Account Quota: $maq"
if ($maq -gt 0) {
Write-Host " [WARN] Any domain user can join up to $maq computers to the domain" -ForegroundColor Yellow
}
$audit.SecurityChecks.MachineAccountQuota = $maq
Write-Host " [OK] Machine account quota checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "MachineAccountQuota"; Error = $_.Exception.Message }
}
# --- AD Recycle Bin ---
Write-Host " Running: AD Recycle Bin check"
try {
$recycleBin = Get-ADOptionalFeature -Filter "Name -eq 'Recycle Bin Feature'" -ErrorAction Stop
if ($recycleBin.EnabledScopes.Count -gt 0) {
Write-Host " AD Recycle Bin: ENABLED" -ForegroundColor Green
$audit.SecurityChecks.RecycleBinEnabled = $true
} else {
Write-Host " [WARN] AD Recycle Bin is NOT enabled - accidental deletions are unrecoverable" -ForegroundColor Yellow
$audit.SecurityChecks.RecycleBinEnabled = $false
}
Write-Host " [OK] Recycle Bin checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "RecycleBin"; Error = $_.Exception.Message }
}
# --- Accounts with Password Never Expires ---
Write-Host " Running: Accounts with PasswordNeverExpires"
try {
$neverExpire = Get-ADUser -Filter { PasswordNeverExpires -eq $true -and Enabled -eq $true } -Properties PasswordNeverExpires, PasswordLastSet |
Select-Object Name, SamAccountName, PasswordLastSet
if ($neverExpire) {
Write-Host " [WARN] $($neverExpire.Count) enabled accounts have PasswordNeverExpires:" -ForegroundColor Yellow
$neverExpire | ForEach-Object { Write-Host " $($_.SamAccountName) - Password set: $($_.PasswordLastSet)" -ForegroundColor Yellow }
} else {
Write-Host " No enabled accounts with PasswordNeverExpires" -ForegroundColor Green
}
$audit.SecurityChecks.PasswordNeverExpires = @($neverExpire | ForEach-Object {
[ordered]@{ Name = $_.Name; SAM = $_.SamAccountName; PasswordLastSet = if ($_.PasswordLastSet) { $_.PasswordLastSet.ToString("yyyy-MM-dd") } else { $null } }
})
Write-Host " [OK] PasswordNeverExpires checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "PasswordNeverExpires"; Error = $_.Exception.Message }
}
# --- Stale Passwords (not changed in 90+ days) ---
Write-Host " Running: Stale passwords check - enabled accounts, 90+ days"
try {
$staleDate = (Get-Date).AddDays(-90)
$stalePasswords = Get-ADUser -Filter { Enabled -eq $true -and PasswordLastSet -lt $staleDate } -Properties PasswordLastSet |
Select-Object Name, SamAccountName, PasswordLastSet | Sort-Object PasswordLastSet
if ($stalePasswords) {
Write-Host " [WARN] $($stalePasswords.Count) enabled accounts have passwords older than 90 days:" -ForegroundColor Yellow
$stalePasswords | Select-Object -First 20 | ForEach-Object {
Write-Host " $($_.SamAccountName) - Last set: $($_.PasswordLastSet)" -ForegroundColor Yellow
}
if ($stalePasswords.Count -gt 20) { Write-Host " ... and $($stalePasswords.Count - 20) more" -ForegroundColor Yellow }
} else {
Write-Host " All enabled accounts have passwords changed within 90 days" -ForegroundColor Green
}
$audit.SecurityChecks.StalePasswords = @($stalePasswords | ForEach-Object {
[ordered]@{ Name = $_.Name; SAM = $_.SamAccountName; PasswordLastSet = if ($_.PasswordLastSet) { $_.PasswordLastSet.ToString("yyyy-MM-dd") } else { $null } }
})
Write-Host " [OK] Stale passwords checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "StalePasswords"; Error = $_.Exception.Message }
}
# --- Inactive Accounts (not logged in 90+ days) ---
Write-Host " Running: Inactive accounts check - enabled, no logon 90+ days"
try {
$inactiveDate = (Get-Date).AddDays(-90)
$inactive = Get-ADUser -Filter { Enabled -eq $true -and LastLogonDate -lt $inactiveDate } -Properties LastLogonDate |
Select-Object Name, SamAccountName, LastLogonDate | Sort-Object LastLogonDate
if ($inactive) {
Write-Host " [WARN] $($inactive.Count) enabled accounts have not logged in for 90+ days:" -ForegroundColor Yellow
$inactive | Select-Object -First 20 | ForEach-Object {
Write-Host " $($_.SamAccountName) - Last logon: $($_.LastLogonDate)" -ForegroundColor Yellow
}
if ($inactive.Count -gt 20) { Write-Host " ... and $($inactive.Count - 20) more" -ForegroundColor Yellow }
} else {
Write-Host " All enabled accounts have logged in within 90 days" -ForegroundColor Green
}
$audit.SecurityChecks.InactiveAccounts = @($inactive | ForEach-Object {
[ordered]@{ Name = $_.Name; SAM = $_.SamAccountName; LastLogon = if ($_.LastLogonDate) { $_.LastLogonDate.ToString("yyyy-MM-dd") } else { $null } }
})
Write-Host " [OK] Inactive accounts checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "InactiveAccounts"; Error = $_.Exception.Message }
}
# --- Locked Accounts ---
Write-Host " Running: Locked accounts check"
try {
$locked = Search-ADAccount -LockedOut | Where-Object { $_.Enabled } | Select-Object Name, SamAccountName, LastLogonDate
if ($locked) {
Write-Host " [WARN] $($locked.Count) accounts are currently LOCKED OUT:" -ForegroundColor Yellow
$locked | ForEach-Object { Write-Host " $($_.SamAccountName)" -ForegroundColor Yellow }
} else {
Write-Host " No locked accounts" -ForegroundColor Green
}
$audit.SecurityChecks.LockedAccounts = @($locked | ForEach-Object {
[ordered]@{ Name = $_.Name; SAM = $_.SamAccountName }
})
Write-Host " [OK] Locked accounts checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "LockedAccounts"; Error = $_.Exception.Message }
}
# --- AdminSDHolder / Protected Users ---
Write-Host " Running: Protected Users group check"
try {
$protectedMembers = Get-ADGroupMember "Protected Users" -ErrorAction Stop | Select-Object Name, SamAccountName
if ($protectedMembers) {
Write-Host " Protected Users members:" -ForegroundColor Green
$protectedMembers | ForEach-Object { Write-Host " $($_.SamAccountName)" }
} else {
Write-Host " [WARN] Protected Users group is EMPTY - admin accounts should be added" -ForegroundColor Yellow
}
$audit.SecurityChecks.ProtectedUsers = @($protectedMembers | ForEach-Object {
[ordered]@{ Name = $_.Name; SAM = $_.SamAccountName }
})
Write-Host " [OK] Protected Users checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ProtectedUsers"; Error = $_.Exception.Message }
}
# --- AdminSDHolder protected accounts ---
Write-Host " Running: AdminSDHolder check"
try {
$adminSDHolder = Get-ADUser -Filter { AdminCount -eq 1 -and Enabled -eq $true } -Properties AdminCount, MemberOf |
Select-Object Name, SamAccountName
if ($adminSDHolder) {
Write-Host " Accounts with AdminCount=1 - $($adminSDHolder.Count) accounts:" -ForegroundColor Yellow
$adminSDHolder | ForEach-Object { Write-Host " $($_.SamAccountName)" }
}
$audit.SecurityChecks.AdminSDHolder = @($adminSDHolder | ForEach-Object {
[ordered]@{ Name = $_.Name; SAM = $_.SamAccountName }
})
Write-Host " [OK] AdminSDHolder checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "AdminSDHolder"; Error = $_.Exception.Message }
}
# --- Kerberoastable Accounts (SPNs on user accounts) ---
Write-Host " Running: Kerberoastable accounts check"
try {
$spnUsers = Get-ADUser -Filter { ServicePrincipalName -like "*" -and Enabled -eq $true } -Properties ServicePrincipalName, PasswordLastSet |
Where-Object { $_.SamAccountName -ne 'krbtgt' } |
Select-Object Name, SamAccountName, PasswordLastSet, ServicePrincipalName
if ($spnUsers) {
Write-Host " [WARN] $($spnUsers.Count) enabled user accounts have SPNs - kerberoastable:" -ForegroundColor Yellow
$spnUsers | ForEach-Object {
Write-Host " $($_.SamAccountName) - Password set: $($_.PasswordLastSet) - SPN: $($_.ServicePrincipalName[0])" -ForegroundColor Yellow
}
} else {
Write-Host " No kerberoastable user accounts found" -ForegroundColor Green
}
$audit.SecurityChecks.Kerberoastable = @($spnUsers | ForEach-Object {
[ordered]@{
Name = $_.Name; SAM = $_.SamAccountName
PasswordLastSet = if ($_.PasswordLastSet) { $_.PasswordLastSet.ToString("yyyy-MM-dd") } else { $null }
SPNs = @($_.ServicePrincipalName)
}
})
Write-Host " [OK] Kerberoastable checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "Kerberoastable"; Error = $_.Exception.Message }
}
# --- ASREPRoastable Accounts (PreAuth disabled) ---
Write-Host " Running: ASREPRoastable accounts check"
try {
$asrep = Get-ADUser -Filter { DoesNotRequirePreAuth -eq $true -and Enabled -eq $true } -Properties DoesNotRequirePreAuth |
Select-Object Name, SamAccountName
if ($asrep) {
Write-Host " [WARN] $($asrep.Count) accounts do NOT require Kerberos pre-auth - ASREPRoastable:" -ForegroundColor Red
$asrep | ForEach-Object { Write-Host " $($_.SamAccountName)" -ForegroundColor Red }
} else {
Write-Host " No ASREPRoastable accounts found" -ForegroundColor Green
}
$audit.SecurityChecks.ASREPRoastable = @($asrep | ForEach-Object {
[ordered]@{ Name = $_.Name; SAM = $_.SamAccountName }
})
Write-Host " [OK] ASREPRoastable checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "ASREPRoastable"; Error = $_.Exception.Message }
}
# --- NULL Sessions ---
Write-Host " Running: NULL session check"
try {
$lsaReg = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -ErrorAction Stop
$restrictAnon = $lsaReg.RestrictAnonymous
$restrictAnonSAM = $lsaReg.RestrictAnonymousSAM
$everyoneAnon = $lsaReg.EveryoneIncludesAnonymous
$nullSession = [ordered]@{
RestrictAnonymous = $restrictAnon
RestrictAnonymousSAM = $restrictAnonSAM
EveryoneIncludesAnonymous = $everyoneAnon
}
$nullSession.GetEnumerator() | ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }
if ($everyoneAnon -eq 1) {
Write-Host " [WARN] EveryoneIncludesAnonymous is ENABLED - anonymous users have Everyone access" -ForegroundColor Red
}
if ($restrictAnon -eq 0) {
Write-Host " [WARN] RestrictAnonymous is 0 - anonymous enumeration of shares/accounts possible" -ForegroundColor Yellow
}
$audit.SecurityChecks.NullSessions = $nullSession
Write-Host " [OK] NULL sessions checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "NullSessions"; Error = $_.Exception.Message }
}
# --- Insecure DNS Zones ---
Write-Host " Running: Insecure DNS zones check"
try {
Import-Module DnsServer -ErrorAction Stop
$insecureZones = Get-DnsServerZone | Where-Object { $_.DynamicUpdate -eq 'NonsecureAndSecure' -and -not $_.IsAutoCreated }
if ($insecureZones) {
Write-Host " [WARN] DNS zones allowing nonsecure dynamic updates:" -ForegroundColor Yellow
$insecureZones | ForEach-Object { Write-Host " $($_.ZoneName)" -ForegroundColor Yellow }
} else {
Write-Host " All DNS zones use secure-only dynamic updates" -ForegroundColor Green
}
$audit.SecurityChecks.InsecureDNSZones = @($insecureZones | ForEach-Object { $_.ZoneName })
Write-Host " [OK] DNS zone security checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "InsecureDNSZones"; Error = $_.Exception.Message }
}
# --- SYSVOL Password Search (GPP cpassword) ---
Write-Host " Running: SYSVOL GPP password search"
try {
$dnsDomain = if ($env:USERDNSDOMAIN) { $env:USERDNSDOMAIN } elseif ($script:domain) { $script:domain.DNSRoot } else { (Get-ADDomain).DNSRoot }
$sysvolPath = "\\$dnsDomain\SYSVOL\$dnsDomain"
$gppFiles = Get-ChildItem -Path $sysvolPath -Recurse -Include "*.xml" -ErrorAction Stop
$cpasswordFiles = @()
foreach ($file in $gppFiles) {
$content = Get-Content $file.FullName -ErrorAction SilentlyContinue
if ($content -match 'cpassword') {
$cpasswordFiles += $file.FullName
Write-Host " [CRITICAL] GPP password found in: $($file.FullName)" -ForegroundColor Red
}
}
if ($cpasswordFiles.Count -eq 0) {
Write-Host " No GPP passwords found in SYSVOL" -ForegroundColor Green
}
$audit.SecurityChecks.GPPPasswords = $cpasswordFiles
Write-Host " [OK] SYSVOL password search complete" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "GPPPasswords"; Error = $_.Exception.Message }
}
# --- Old/EOL Operating Systems ---
Write-Host " Running: End-of-life OS check"
try {
$eolPatterns = @('Windows XP*', 'Windows Vista*', 'Windows 7*', 'Windows 8 *', 'Windows 8.1*',
'Windows Server 2003*', 'Windows Server 2008*', 'Windows Server 2012 *', 'Windows Server 2012 R2*')
$eolComputers = @()
foreach ($pattern in $eolPatterns) {
$found = Get-ADComputer -Filter "OperatingSystem -like '$pattern' -and Enabled -eq 'True'" -Properties OperatingSystem, LastLogonDate -ErrorAction SilentlyContinue
if ($found) { $eolComputers += $found }
}
if ($eolComputers) {
Write-Host " [WARN] $($eolComputers.Count) enabled computers running END OF LIFE operating systems:" -ForegroundColor Red
$eolComputers | ForEach-Object { Write-Host " $($_.Name) - $($_.OperatingSystem) - Last logon: $($_.LastLogonDate)" -ForegroundColor Red }
} else {
Write-Host " No end-of-life operating systems detected" -ForegroundColor Green
}
$audit.SecurityChecks.EOLComputers = @($eolComputers | ForEach-Object {
[ordered]@{ Name = $_.Name; OS = $_.OperatingSystem; LastLogon = if ($_.LastLogonDate) { $_.LastLogonDate.ToString("yyyy-MM-dd") } else { $null } }
})
Write-Host " [OK] EOL OS check complete" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "EOLComputers"; Error = $_.Exception.Message }
}
# --- Functional Level ---
Write-Host " Running: Functional level assessment"
try {
$domainFL = if ($script:domain) { $script:domain.DomainMode } else { (Get-ADDomain).DomainMode }
$forestFL = if ($script:forest) { $script:forest.ForestMode } else { (Get-ADForest).ForestMode }
Write-Host " Domain Functional Level: $domainFL"
Write-Host " Forest Functional Level: $forestFL"
$outdatedLevels = @('Windows2003Domain', 'Windows2008Domain', 'Windows2008R2Domain', 'Windows2012Domain', 'Windows2012R2Domain',
'Windows2003Forest', 'Windows2008Forest', 'Windows2008R2Forest', 'Windows2012Forest', 'Windows2012R2Forest')
if ($domainFL -in $outdatedLevels -or $forestFL -in $outdatedLevels) {
Write-Host " [WARN] Functional level is outdated - limits security features like Protected Users, auth silos" -ForegroundColor Yellow
}
$audit.SecurityChecks.FunctionalLevel = [ordered]@{ Domain = "$domainFL"; Forest = "$forestFL" }
Write-Host " [OK] Functional level checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "FunctionalLevel"; Error = $_.Exception.Message }
}
# --- Replication Type (FRS vs DFSR) ---
Write-Host " Running: SYSVOL replication type check"
try {
$dn = if ($script:domain) { $script:domain.DistinguishedName } else { (Get-ADDomain).DistinguishedName }
$replType = (Get-ADObject "CN=DFSR-GlobalSettings,CN=System,$dn" -ErrorAction Stop)
Write-Host " SYSVOL Replication: DFSR" -ForegroundColor Green
$audit.SecurityChecks.SYSVOLReplication = "DFSR"
}
catch {
Write-Host " SYSVOL Replication: FRS (legacy - should migrate to DFSR)" -ForegroundColor Yellow
$audit.SecurityChecks.SYSVOLReplication = "FRS"
}
Write-Host " [OK] Replication type checked" -ForegroundColor Green
# --- LDAP Signing + Channel Binding ---
Write-Host " Running: LDAP signing and channel binding check"
try {
$ntdsReg = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' -ErrorAction Stop
$ldapSigning = $ntdsReg.LDAPServerIntegrity
$ldapSigningText = switch ($ldapSigning) {
0 { "None (not required)" } 1 { "Require signing" } 2 { "Require signing" } default { "Unknown ($ldapSigning)" }
}
Write-Host " LDAP Server Signing: $ldapSigningText"
if ($ldapSigning -eq 0 -or $null -eq $ldapSigning) {
Write-Host " [WARN] LDAP signing is NOT required - MITM risk" -ForegroundColor Yellow
}
$audit.SecurityChecks.LDAPSigning = $ldapSigningText
$ldapCB = $ntdsReg.LdapEnforceChannelBinding
$ldapCBText = switch ($ldapCB) {
0 { "Never" } 1 { "When supported" } 2 { "Always" } default { "Not configured (0)" }
}
Write-Host " LDAP Channel Binding: $ldapCBText"
if ($ldapCB -ne 2) {
Write-Host " [WARN] LDAP channel binding is not set to Always - credential relay risk" -ForegroundColor Yellow
}
$audit.SecurityChecks.LDAPChannelBinding = $ldapCBText
Write-Host " [OK] LDAP settings checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "LDAPSettings"; Error = $_.Exception.Message }
}
# --- DCs Not Owned by Domain Admins ---
Write-Host " Running: DC ownership check"
try {
$dcsNotOwned = Get-ADComputer -Filter { PrimaryGroupID -eq 516 } -Properties nTSecurityDescriptor -ErrorAction Stop | ForEach-Object {
$owner = $_.nTSecurityDescriptor.Owner
if ($owner -notmatch 'Domain Admins') {
[PSCustomObject]@{ Name = $_.Name; Owner = $owner }
}
}
if ($dcsNotOwned) {
Write-Host " [WARN] DCs not owned by Domain Admins:" -ForegroundColor Yellow
$dcsNotOwned | ForEach-Object { Write-Host " $($_.Name) owned by $($_.Owner)" -ForegroundColor Yellow }
} else {
Write-Host " All DCs owned by Domain Admins" -ForegroundColor Green
}
$audit.SecurityChecks.DCsNotOwnedByDA = @($dcsNotOwned | ForEach-Object {
[ordered]@{ Name = $_.Name; Owner = $_.Owner }
})
Write-Host " [OK] DC ownership checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "DCOwnership"; Error = $_.Exception.Message }
}
# --- Recent Changes (last 30 days) ---
Write-Host " Running: Recent AD changes - last 30 days"
try {
$recentDate = (Get-Date).AddDays(-30)
$newUsers = Get-ADUser -Filter { WhenCreated -gt $recentDate } -Properties WhenCreated |
Select-Object Name, SamAccountName, WhenCreated
$newGroups = Get-ADGroup -Filter { WhenCreated -gt $recentDate } -Properties WhenCreated |
Select-Object Name, WhenCreated
$newComputers = Get-ADComputer -Filter { WhenCreated -gt $recentDate } -Properties WhenCreated |
Select-Object Name, WhenCreated
if ($newUsers) {
Write-Host " New users in last 30 days:" -ForegroundColor Yellow
$newUsers | ForEach-Object { Write-Host " $($_.SamAccountName) - Created: $($_.WhenCreated)" }
}
if ($newGroups) {
Write-Host " New groups in last 30 days:" -ForegroundColor Yellow
$newGroups | ForEach-Object { Write-Host " $($_.Name) - Created: $($_.WhenCreated)" }
}
if ($newComputers) {
Write-Host " New computers in last 30 days:" -ForegroundColor Yellow
$newComputers | ForEach-Object { Write-Host " $($_.Name) - Created: $($_.WhenCreated)" }
}
if (-not $newUsers -and -not $newGroups -and -not $newComputers) {
Write-Host " No new AD objects in the last 30 days" -ForegroundColor Green
}
$audit.SecurityChecks.RecentChanges = [ordered]@{
NewUsers = @($newUsers | ForEach-Object { [ordered]@{ Name = $_.Name; SAM = $_.SamAccountName; Created = $_.WhenCreated.ToString("yyyy-MM-dd") } })
NewGroups = @($newGroups | ForEach-Object { [ordered]@{ Name = $_.Name; Created = $_.WhenCreated.ToString("yyyy-MM-dd") } })
NewComputers = @($newComputers | ForEach-Object { [ordered]@{ Name = $_.Name; Created = $_.WhenCreated.ToString("yyyy-MM-dd") } })
}
Write-Host " [OK] Recent changes checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "RecentChanges"; Error = $_.Exception.Message }
}
# --- krbtgt Password Age ---
Write-Host " Running: krbtgt password age check"
try {
$krbtgt = Get-ADUser 'krbtgt' -Properties PasswordLastSet -ErrorAction Stop
$krbtgtAge = ((Get-Date) - $krbtgt.PasswordLastSet).Days
Write-Host " krbtgt password last set: $($krbtgt.PasswordLastSet) - $krbtgtAge days ago"
if ($krbtgtAge -gt 180) {
Write-Host " [WARN] krbtgt password is $krbtgtAge days old - should be rotated at least every 180 days" -ForegroundColor Yellow
}
$audit.SecurityChecks.KrbtgtPasswordAge = [ordered]@{
LastSet = $krbtgt.PasswordLastSet.ToString("yyyy-MM-dd")
AgeDays = $krbtgtAge
}
Write-Host " [OK] krbtgt checked" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "KrbtgtAge"; Error = $_.Exception.Message }
}
# --- Audit Policy (are logons being audited?) ---
Write-Host " Running: Audit policy check"
try {
$auditPolicy = auditpol /get /category:* 2>&1
$auditPolicy | ForEach-Object { Write-Host " $_" }
# Check critical audit settings
$audit.SecurityChecks.AuditPolicyRaw = @($auditPolicy | ForEach-Object { "$_".Trim() } | Where-Object { $_ })
Write-Host " [OK] Audit policy collected" -ForegroundColor Green
}
catch {
Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red
$audit._errors += @{ Section = "AuditPolicy"; Error = $_.Exception.Message }
}
# --- Stopped Auto-Start Services ---
Write-Host " Running: 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.SecurityChecks.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 }
}
Write-Host ""
Write-Host " === Security Summary ===" -ForegroundColor Cyan
$totalWarnings = 0
if ($audit.SecurityChecks.PasswordPolicy.Issues) { $totalWarnings += $audit.SecurityChecks.PasswordPolicy.Issues.Count }
if ($audit.SecurityChecks.SMBv1Enabled) { $totalWarnings++ }
if ($audit.SecurityChecks.MachineAccountQuota -gt 0) { $totalWarnings++ }
if (-not $audit.SecurityChecks.RecycleBinEnabled) { $totalWarnings++ }
if ($audit.SecurityChecks.PasswordNeverExpires.Count -gt 0) { $totalWarnings++ }
if ($audit.SecurityChecks.Kerberoastable.Count -gt 0) { $totalWarnings++ }
if ($audit.SecurityChecks.ASREPRoastable.Count -gt 0) { $totalWarnings++ }
if ($audit.SecurityChecks.GPPPasswords.Count -gt 0) { $totalWarnings++ }
if ($audit.SecurityChecks.EOLComputers.Count -gt 0) { $totalWarnings++ }
if (-not $audit.SecurityChecks.LAPS.LegacyLAPS -and -not $audit.SecurityChecks.LAPS.WindowsLAPS) { $totalWarnings++ }
if ($totalWarnings -gt 0) {
Write-Host " $totalWarnings security findings detected - review warnings above" -ForegroundColor Yellow
} else {
Write-Host " No critical security findings" -ForegroundColor Green
}
# =====================================================
# DONE - SAVE JSON
# =====================================================
Write-Host ""
Write-Host "======================================="
Write-Host " AUDIT COMPLETE"
Write-Host "======================================="
$errorCount = $audit._errors.Count
if ($errorCount -gt 0) {
Write-Host "Completed with $errorCount section errors (see _errors in JSON)" -ForegroundColor Yellow
} else {
Write-Host "All sections completed successfully" -ForegroundColor Green
}
Write-Host "JSON data: $JsonFile"
Write-Host "======================================="
# Save JSON
$audit | ConvertTo-Json -Depth 10 | Out-File $JsonFile -Encoding UTF8