[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 += ("
  • {0}: {1}% (PASS {2} / FAIL {3} / Total {4})
  • " -f $k,$s.Pct,$s.Pass,$s.Fail,$s.Total) } } $summary = "" $rows = $results | ForEach-Object { "$($_.RuleId)$($_.Strategy)$($_.Title)$($_.Level)$($_.Status)
    $($_.Evidence -replace '<','<')
    " } | Out-String @" E8-CAT $Profile

    E8-CAT - Profile $Profile

    Scores

    $summary $rows
    RuleIdStrategyTitleLevelStatusEvidence
    "@ | Set-Content -Path $htmlPath -Encoding UTF8 Write-Host "Saved: $jsonPath" Write-Host "Saved: $csvPath" Write-Host "Saved: $htmlPath"