Initial commit
This commit is contained in:
198
E8-CAT.ps1
Normal file
198
E8-CAT.ps1
Normal file
@@ -0,0 +1,198 @@
|
||||
[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 '<','<')</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"
|
Reference in New Issue
Block a user