Compliance/Test-CISBenchmark.ps1
|
function Test-CISBenchmark { <# .SYNOPSIS Tests system compliance against CIS Benchmark controls .DESCRIPTION Evaluates Windows system against CIS (Center for Internet Security) Benchmark recommendations for security configuration .PARAMETER Level CIS Benchmark Level: 1 (basic) or 2 (high security) .PARAMETER Category Specific category to test: All, PasswordPolicy, AuditPolicy, SecurityOptions, WindowsFirewall .EXAMPLE Test-CISBenchmark -Level 1 Test-CISBenchmark -Level 2 -Category PasswordPolicy .OUTPUTS PSCustomObject with CIS compliance results #> [CmdletBinding()] param( [Parameter()] [ValidateSet(1, 2)] [int]$Level = 1, [Parameter()] [ValidateSet('All', 'PasswordPolicy', 'AuditPolicy', 'SecurityOptions', 'WindowsFirewall', 'UserRights')] [string]$Category = 'All' ) begin { Write-Host "=== CIS BENCHMARK COMPLIANCE TEST ===" -ForegroundColor Cyan Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow Write-Host "CIS Level: $Level" -ForegroundColor Yellow Write-Host "Category: $Category" -ForegroundColor Yellow $results = [PSCustomObject]@{ TestDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ComputerName = $env:COMPUTERNAME CISLevel = $Level Category = $Category Controls = @() Summary = @{ TotalControls = 0 Passed = 0 Failed = 0 NotApplicable = 0 CompliancePercentage = 0 } Recommendations = @() } # Helper function to test registry value function Test-RegistryValue { param( [string]$Path, [string]$Name, [object]$ExpectedValue, [string]$Comparison = 'Equal' ) try { if (Test-Path $Path) { $value = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $Name $result = switch ($Comparison) { 'Equal' { $value -eq $ExpectedValue } 'GreaterOrEqual' { $value -ge $ExpectedValue } 'LessOrEqual' { $value -le $ExpectedValue } 'NotEqual' { $value -ne $ExpectedValue } default { $false } } return @{ Compliant = $result ActualValue = $value ExpectedValue = $ExpectedValue } } else { return @{ Compliant = $false ActualValue = "Path not found" ExpectedValue = $ExpectedValue } } } catch { return @{ Compliant = $false ActualValue = "Error: $_" ExpectedValue = $ExpectedValue } } } # Helper function to safely parse secedit value # Helper function to safely parse secedit value - FIXED function Get-SecEditValue { param( [string]$Content, [string]$Setting ) try { # Split content into lines and search - FIX: regex match on lines $lines = $Content -split "`r?`n" $matchedLine = $lines | Where-Object { $_ -match "^\s*$Setting\s*=" } | Select-Object -First 1 if ($matchedLine) { # Split on = and take second part, trim whitespace $value = ($matchedLine -split '=', 2)[1].Trim() return $value } return $null } catch { return $null } } } process { try { # 1. PASSWORD POLICY CONTROLS if ($Category -in @('All', 'PasswordPolicy')) { Write-Host "`n[1/5] Testing Password Policy Controls..." -ForegroundColor Cyan try { # Export security policy $tempFile = "$env:TEMP\secpol_$(Get-Random).cfg" $null = secedit /export /cfg $tempFile /quiet Start-Sleep -Milliseconds 500 if (Test-Path $tempFile) { $secpolContent = Get-Content $tempFile -Raw # CIS 1.1.1 - Password History $passwordHistory = Get-SecEditValue -Content $secpolContent -Setting "PasswordHistorySize" if ($passwordHistory) { $control = [PSCustomObject]@{ ControlID = 'CIS-1.1.1' ControlName = 'Enforce password history' Category = 'PasswordPolicy' Expected = '24 or more passwords' Actual = "$passwordHistory passwords" Status = if ([int]$passwordHistory -ge 24) { 'Pass' } else { 'Fail' } Severity = 'High' Remediation = 'Set to 24 or more passwords remembered' } $results.Controls += $control } # CIS 1.1.2 - Maximum password age $maxPasswordAge = Get-SecEditValue -Content $secpolContent -Setting "MaximumPasswordAge" if ($maxPasswordAge) { $control = [PSCustomObject]@{ ControlID = 'CIS-1.1.2' ControlName = 'Maximum password age' Category = 'PasswordPolicy' Expected = '365 or fewer days (but not 0)' Actual = "$maxPasswordAge days" Status = if ([int]$maxPasswordAge -le 365 -and [int]$maxPasswordAge -gt 0) { 'Pass' } else { 'Fail' } Severity = 'High' Remediation = 'Set to 365 or fewer days' } $results.Controls += $control } # CIS 1.1.3 - Minimum password age $minPasswordAge = Get-SecEditValue -Content $secpolContent -Setting "MinimumPasswordAge" if ($minPasswordAge) { $control = [PSCustomObject]@{ ControlID = 'CIS-1.1.3' ControlName = 'Minimum password age' Category = 'PasswordPolicy' Expected = '1 or more days' Actual = "$minPasswordAge days" Status = if ([int]$minPasswordAge -ge 1) { 'Pass' } else { 'Fail' } Severity = 'Medium' Remediation = 'Set to 1 or more days' } $results.Controls += $control } # CIS 1.1.4 - Minimum password length $minPasswordLength = Get-SecEditValue -Content $secpolContent -Setting "MinimumPasswordLength" if ($minPasswordLength) { $control = [PSCustomObject]@{ ControlID = 'CIS-1.1.4' ControlName = 'Minimum password length' Category = 'PasswordPolicy' Expected = '14 or more characters' Actual = "$minPasswordLength characters" Status = if ([int]$minPasswordLength -ge 14) { 'Pass' } else { 'Fail' } Severity = 'High' Remediation = 'Set to 14 or more characters' } $results.Controls += $control } # CIS 1.1.5 - Password complexity $passwordComplexity = Get-SecEditValue -Content $secpolContent -Setting "PasswordComplexity" if ($passwordComplexity) { $control = [PSCustomObject]@{ ControlID = 'CIS-1.1.5' ControlName = 'Password must meet complexity requirements' Category = 'PasswordPolicy' Expected = 'Enabled' Actual = if ($passwordComplexity -eq '1') { 'Enabled' } else { 'Disabled' } Status = if ($passwordComplexity -eq '1') { 'Pass' } else { 'Fail' } Severity = 'Critical' Remediation = 'Enable password complexity requirements' } $results.Controls += $control } Remove-Item $tempFile -Force -ErrorAction SilentlyContinue } else { Write-Warning "Could not export security policy" } } catch { Write-Warning "Error testing password policy: $_" } } # 2. AUDIT POLICY CONTROLS if ($Category -in @('All', 'AuditPolicy')) { Write-Host "`n[2/5] Testing Audit Policy Controls..." -ForegroundColor Cyan try { # CIS 17.1.1 - Audit Credential Validation $auditTable = Get-AuditPolicyTable -Subcategory 'Credential Validation', 'Application Group Management', 'Account Lockout' $credentialValidationGuid = Get-AuditSubcategoryGuid -Name 'Credential Validation' $credentialValidation = $null if ($null -ne $credentialValidationGuid) { $credentialValidationKey = ($credentialValidationGuid -replace '[{}]', '').ToLowerInvariant() $credentialValidation = $auditTable[$credentialValidationKey] } $control = [PSCustomObject]@{ ControlID = 'CIS-17.1.1' ControlName = 'Audit Credential Validation' Category = 'AuditPolicy' Expected = 'Success and Failure' Actual = if ($null -eq $credentialValidation -or $null -eq $credentialValidation.Value) { 'Unable to determine' } elseif ($credentialValidation.Success -and $credentialValidation.Failure) { 'Success and Failure' } elseif ($credentialValidation.Success) { 'Success' } elseif ($credentialValidation.Failure) { 'Failure' } else { 'No Auditing' } Status = if ($null -eq $credentialValidation -or $null -eq $credentialValidation.Value) { 'Manual' } elseif ($credentialValidation.Value -eq 3) { 'Pass' } else { 'Fail' } Severity = 'High' Remediation = 'Configure to audit Success and Failure' } $results.Controls += $control # CIS 17.2.1 - Audit Application Group Management $appGroupGuid = Get-AuditSubcategoryGuid -Name 'Application Group Management' $appGroupManagement = $null if ($null -ne $appGroupGuid) { $appGroupKey = ($appGroupGuid -replace '[{}]', '').ToLowerInvariant() $appGroupManagement = $auditTable[$appGroupKey] } $control = [PSCustomObject]@{ ControlID = 'CIS-17.2.1' ControlName = 'Audit Application Group Management' Category = 'AuditPolicy' Expected = 'Success and Failure' Actual = if ($null -eq $appGroupManagement -or $null -eq $appGroupManagement.Value) { 'Unable to determine' } elseif ($appGroupManagement.Success -and $appGroupManagement.Failure) { 'Success and Failure' } elseif ($appGroupManagement.Success) { 'Success' } elseif ($appGroupManagement.Failure) { 'Failure' } else { 'No Auditing' } Status = if ($null -eq $appGroupManagement -or $null -eq $appGroupManagement.Value) { 'Manual' } elseif ($appGroupManagement.Value -eq 3) { 'Pass' } else { 'Fail' } Severity = 'Medium' Remediation = 'Configure to audit Success and Failure' } $results.Controls += $control # CIS 17.5.1 - Audit Account Lockout $accountLockoutGuid = Get-AuditSubcategoryGuid -Name 'Account Lockout' $accountLockout = $null if ($null -ne $accountLockoutGuid) { $accountLockoutKey = ($accountLockoutGuid -replace '[{}]', '').ToLowerInvariant() $accountLockout = $auditTable[$accountLockoutKey] } $control = [PSCustomObject]@{ ControlID = 'CIS-17.5.1' ControlName = 'Audit Account Lockout' Category = 'AuditPolicy' Expected = 'Success' Actual = if ($null -eq $accountLockout -or $null -eq $accountLockout.Value) { 'Unable to determine' } elseif ($accountLockout.Value -ge 1 -and $accountLockout.Value -le 3) { 'Configured' } else { 'Not Configured' } Status = if ($null -eq $accountLockout -or $null -eq $accountLockout.Value) { 'Manual' } elseif ($accountLockout.Value -ge 1 -and $accountLockout.Value -le 3) { 'Pass' } else { 'Fail' } Severity = 'Medium' Remediation = 'Configure to audit Success' } $results.Controls += $control } catch { Write-Warning "Error testing audit policy: $_" } } # 3. SECURITY OPTIONS if ($Category -in @('All', 'SecurityOptions')) { Write-Host "`n[3/5] Testing Security Options..." -ForegroundColor Cyan try { # CIS 2.3.1.1 - Accounts: Administrator account status $adminUser = Get-LocalUserByRid -Rid 500 -ErrorAction SilentlyContinue $adminKnown = ($null -ne $adminUser) -and ($null -ne $adminUser.Enabled) $control = [PSCustomObject]@{ ControlID = 'CIS-2.3.1.1' ControlName = 'Accounts: Administrator account status' Category = 'SecurityOptions' Expected = 'Disabled' Actual = if (-not $adminKnown) { 'Unable to determine' } elseif ($adminUser.Enabled) { 'Enabled' } else { 'Disabled' } Status = if (-not $adminKnown) { 'Manual' } elseif ($adminUser.Enabled) { 'Fail' } else { 'Pass' } Severity = 'High' Remediation = 'Disable the built-in Administrator account' } $results.Controls += $control # CIS 2.3.1.5 - Accounts: Guest account status $guestUser = Get-LocalUserByRid -Rid 501 -ErrorAction SilentlyContinue $guestKnown = ($null -ne $guestUser) -and ($null -ne $guestUser.Enabled) $control = [PSCustomObject]@{ ControlID = 'CIS-2.3.1.5' ControlName = 'Accounts: Guest account status' Category = 'SecurityOptions' Expected = 'Disabled' Actual = if (-not $guestKnown) { 'Unable to determine' } elseif ($guestUser.Enabled) { 'Enabled' } else { 'Disabled' } Status = if (-not $guestKnown) { 'Manual' } elseif ($guestUser.Enabled) { 'Fail' } else { 'Pass' } Severity = 'High' Remediation = 'Disable the Guest account' } $results.Controls += $control # CIS 2.3.7.1 - Interactive logon: Do not display last user name $regTest = Test-RegistryValue -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' ` -Name 'DontDisplayLastUserName' -ExpectedValue 1 $control = [PSCustomObject]@{ ControlID = 'CIS-2.3.7.1' ControlName = 'Interactive logon: Do not display last user name' Category = 'SecurityOptions' Expected = 'Enabled' Actual = if ($regTest.ActualValue -eq 1) { 'Enabled' } else { 'Disabled' } Status = if ($regTest.Compliant) { 'Pass' } else { 'Fail' } Severity = 'Medium' Remediation = 'Enable to hide last logged on user' } $results.Controls += $control # CIS 2.3.11.1 - Network security: Do not store LAN Manager hash $regTest = Test-RegistryValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' ` -Name 'NoLMHash' -ExpectedValue 1 $control = [PSCustomObject]@{ ControlID = 'CIS-2.3.11.1' ControlName = 'Network security: Do not store LAN Manager hash' Category = 'SecurityOptions' Expected = 'Enabled' Actual = if ($regTest.ActualValue -eq 1) { 'Enabled' } else { 'Disabled' } Status = if ($regTest.Compliant) { 'Pass' } else { 'Fail' } Severity = 'High' Remediation = 'Enable to prevent storing weak LM hashes' } $results.Controls += $control } catch { Write-Warning "Error testing security options: $_" } } # 4. WINDOWS FIREWALL if ($Category -in @('All', 'WindowsFirewall')) { Write-Host "`n[4/5] Testing Windows Firewall..." -ForegroundColor Cyan try { $firewallProfiles = Get-NetFirewallProfile -ErrorAction SilentlyContinue foreach ($profile in $firewallProfiles) { # CIS 9.1.1 - Domain Profile Firewall State $control = [PSCustomObject]@{ ControlID = "CIS-9.$($profile.Name).1" ControlName = "Windows Firewall: $($profile.Name): Firewall state" Category = 'WindowsFirewall' Expected = 'On' Actual = if ($profile.Enabled) { 'On' } else { 'Off' } Status = if ($profile.Enabled) { 'Pass' } else { 'Fail' } Severity = 'Critical' Remediation = "Enable Windows Firewall for $($profile.Name) profile" } $results.Controls += $control # CIS 9.1.2 - Inbound connections $control = [PSCustomObject]@{ ControlID = "CIS-9.$($profile.Name).2" ControlName = "Windows Firewall: $($profile.Name): Inbound connections" Category = 'WindowsFirewall' Expected = 'Block (default)' Actual = $profile.DefaultInboundAction Status = if ($profile.DefaultInboundAction -eq 'Block') { 'Pass' } else { 'Fail' } Severity = 'High' Remediation = "Set inbound connections to Block for $($profile.Name)" } $results.Controls += $control } } catch { Write-Warning "Error testing Windows Firewall: $_" } } # 5. USER RIGHTS ASSIGNMENT if ($Category -in @('All', 'UserRights')) { Write-Host "`n[5/5] Testing User Rights Assignment..." -ForegroundColor Cyan try { # CIS 2.2.1 - Access this computer from the network $control = [PSCustomObject]@{ ControlID = 'CIS-2.2.1' ControlName = 'Access this computer from the network' Category = 'UserRights' Expected = 'Administrators, Remote Desktop Users (for workstations)' Actual = 'Manual verification required' Status = 'Manual' Severity = 'Medium' Remediation = 'Limit to Administrators and Remote Desktop Users only' } $results.Controls += $control # CIS 2.2.3 - Allow log on locally $control = [PSCustomObject]@{ ControlID = 'CIS-2.2.3' ControlName = 'Allow log on locally' Category = 'UserRights' Expected = 'Administrators, Users' Actual = 'Manual verification required' Status = 'Manual' Severity = 'Medium' Remediation = 'Review and restrict local logon rights' } $results.Controls += $control } catch { Write-Warning "Error testing user rights: $_" } } # Calculate summary - FIX: Use @() to ensure array $results.Summary.TotalControls = $results.Controls.Count $results.Summary.Passed = @($results.Controls | Where-Object { $_.Status -eq 'Pass' }).Count $results.Summary.Failed = @($results.Controls | Where-Object { $_.Status -eq 'Fail' }).Count $results.Summary.NotApplicable = @($results.Controls | Where-Object { $_.Status -eq 'Manual' }).Count if ($results.Summary.TotalControls -gt 0) { $testableControls = $results.Summary.TotalControls - $results.Summary.NotApplicable if ($testableControls -gt 0) { $results.Summary.CompliancePercentage = [math]::Round( ($results.Summary.Passed / $testableControls) * 100, 2 ) } } # Generate recommendations - FIX: Use @() to ensure array $failedCritical = @($results.Controls | Where-Object { $_.Status -eq 'Fail' -and $_.Severity -eq 'Critical' }) $failedHigh = @($results.Controls | Where-Object { $_.Status -eq 'Fail' -and $_.Severity -eq 'High' }) if ($failedCritical.Count -gt 0) { $results.Recommendations += "CRITICAL: $($failedCritical.Count) critical control(s) failed" $results.Recommendations += "Address critical failures immediately to reduce security risk" } if ($failedHigh.Count -gt 0) { $results.Recommendations += "HIGH: $($failedHigh.Count) high-severity control(s) failed" } if ($results.Summary.CompliancePercentage -lt 70) { $results.Recommendations += "Compliance below 70% - comprehensive security review required" } $results.Recommendations += "Review failed controls and implement remediation steps" $results.Recommendations += "Document exceptions for controls that cannot be implemented" } catch { Write-Error "Error during CIS benchmark testing: $_" throw } } end { Write-Host "`n=== COMPLIANCE TEST COMPLETE ===" -ForegroundColor $( if ($results.Summary.CompliancePercentage -ge 80) { 'Green' } elseif ($results.Summary.CompliancePercentage -ge 60) { 'Yellow' } else { 'Red' } ) Write-Host "`nCompliance Summary:" -ForegroundColor Cyan Write-Host " Total Controls Tested: $($results.Summary.TotalControls)" -ForegroundColor White Write-Host " Passed: $($results.Summary.Passed)" -ForegroundColor Green Write-Host " Failed: $($results.Summary.Failed)" -ForegroundColor Red Write-Host " Manual Review: $($results.Summary.NotApplicable)" -ForegroundColor Yellow Write-Host " Compliance: $($results.Summary.CompliancePercentage)%" -ForegroundColor $( if ($results.Summary.CompliancePercentage -ge 80) { 'Green' } elseif ($results.Summary.CompliancePercentage -ge 60) { 'Yellow' } else { 'Red' } ) if ($results.Controls.Count -gt 0) { $failedControls = @($results.Controls | Where-Object { $_.Status -eq 'Fail' }) if ($failedControls.Count -gt 0) { Write-Host "`nFailed Controls by Severity:" -ForegroundColor Cyan $failedBySeverity = $failedControls | Group-Object Severity foreach ($group in $failedBySeverity | Sort-Object Name) { Write-Host " $($group.Name): $($group.Count)" -ForegroundColor $( switch ($group.Name) { 'Critical' { 'Red' } 'High' { 'Red' } 'Medium' { 'Yellow' } default { 'White' } } ) } } } if ($results.Recommendations.Count -gt 0) { Write-Host "`nRecommendations:" -ForegroundColor Cyan $results.Recommendations | ForEach-Object { Write-Host " ! $_" -ForegroundColor Yellow } } Write-Host "`nCIS Benchmarks: https://www.cisecurity.org/cis-benchmarks" -ForegroundColor Gray return $results } } |