# .SYNOPSIS Windows Security Baseline Audit Script .DESCRIPTION Audits a Windows Server or Workstation against Microsoft Security Baselines and CIS Benchmarks. Checks account policies, audit policies, user rights, security options, firewall configuration, Windows Update, SMB hardening, TLS settings, and service configuration. Produces a summary report with pass/fail/warning counts and an HTML report. .PARAMETER OutputPath Path for the HTML report (default: C:\SecurityAudit\baseline-audit.html) .PARAMETER Format Output format: 'html' (default), 'csv', or 'json' .PARAMETER ChecksFile Path to a custom checks JSON file (optional) .NOTES Author: Phil Connor Contact: contact@mylinux.work Website: https://mylinux.work License: MIT Version: 1.0 #> param( [string]$OutputPath = "C:\SecurityAudit\baseline-audit-$(Get-Date -Format 'yyyyMMdd-HHmmss').html", [ValidateSet('html', 'csv', 'json')] [string]$Format = 'html', [string]$ChecksFile ) $ErrorActionPreference = 'SilentlyContinue' # ============================================================================ # RESULTS COLLECTION # ============================================================================ $script:Results = [System.Collections.ArrayList]::new() $script:PassCount = 0 $script:FailCount = 0 $script:WarnCount = 0 $script:InfoCount = 0 function Add-AuditResult { param( [string]$Category, [string]$Check, [ValidateSet('Pass', 'Fail', 'Warn', 'Info')] [string]$Status, [string]$Expected, [string]$Actual, [string]$Description ) switch ($Status) { 'Pass' { $script:PassCount++ } 'Fail' { $script:FailCount++ } 'Warn' { $script:WarnCount++ } 'Info' { $script:InfoCount++ } } [void]$script:Results.Add([PSCustomObject]@{ Category = $Category Check = $Check Status = $Status Expected = $Expected Actual = $Actual Description = $Description }) } # ============================================================================ # ACCOUNT POLICIES # ============================================================================ function Test-AccountPolicies { Write-Host "Checking account policies..." -ForegroundColor Cyan # Export security policy $tempFile = "$env:TEMP\secpol_export.cfg" secedit /export /cfg $tempFile /quiet 2>$null if (-not (Test-Path $tempFile)) { Add-AuditResult "Account Policies" "Security Policy Export" "Fail" "Exported" "Failed" "Cannot export security policy" return } $secpol = Get-Content $tempFile # Password length $minLen = ($secpol | Select-String 'MinimumPasswordLength\s*=\s*(\d+)' | ForEach-Object { $_.Matches.Groups[1].Value }) -as [int] $expected = 14 if ($minLen -ge $expected) { Add-AuditResult "Account Policies" "Minimum Password Length" "Pass" ">= $expected" "$minLen" "CIS: Minimum password length" } else { Add-AuditResult "Account Policies" "Minimum Password Length" "Fail" ">= $expected" "$minLen" "CIS: Minimum password length should be $expected+" } # Password history $history = ($secpol | Select-String 'PasswordHistorySize\s*=\s*(\d+)' | ForEach-Object { $_.Matches.Groups[1].Value }) -as [int] if ($history -ge 24) { Add-AuditResult "Account Policies" "Password History" "Pass" ">= 24" "$history" "CIS: Enforce password history" } else { Add-AuditResult "Account Policies" "Password History" "Fail" ">= 24" "$history" "CIS: Should remember 24+ passwords" } # Maximum password age $maxAge = ($secpol | Select-String 'MaximumPasswordAge\s*=\s*(\d+)' | ForEach-Object { $_.Matches.Groups[1].Value }) -as [int] if ($maxAge -le 365 -and $maxAge -gt 0) { Add-AuditResult "Account Policies" "Maximum Password Age" "Pass" "<= 365 days" "$maxAge days" "CIS: Maximum password age" } else { Add-AuditResult "Account Policies" "Maximum Password Age" "Fail" "<= 365 days" "$maxAge days" "Password age too high or unlimited" } # Account lockout threshold $lockout = ($secpol | Select-String 'LockoutBadCount\s*=\s*(\d+)' | ForEach-Object { $_.Matches.Groups[1].Value }) -as [int] if ($lockout -ge 1 -and $lockout -le 5) { Add-AuditResult "Account Policies" "Account Lockout Threshold" "Pass" "1-5 attempts" "$lockout" "CIS: Account lockout threshold" } else { Add-AuditResult "Account Policies" "Account Lockout Threshold" "Fail" "1-5 attempts" "$lockout" "Should lock after 1-5 failed attempts" } # Lockout duration $lockDuration = ($secpol | Select-String 'LockoutDuration\s*=\s*(\d+)' | ForEach-Object { $_.Matches.Groups[1].Value }) -as [int] if ($lockDuration -ge 15) { Add-AuditResult "Account Policies" "Account Lockout Duration" "Pass" ">= 15 min" "$lockDuration min" "CIS: Lockout duration" } else { Add-AuditResult "Account Policies" "Account Lockout Duration" "Fail" ">= 15 min" "$lockDuration min" "Should be 15+ minutes" } # Password complexity $complexity = $secpol | Select-String 'PasswordComplexity\s*=\s*1' if ($complexity) { Add-AuditResult "Account Policies" "Password Complexity" "Pass" "Enabled" "Enabled" "CIS: Password must meet complexity requirements" } else { Add-AuditResult "Account Policies" "Password Complexity" "Fail" "Enabled" "Disabled" "Enable password complexity" } Remove-Item $tempFile -Force -ErrorAction SilentlyContinue } # ============================================================================ # AUDIT POLICIES # ============================================================================ function Test-AuditPolicies { Write-Host "Checking audit policies..." -ForegroundColor Cyan $auditPolicies = @{ "Credential Validation" = "Success and Failure" "Logon" = "Success and Failure" "Logoff" = "Success" "Account Lockout" = "Failure" "User Account Management" = "Success and Failure" "Security Group Management" = "Success" "Process Creation" = "Success" "Audit Policy Change" = "Success and Failure" "Authentication Policy Change" = "Success" "Sensitive Privilege Use" = "Success and Failure" "System Integrity" = "Success and Failure" "Security State Change" = "Success" } $auditpolOutput = auditpol /get /category:* 2>$null foreach ($policy in $auditPolicies.GetEnumerator()) { $line = $auditpolOutput | Select-String $policy.Key | Select-Object -First 1 if ($line) { $currentSetting = $line.ToString().Trim() if ($currentSetting -match $policy.Value -or $currentSetting -match "Success and Failure") { Add-AuditResult "Audit Policies" $policy.Key "Pass" $policy.Value "Configured" "Advanced audit policy" } else { Add-AuditResult "Audit Policies" $policy.Key "Fail" $policy.Value "Not configured" "Enable in Advanced Audit Policy" } } else { Add-AuditResult "Audit Policies" $policy.Key "Warn" $policy.Value "Not found" "Cannot determine audit policy status" } } } # ============================================================================ # FIREWALL # ============================================================================ function Test-FirewallConfiguration { Write-Host "Checking firewall configuration..." -ForegroundColor Cyan $profiles = @('Domain', 'Private', 'Public') foreach ($profile in $profiles) { $fw = Get-NetFirewallProfile -Name $profile -ErrorAction SilentlyContinue if ($fw) { if ($fw.Enabled) { Add-AuditResult "Firewall" "$profile Profile Enabled" "Pass" "Enabled" "Enabled" "Windows Firewall $profile profile" } else { Add-AuditResult "Firewall" "$profile Profile Enabled" "Fail" "Enabled" "Disabled" "Enable Windows Firewall for $profile" } if ($fw.DefaultInboundAction -eq 'Block') { Add-AuditResult "Firewall" "$profile Inbound Default" "Pass" "Block" "Block" "Default inbound action" } else { Add-AuditResult "Firewall" "$profile Inbound Default" "Fail" "Block" "$($fw.DefaultInboundAction)" "Set default inbound to Block" } if ($fw.LogFileName) { Add-AuditResult "Firewall" "$profile Logging" "Pass" "Enabled" $fw.LogFileName "Firewall logging path" } else { Add-AuditResult "Firewall" "$profile Logging" "Warn" "Enabled" "Not configured" "Enable firewall logging" } } } } # ============================================================================ # SMB HARDENING # ============================================================================ function Test-SMBHardening { Write-Host "Checking SMB hardening..." -ForegroundColor Cyan # SMBv1 $smb1 = Get-SmbServerConfiguration -ErrorAction SilentlyContinue if ($smb1) { if (-not $smb1.EnableSMB1Protocol) { Add-AuditResult "SMB" "SMBv1 Disabled" "Pass" "Disabled" "Disabled" "SMBv1 should be disabled (security risk)" } else { Add-AuditResult "SMB" "SMBv1 Disabled" "Fail" "Disabled" "Enabled" "Disable SMBv1: Set-SmbServerConfiguration -EnableSMB1Protocol $false" } if ($smb1.RequireSecuritySignature) { Add-AuditResult "SMB" "SMB Signing Required" "Pass" "Required" "Required" "SMB signing prevents MITM attacks" } else { Add-AuditResult "SMB" "SMB Signing Required" "Fail" "Required" "Not required" "Enable SMB signing" } if ($smb1.EncryptData) { Add-AuditResult "SMB" "SMB Encryption" "Pass" "Enabled" "Enabled" "SMB 3.0+ encryption" } else { Add-AuditResult "SMB" "SMB Encryption" "Warn" "Enabled" "Disabled" "Consider enabling SMB encryption" } } } # ============================================================================ # TLS CONFIGURATION # ============================================================================ function Test-TLSConfiguration { Write-Host "Checking TLS configuration..." -ForegroundColor Cyan $protocols = @{ 'SSL 2.0' = @{ Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server'; Expected = 'Disabled' } 'SSL 3.0' = @{ Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server'; Expected = 'Disabled' } 'TLS 1.0' = @{ Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server'; Expected = 'Disabled' } 'TLS 1.1' = @{ Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server'; Expected = 'Disabled' } 'TLS 1.2' = @{ Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server'; Expected = 'Enabled' } } foreach ($proto in $protocols.GetEnumerator()) { $regPath = $proto.Value.Path $expected = $proto.Value.Expected $enabled = $true if (Test-Path $regPath) { $val = Get-ItemProperty -Path $regPath -Name 'Enabled' -ErrorAction SilentlyContinue if ($val -and $val.Enabled -eq 0) { $enabled = $false } } if ($expected -eq 'Disabled') { if (-not $enabled -or (Test-Path $regPath)) { $disabledCheck = Get-ItemProperty -Path $regPath -Name 'Enabled' -ErrorAction SilentlyContinue if ($disabledCheck -and $disabledCheck.Enabled -eq 0) { Add-AuditResult "TLS" "$($proto.Key) Disabled" "Pass" "Disabled" "Disabled" "Legacy protocol disabled" } else { Add-AuditResult "TLS" "$($proto.Key) Disabled" "Warn" "Disabled" "Not explicitly disabled" "Disable $($proto.Key) via registry" } } else { Add-AuditResult "TLS" "$($proto.Key) Disabled" "Warn" "Disabled" "Registry key not set" "Explicitly disable $($proto.Key)" } } else { Add-AuditResult "TLS" "$($proto.Key) Enabled" "Info" "Enabled" "Default" "TLS 1.2 enabled by default on modern Windows" } } } # ============================================================================ # WINDOWS UPDATE # ============================================================================ function Test-WindowsUpdate { Write-Host "Checking Windows Update status..." -ForegroundColor Cyan try { $lastUpdate = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 1 if ($lastUpdate) { $daysSince = ((Get-Date) - $lastUpdate.InstalledOn).Days if ($daysSince -le 30) { Add-AuditResult "Windows Update" "Last Update" "Pass" "<= 30 days" "$daysSince days ago" "HotFix: $($lastUpdate.HotFixID)" } elseif ($daysSince -le 90) { Add-AuditResult "Windows Update" "Last Update" "Warn" "<= 30 days" "$daysSince days ago" "Updates may be overdue" } else { Add-AuditResult "Windows Update" "Last Update" "Fail" "<= 30 days" "$daysSince days ago" "System needs patching" } } } catch { Add-AuditResult "Windows Update" "Last Update" "Warn" "<= 30 days" "Unknown" "Cannot determine update status" } } # ============================================================================ # SERVICES # ============================================================================ function Test-ServiceConfiguration { Write-Host "Checking service configuration..." -ForegroundColor Cyan $riskyServices = @{ 'RemoteRegistry' = 'Remote Registry - should be disabled' 'Spooler' = 'Print Spooler - disable on servers not used for printing' 'SNMP' = 'SNMP - legacy protocol, disable if unused' 'TelnetClient' = 'Telnet - insecure, use SSH instead' 'W3SVC' = 'IIS - disable if not serving web content' } foreach ($svc in $riskyServices.GetEnumerator()) { $service = Get-Service -Name $svc.Key -ErrorAction SilentlyContinue if ($service) { if ($service.Status -eq 'Running') { Add-AuditResult "Services" "$($svc.Key)" "Warn" "Stopped/Disabled" "Running" $svc.Value } elseif ($service.StartType -eq 'Automatic') { Add-AuditResult "Services" "$($svc.Key)" "Warn" "Disabled" "Auto-start" $svc.Value } else { Add-AuditResult "Services" "$($svc.Key)" "Pass" "Disabled" "$($service.StartType)" $svc.Value } } } } # ============================================================================ # ADDITIONAL CHECKS # ============================================================================ function Test-AdditionalSecurity { Write-Host "Checking additional security settings..." -ForegroundColor Cyan # RDP NLA $nla = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -ErrorAction SilentlyContinue if ($nla -and $nla.UserAuthentication -eq 1) { Add-AuditResult "RDP" "Network Level Authentication" "Pass" "Enabled" "Enabled" "NLA required for RDP" } else { Add-AuditResult "RDP" "Network Level Authentication" "Fail" "Enabled" "Disabled" "Enable NLA for RDP connections" } # BitLocker $bitlocker = Get-BitLockerVolume -MountPoint 'C:' -ErrorAction SilentlyContinue if ($bitlocker -and $bitlocker.ProtectionStatus -eq 'On') { Add-AuditResult "Encryption" "BitLocker C: Drive" "Pass" "On" "On" "System drive encrypted" } else { Add-AuditResult "Encryption" "BitLocker C: Drive" "Warn" "On" "Off or N/A" "Consider enabling BitLocker" } # Windows Defender $defender = Get-MpComputerStatus -ErrorAction SilentlyContinue if ($defender) { if ($defender.RealTimeProtectionEnabled) { Add-AuditResult "Defender" "Real-Time Protection" "Pass" "Enabled" "Enabled" "Windows Defender real-time protection" } else { Add-AuditResult "Defender" "Real-Time Protection" "Fail" "Enabled" "Disabled" "Enable real-time protection" } $sigAge = $defender.AntivirusSignatureAge if ($sigAge -le 3) { Add-AuditResult "Defender" "Signature Age" "Pass" "<= 3 days" "$sigAge days" "AV signatures up to date" } else { Add-AuditResult "Defender" "Signature Age" "Fail" "<= 3 days" "$sigAge days" "Update AV signatures" } } # Guest account $guest = Get-LocalUser -Name 'Guest' -ErrorAction SilentlyContinue if ($guest -and -not $guest.Enabled) { Add-AuditResult "Accounts" "Guest Account Disabled" "Pass" "Disabled" "Disabled" "Guest account should be disabled" } elseif ($guest) { Add-AuditResult "Accounts" "Guest Account Disabled" "Fail" "Disabled" "Enabled" "Disable the Guest account" } # Administrator account renamed $admin = Get-LocalUser | Where-Object { $_.SID -match 'S-1-5-21-.*-500$' } if ($admin -and $admin.Name -ne 'Administrator') { Add-AuditResult "Accounts" "Administrator Renamed" "Pass" "Renamed" $admin.Name "Built-in admin account renamed" } else { Add-AuditResult "Accounts" "Administrator Renamed" "Warn" "Renamed" "Administrator" "Consider renaming the built-in admin account" } } # ============================================================================ # REPORT GENERATION # ============================================================================ function New-HTMLReport { $total = $script:PassCount + $script:FailCount + $script:WarnCount + $script:InfoCount $scorePercent = if (($script:PassCount + $script:FailCount) -gt 0) { [math]::Round($script:PassCount / ($script:PassCount + $script:FailCount) * 100, 1) } else { 0 } $html = @"
Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Host: $env:COMPUTERNAME | OS: $((Get-CimInstance Win32_OperatingSystem).Caption)
| Check | Status | Expected | Actual | Description |
|---|---|---|---|---|
| $($result.Check) | $($result.Status) | $($result.Expected) | $($result.Actual) | $($result.Description) |