Files
E8-CAT/E8-CAT.ps1
2025-09-02 16:42:12 +02:00

199 lines
7.6 KiB
PowerShell

[CmdletBinding()]
param(
[ValidateSet('ML1','ML2','ML3','All')] [string]$Profile = 'ML1',
[string]$RulesPath = ".\rules",
[string]$ProfilesPath = ".\profiles",
[string]$OutDir = ".\out"
)
function New-Result($rule,$status,$evidence){
[pscustomobject]@{
RuleId = $rule.id
Title = $rule.title
Strategy = $rule.strategy
Level = $Profile
Status = $status # PASS / FAIL / SKIPPED / N/A / ERROR
Evidence = $evidence
Timestamp = (Get-Date).ToString("s")
}
}
function Test-Registry {
param($r)
$root = if ($r.scope -eq 'HKCU') { 'HKCU:' } else { 'HKLM:' }
$path = if ($r.path.StartsWith('\')) { Join-Path $root $r.path.Substring(1) } else { Join-Path $root $r.path }
try {
if (-not (Test-Path $path)) { return @{ok=$false; ev="Path missing: $path"} }
$prop = Get-ItemProperty -Path $path -ErrorAction Stop
if (-not $r.name -or $r.name -eq '') { return @{ ok=$true; ev="Key exists: $path" } }
if ($null -eq $prop.$($r.name)) { return @{ok=$false; ev="Value missing: $path\$($r.name)"} }
$val = $prop.$($r.name)
$op = if ($r.op) { $r.op } else { 'eq' }
$ok = $false
switch ($op) {
'eq' { $ok = ([string]$val -eq [string]$r.expected) }
'ne' { $ok = ([string]$val -ne [string]$r.expected) }
'in' { $ok = ($r.expected -contains ([string]$val)) }
'ge' { $ok = ([double]$val -ge [double]$r.expected) }
'le' { $ok = ([double]$val -le [double]$r.expected) }
default { $ok = ([string]$val -eq [string]$r.expected) }
}
@{ ok=$ok; ev="[$path] $($r.name)=$val (expect $op $($r.expected))" }
} catch { @{ ok=$false; ev=("Error: {0}" -f $_.Exception.Message) } }
}
function Test-File {
param($r)
$exists = Test-Path $r.path
$ok = if ($r.op -eq 'absent') { -not $exists } else { $exists }
@{ ok=$ok; ev="$($r.path) Exists=$exists (expect $($r.op))" }
}
function Test-Command {
param($r)
try {
$expected = if ($null -ne $r.expectedExitCode) { [int]$r.expectedExitCode } else { 0 }
$p = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -NonInteractive -Command `$ErrorActionPreference='Stop'; $($r.command)" -NoNewWindow -PassThru -Wait
$ok = ($p.ExitCode -eq $expected)
@{ ok=$ok; ev=("ExitCode={0} (expect {1})" -f $p.ExitCode, $expected) }
} catch { @{ ok=$false; ev=("Error: {0}" -f $_.Exception.Message) } }
}
function Test-ScriptBlock {
param($r)
try {
$sb = [ScriptBlock]::Create($r.script)
$res = & $sb
if ($null -eq $res) { return @{ ok=$false; ev="Script returned `$null (treat as SKIPPED)"; skipped=$true } }
$ok = [bool]$res
@{ ok=$ok; ev=("Script returned: {0}. Expected $true; return $null to SKIP." -f $res) }
} catch { @{ ok=$false; ev=("Error: {0}" -f $_.Exception.Message) } }
}
function Test-ADOptional {
param($r)
if (-not (Get-Module -ListAvailable ActiveDirectory)) {
return @{ ok=$false; ev="Skipped (ActiveDirectory module not found)"; skipped=$true }
}
try {
$sb = [ScriptBlock]::Create($r.script)
$res = & $sb
if ($null -eq $res) { return @{ ok=$false; ev="Script returned `$null (treat as SKIPPED)"; skipped=$true } }
$ok = [bool]$res
@{ ok=$ok; ev=("Script returned: {0}. Expected $true; return $null to SKIP." -f $res) }
} catch { @{ ok=$false; ev=("Error: {0}" -f $_.Exception.Message) } }
}
# Load rules
$ruleFiles = Get-ChildItem -Path $RulesPath -Filter *.json -File
$allRules = @()
foreach ($f in $ruleFiles) {
$data = Get-Content $f.FullName -Raw | ConvertFrom-Json
if ($data -is [System.Array]) { $allRules += $data } else { $allRules += ,$data }
}
# Load profile object unless All
$ProfileObj = $null
if ($Profile -ne 'All') {
$profileFile = Join-Path $ProfilesPath ("{0}.json" -f $Profile.ToLower())
if (-not (Test-Path $profileFile)) { throw "Profile not found: $profileFile" }
$ProfileObj = Get-Content $profileFile -Raw | ConvertFrom-Json
}
# Evaluate (single level or all levels)
$results = New-Object System.Collections.Generic.List[object]
# Determine rule set
$rules = $allRules
if ($Profile -ne 'All' -and $ProfileObj -and ($ProfileObj.PSObject.Properties.Name -contains 'includeRuleIds') -and $ProfileObj.includeRuleIds) {
$rules = @()
foreach ($r in $allRules) { if ($ProfileObj.includeRuleIds -contains $r.id) { $rules += ,$r } }
}
$levelsToRun = if ($Profile -eq 'All') { @('ML1','ML2','ML3') } else { @($Profile) }
$levelOrder = @{ ML1=1; ML2=2; ML3=3 }
foreach ($runLevel in $levelsToRun) {
foreach ($rule in $rules) {
switch ($rule.type) {
'registry' { $r = Test-Registry $rule }
'file' { $r = Test-File $rule }
'command' { $r = Test-Command $rule }
'scriptblock' { $r = Test-ScriptBlock $rule }
'ad-optional' { $r = Test-ADOptional $rule }
default { $r = @{ ok=$false; ev=("Unknown type {0}" -f $rule.type) } }
}
$applies = $true
if ($rule.minLevel) { $applies = ($levelOrder[$runLevel] -ge $levelOrder[$rule.minLevel]) }
if ($r.skipped) { $status = 'SKIPPED' }
else {
if (-not $applies) { $status = 'N/A' }
else { if ($r.ok) { $status = 'PASS' } else { $status = 'FAIL' } }
}
$results.Add([pscustomobject]@{
RuleId = $rule.id
Title = $rule.title
Strategy = $rule.strategy
Level = $runLevel
Status = $status
Evidence = $r.ev
Timestamp= (Get-Date).ToString("s")
})
}
}
# Score per level (avoid $pass name to prevent any oddities)
$levelScores = @{}
foreach ($lvl in $levelsToRun) {
$scored = $results | Where-Object { $_.Level -eq $lvl -and $_.Status -in 'PASS','FAIL' }
$passCount = ($scored | Where-Object { $_.Status -eq 'PASS' }).Count
$failCount = ($scored | Where-Object { $_.Status -eq 'FAIL' }).Count
$totalCount = [math]::Max(1, $scored.Count)
$pct = [math]::Round(100 * $passCount / $totalCount, 1)
$levelScores[$lvl] = @{ Pass=$passCount; Fail=$failCount; Total=$totalCount; Pct=$pct }
Write-Host ("E8-CAT {0} score: {1}% (PASS={2} / FAIL={3} / Total={4})" -f $lvl,$pct,$passCount,$failCount,$totalCount)
}
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$ts = (Get-Date).ToString("yyyyMMdd-HHmmss")
$jsonPath = Join-Path $OutDir ("E8CAT-{0}-{1}.json" -f $Profile,$ts)
$csvPath = Join-Path $OutDir ("E8CAT-{0}-{1}.csv" -f $Profile,$ts)
$htmlPath = Join-Path $OutDir ("E8CAT-{0}-{1}.html" -f $Profile,$ts)
$results | ConvertTo-Json -Depth 6 | Set-Content -Path $jsonPath -Encoding UTF8
$results | Export-Csv -NoTypeInformation -Path $csvPath -Encoding UTF8
$summary = ''
foreach ($k in ('ML1','ML2','ML3')) {
if ($levelScores.ContainsKey($k)) {
$s = $levelScores[$k]
$summary += ("<li><strong>{0}</strong>: {1}% (PASS {2} / FAIL {3} / Total {4})</li>" -f $k,$s.Pct,$s.Pass,$s.Fail,$s.Total)
}
}
$summary = "<ul>" + $summary + "</ul>"
$rows = $results | ForEach-Object {
"<tr><td>$($_.RuleId)</td><td>$($_.Strategy)</td><td>$($_.Title)</td><td>$($_.Level)</td><td>$($_.Status)</td><td><pre>$($_.Evidence -replace '<','&lt;')</pre></td></tr>"
} | Out-String
@"
<!doctype html><html><head><meta charset="utf-8"><title>E8-CAT $Profile</title>
<style>body{font-family:Segoe UI,Arial;margin:24px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:8px}th{background:#f5f5f5;text-align:left}td pre{white-space:pre-wrap}</style>
</head><body>
<h1>E8-CAT - Profile $Profile</h1>
<h3>Scores</h3>
$summary
<table><thead><tr><th>RuleId</th><th>Strategy</th><th>Title</th><th>Level</th><th>Status</th><th>Evidence</th></tr></thead>
<tbody>
$rows
</tbody></table>
</body></html>
"@ | Set-Content -Path $htmlPath -Encoding UTF8
Write-Host "Saved: $jsonPath"
Write-Host "Saved: $csvPath"
Write-Host "Saved: $htmlPath"