Skip to content

Commit 0d5c36b

Browse files
committed
v0.4.3: AMSI registry checks, skippable baseline, ASCII CI box, pre-commit hook, TrustedAppDirs in process checks
Made-with: Cursor
1 parent 1124bc6 commit 0d5c36b

10 files changed

Lines changed: 168 additions & 47 deletions

File tree

.githooks/pre-commit

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/sh
2+
# Pre-commit hook: enforce UTF-8 BOM on all staged .ps1 files.
3+
# PowerShell 5.1 reads files without BOM as Windows-1252, which corrupts
4+
# non-ASCII characters (box-drawing, checkmarks) and causes parse errors.
5+
6+
missing=""
7+
8+
for file in $(git diff --cached --name-only --diff-filter=ACM | grep '\.ps1$'); do
9+
# Read first 3 bytes and check for UTF-8 BOM (EF BB BF)
10+
bom=$(head -c 3 "$file" | od -An -tx1 | tr -d ' \n')
11+
if [ "$bom" != "efbbbf" ]; then
12+
missing="$missing\n $file"
13+
fi
14+
done
15+
16+
if [ -n "$missing" ]; then
17+
echo ""
18+
echo "ERROR: The following .ps1 files are missing a UTF-8 BOM:"
19+
printf "$missing\n"
20+
echo ""
21+
echo "Fix by running: pwsh -File fix-bom.ps1 (or: powershell -File fix-bom.ps1)"
22+
echo "Then re-stage the files and commit again."
23+
echo ""
24+
exit 1
25+
fi
26+
27+
exit 0

AmIHacked.ps1

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ if ($script:NonInteractive) {
7979
}
8080
$script:RedactMap = @{}
8181

82-
$script:Version = "0.4.2"
82+
$script:Version = "0.4.3"
8383

8484
# ── Helpers (loaded first) ───────────────────────────────────────────────────
8585

@@ -178,16 +178,29 @@ if ($script:RedactMode) {
178178

179179
$defaultBaselinePath = Join-Path (Join-Path $PSScriptRoot "reports") "baseline_latest.json"
180180

181-
if ($BaselinePath -and (Test-Path $BaselinePath)) {
182-
Write-Section "Baseline Comparison"
183-
Compare-Baseline -BaselinePath $BaselinePath
184-
} elseif (-not $BaselinePath -and (Test-Path $defaultBaselinePath)) {
185-
Write-Section "Baseline Comparison"
186-
Compare-Baseline -BaselinePath $defaultBaselinePath
187-
} elseif (-not $CreateBaseline) {
188-
Add-Finding -Severity "INFO" -Category "General" -Title "No Baseline Found" `
189-
-Description "No baseline snapshot exists for comparison. Run with -CreateBaseline on a known-clean system to enable change detection on future scans." `
190-
-Remediation ".\AmIHacked.ps1 -CreateBaseline"
181+
if ('Baseline' -notin $SkipModules) {
182+
if ($BaselinePath -and (Test-Path $BaselinePath)) {
183+
Write-Section "Baseline Comparison"
184+
Compare-Baseline -BaselinePath $BaselinePath
185+
} elseif (-not $BaselinePath -and (Test-Path $defaultBaselinePath)) {
186+
Write-Section "Baseline Comparison"
187+
Compare-Baseline -BaselinePath $defaultBaselinePath
188+
} elseif (-not $CreateBaseline) {
189+
Add-Finding -Severity "INFO" -Category "General" -Title "No Baseline Found" `
190+
-Description "No baseline snapshot exists for comparison. Run with -CreateBaseline on a known-clean system to enable change detection on future scans." `
191+
-Remediation ".\AmIHacked.ps1 -CreateBaseline"
192+
}
193+
}
194+
195+
# ── Preflight: Signature Verification Capability ─────────────────────────────
196+
197+
Import-Module Microsoft.PowerShell.Security -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue 2>$null
198+
$script:SignatureCheckAvailable = $null -ne (Get-Command Get-AuthenticodeSignature -ErrorAction SilentlyContinue)
199+
if (-not $script:SignatureCheckAvailable) {
200+
Add-Finding -Severity "WARNING" -Category "General" `
201+
-Title "Signature Verification Degraded" `
202+
-Description "The PowerShell Security module (Microsoft.PowerShell.Security) could not be loaded in this session. Authenticode signature checks will be skipped or may produce false positives. Affected checks: AMSI DLL integrity, COM hijack detection, unsigned process/file detection." `
203+
-Remediation "Run the scan in a fresh PowerShell session. If the issue persists, run 'sfc /scannow' to repair PowerShell modules."
191204
}
192205

193206
# ── Dynamic Module Discovery ─────────────────────────────────────────────────
@@ -318,40 +331,65 @@ $w = 44
318331

319332
Write-Host ""
320333
Write-Host ""
321-
Write-Host "$('' * $w)" -ForegroundColor DarkCyan
322-
Write-Host "" -NoNewline -ForegroundColor DarkCyan
323-
$verdictPad = $verdict.PadLeft([math]::Floor(($w + $verdict.Length) / 2)).PadRight($w)
324-
Write-Host $verdictPad -NoNewline -ForegroundColor $verdictColor
325-
Write-Host "" -ForegroundColor DarkCyan
326-
Write-Host "$('' * $w)" -ForegroundColor DarkCyan
327-
328-
function Write-SummaryLine { param($Label, $Value, $Color, $Width)
329-
$content = "$Label$Value"
330-
$innerWidth = $Width - 4
331-
$padded = $content.PadRight($innerWidth)
332-
Write-Host "" -NoNewline -ForegroundColor DarkCyan
333-
Write-Host $Label -NoNewline -ForegroundColor DarkGray
334-
Write-Host $Value -NoNewline -ForegroundColor $Color
335-
$remaining = $innerWidth - $content.Length
336-
if ($remaining -gt 0) { Write-Host (' ' * $remaining) -NoNewline }
337-
Write-Host "" -ForegroundColor DarkCyan
338-
}
339334

340-
Write-SummaryLine "CRITICAL " "$critCount" $(if ($critCount -gt 0) { "Red" } else { "Green" }) $w
341-
Write-SummaryLine "WARNING " "$warnCount" $(if ($warnCount -gt 0) { "Yellow" } else { "Green" }) $w
342-
Write-SummaryLine "INFO " "$infoCount" "DarkCyan" $w
343-
Write-Host "$(' ' * $w)" -ForegroundColor DarkCyan
335+
if ($script:NonInteractive) {
336+
# ASCII-only box for CI/agent environments (avoids encoding issues in piped output)
337+
$hbar = '=' * ($w + 2)
338+
Write-Host " +$hbar+"
339+
$verdictPad = $verdict.PadLeft([math]::Floor(($w + 2 + $verdict.Length) / 2)).PadRight($w + 2)
340+
Write-Host " |$verdictPad|"
341+
Write-Host " +$hbar+"
342+
343+
function Write-SummaryLine { param($Label, $Value, $Color, $Width)
344+
$content = " $Label$Value"
345+
$innerWidth = $Width - 2
346+
$line = " | $Label$Value".PadRight($Width + 3) + " |"
347+
Write-Host $line
348+
}
344349

345-
Write-SummaryLine "Total " "$totalCount findings" "White" $w
346-
Write-SummaryLine "Duration " "$durationStr" "DarkGray" $w
347-
Write-Host "$(' ' * $w)" -ForegroundColor DarkCyan
348-
Write-SummaryLine "Report " "See path below" "DarkGray" $w
350+
Write-SummaryLine "CRITICAL " "$critCount" "Red" $w
351+
Write-SummaryLine "WARNING " "$warnCount" "Yellow" $w
352+
Write-SummaryLine "INFO " "$infoCount" "DarkCyan" $w
353+
Write-Host " | $(' ' * ($w - 2)) |"
354+
Write-SummaryLine "Total " "$totalCount findings" "White" $w
355+
Write-SummaryLine "Duration " "$durationStr" "DarkGray" $w
356+
Write-Host " | $(' ' * ($w - 2)) |"
357+
Write-SummaryLine "Report " "See path below" "DarkGray" $w
358+
Write-Host " +$hbar+"
359+
} else {
360+
Write-Host "$('' * $w)" -ForegroundColor DarkCyan
361+
Write-Host "" -NoNewline -ForegroundColor DarkCyan
362+
$verdictPad = $verdict.PadLeft([math]::Floor(($w + $verdict.Length) / 2)).PadRight($w)
363+
Write-Host $verdictPad -NoNewline -ForegroundColor $verdictColor
364+
Write-Host "" -ForegroundColor DarkCyan
365+
Write-Host "$('' * $w)" -ForegroundColor DarkCyan
366+
367+
function Write-SummaryLine { param($Label, $Value, $Color, $Width)
368+
$content = "$Label$Value"
369+
$innerWidth = $Width - 4
370+
Write-Host "" -NoNewline -ForegroundColor DarkCyan
371+
Write-Host $Label -NoNewline -ForegroundColor DarkGray
372+
Write-Host $Value -NoNewline -ForegroundColor $Color
373+
$remaining = $innerWidth - $content.Length
374+
if ($remaining -gt 0) { Write-Host (' ' * $remaining) -NoNewline }
375+
Write-Host "" -ForegroundColor DarkCyan
376+
}
377+
378+
Write-SummaryLine "CRITICAL " "$critCount" $(if ($critCount -gt 0) { "Red" } else { "Green" }) $w
379+
Write-SummaryLine "WARNING " "$warnCount" $(if ($warnCount -gt 0) { "Yellow" } else { "Green" }) $w
380+
Write-SummaryLine "INFO " "$infoCount" "DarkCyan" $w
381+
Write-Host "$(' ' * $w)" -ForegroundColor DarkCyan
382+
Write-SummaryLine "Total " "$totalCount findings" "White" $w
383+
Write-SummaryLine "Duration " "$durationStr" "DarkGray" $w
384+
Write-Host "$(' ' * $w)" -ForegroundColor DarkCyan
385+
Write-SummaryLine "Report " "See path below" "DarkGray" $w
386+
Write-Host "$('' * $w)" -ForegroundColor DarkCyan
387+
}
349388

350-
Write-Host "$('' * $w)" -ForegroundColor DarkCyan
351389
Write-Host ""
352390

353391
if ($critCount -gt 0) {
354-
Write-Host " ██ CRITICAL findings detected. Review the report immediately." -ForegroundColor Red
392+
Write-Host " !! CRITICAL findings detected. Review the report immediately." -ForegroundColor Red
355393
} elseif ($warnCount -eq 0 -and $critCount -eq 0) {
356394
Write-Host " [+] No threats detected. System appears clean." -ForegroundColor Green
357395
}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [0.4.3] - 2026-03-15
8+
9+
### Added
10+
- **AMSI registry checks** -- detects `AmsiEnable=0` (CRITICAL, T1562.001) and missing PowerShell Script Block Logging (INFO, T1562.002)
11+
- **PS.Security preflight** -- detects when `Get-AuthenticodeSignature` is unavailable and emits a WARNING at scan startup listing affected checks
12+
- **`-SkipModules Baseline`** -- baseline comparison is now skippable, enabling faster targeted scans without baseline drift noise
13+
- **ASCII verdict box in CI mode** -- non-interactive output uses `+==+|` instead of Unicode box-drawing, preventing encoding issues in piped/agent output
14+
- **Pre-commit hook** -- `.githooks/pre-commit` rejects commits with `.ps1` files missing UTF-8 BOM
15+
16+
### Fixed
17+
- **Unsigned process false positives** -- `Check-Processes.ps1` now uses `TrustedAppDirs` from config to skip processes in trusted directories (e.g. Git for Windows)
18+
- **Ephemeral port baseline noise** -- ports in the Windows dynamic range (49152-65535) are excluded from baseline diffs
19+
- Added `Git\usr\bin` to `TrustedAppDirs` in `config.example.json`
20+
721
## [0.4.2] - 2026-03-15
822

923
### Fixed

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Copied from `config/config.example.json`. Controls:
8383

8484
### Baseline System
8585

86-
`-CreateBaseline` snapshots ports, services, accounts, Run keys, scheduled tasks, and Defender exclusions to JSON. Baselines are **never auto-overwritten** — this is intentional to prevent a compromised system from poisoning its own baseline.
86+
`-CreateBaseline` snapshots ports, services, accounts, Run keys, scheduled tasks, and Defender exclusions to JSON. Baselines are **never auto-overwritten** — this is intentional to prevent a compromised system from poisoning its own baseline. Baseline comparison can be skipped with `-SkipModules Baseline`. Ephemeral ports (49152-65535) are excluded from baseline diffs.
8787

8888
### Redaction System
8989

@@ -102,7 +102,7 @@ Individual modules do not need to handle redaction.
102102

103103
`-CIMode` makes the tool usable by AI terminal agents and CI pipelines:
104104

105-
- Suppresses ASCII banner (plain-text header instead) and browser auto-open
105+
- Suppresses ASCII banner (plain-text header instead), uses ASCII-only verdict box, and suppresses browser auto-open
106106
- Auto-enables `-Redact` so operator identity is never leaked
107107
- Prints a JSON summary to stdout after all output, delimited by `---AMIHACKED-SUMMARY-JSON---`
108108
- Exits with structured code: 0 = clean, 1 = warnings, 2 = critical

CONTRIBUTING.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,22 @@ Edit `config/config.example.json` to add (or `config/config.json` locally):
9696

9797
## Code Style
9898

99-
- All `.ps1` files must use **UTF-8 BOM** encoding (PowerShell 5.1 reads files as Windows-1252 without it, breaking non-ASCII characters)
99+
- All `.ps1` files must use **UTF-8 BOM** encoding (PowerShell 5.1 reads files as Windows-1252 without it, breaking non-ASCII characters). Run `fix-bom.ps1` after any edit to re-apply the BOM (most editors and the Claude Code Edit tool strip it on save).
100100
- PowerShell verb-noun naming for functions
101101
- Consistent indentation (4 spaces)
102102
- Section headers with `# ── N. Section Name ──...` pattern
103103
- Status messages via `Write-Status`, not `Write-Host`
104104

105+
### Git hooks (BOM enforcement)
106+
107+
A pre-commit hook rejects commits that include `.ps1` files without a UTF-8 BOM. Activate it once per clone:
108+
109+
```sh
110+
git config core.hooksPath .githooks
111+
```
112+
113+
If the hook blocks your commit, run `fix-bom.ps1`, re-stage, and commit again.
114+
105115
## Submitting a PR
106116

107117
1. Fork the repo and create a feature branch

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
[![PowerShell 5.1+](https://img.shields.io/badge/PowerShell-5.1%2B-0d1117?style=for-the-badge&logo=powershell&logoColor=5391FE)](https://docs.microsoft.com/powershell/)
1313
[![Windows 10/11](https://img.shields.io/badge/Windows-10%20%2F%2011-0d1117?style=for-the-badge&logo=windows&logoColor=white)](https://www.microsoft.com/windows)
1414
[![License: MIT](https://img.shields.io/badge/License-MIT-0d1117?style=for-the-badge&logoColor=white)](LICENSE)
15-
[![Version](https://img.shields.io/badge/Version-0.4.2-FF6B6B?style=for-the-badge)](#changelog)
15+
[![Version](https://img.shields.io/badge/Version-0.4.3-FF6B6B?style=for-the-badge)](#changelog)
1616

1717
[![Zero Dependencies](https://img.shields.io/badge/Dependencies-Zero-0d1117?style=flat-square&labelColor=0d1117)](#)
1818
[![MITRE ATT&CK](https://img.shields.io/badge/MITRE%20ATT%26CK-40%2B%20Techniques-ff3333?style=flat-square&labelColor=0d1117)](#mitre-attck-coverage)
@@ -111,7 +111,7 @@ Baselines enable **change detection** — the most powerful signal for catching
111111
| Parameter | Description |
112112
|-----------|-------------|
113113
| `-OutputPath` | Report output directory (default: `.\reports`) |
114-
| `-SkipModules` | Module names to skip (e.g. `Network,Accounts`) |
114+
| `-SkipModules` | Module names to skip (e.g. `Network,Accounts,Baseline`) |
115115
| `-ConfigPath` | Path to `config.json` (default: `.\config\config.json`) |
116116
| `-Offline` | Disable all external API calls |
117117
| `-BaselinePath` | Path to a specific baseline JSON |
@@ -140,7 +140,7 @@ Baselines enable **change detection** — the most powerful signal for catching
140140

141141
```
142142
---AMIHACKED-SUMMARY-JSON---
143-
{"verdict":"CAUTION","critical":0,"warning":3,"info":12,"total":15,"duration":28.4,"reportPath":"...","version":"0.4.2"}
143+
{"verdict":"CAUTION","critical":0,"warning":3,"info":12,"total":15,"duration":28.4,"reportPath":"...","version":"0.4.3"}
144144
```
145145

146146
- Exit code reflects findings: **0** = clean, **1** = warnings only, **2** = critical findings detected

config/config.example.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
"1Password", "Bitwarden", "Proton", "node_modules",
6767
"OneDrive", "WindowsApps", "WinGet", "squirrel.windows",
6868
"Plutonium", "cursor", "Claude", "Amazon Games",
69-
"NuGet", "npm", "pip", "cargo"
69+
"NuGet", "npm", "pip", "cargo",
70+
"Git\\usr\\bin"
7071
],
7172
"TrustedDomainSuffixes": [
7273
".microsoft.com", ".windowsupdate.com", ".akamaized.net",

lib/Helpers.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ function Compare-Baseline {
547547
$currentPorts = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
548548
ForEach-Object { $_.LocalPort } | Sort-Object -Unique
549549
$oldPorts = $old.ListeningPorts | ForEach-Object { $_.Port } | Sort-Object -Unique
550-
$newPorts = $currentPorts | Where-Object { $_ -notin $oldPorts }
550+
$newPorts = $currentPorts | Where-Object { $_ -notin $oldPorts -and $_ -lt 49152 } # skip ephemeral range
551551
foreach ($port in $newPorts) {
552552
Add-Finding -Severity "WARNING" -Category "Baseline" `
553553
-Title "New Listening Port: $port" `

modules/Check-DefenseEvasion.ps1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,30 @@ function Invoke-DefenseEvasionChecks {
8989
}
9090
} catch {}
9191

92+
try {
93+
$amsiEnable = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows Script\Settings" -Name "AmsiEnable" -ErrorAction SilentlyContinue
94+
if ($amsiEnable -and $amsiEnable.AmsiEnable -eq 0) {
95+
Add-Finding -Severity "CRITICAL" -Category "DefenseEvasion" `
96+
-Title "AMSI Explicitly Disabled via Registry" `
97+
-Description "HKLM:\SOFTWARE\Microsoft\Windows Script\Settings!AmsiEnable is set to 0. This explicitly disables AMSI for Windows Script Host (VBScript, JScript), allowing malicious scripts to run without AV scanning." `
98+
-Remediation "Remove-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows Script\Settings' -Name 'AmsiEnable'" `
99+
-Details @{ Key = "HKLM:\SOFTWARE\Microsoft\Windows Script\Settings\AmsiEnable"; Value = 0 } `
100+
-MITRE @("T1562.001")
101+
}
102+
} catch {}
103+
104+
try {
105+
$sbl = Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -ErrorAction SilentlyContinue
106+
if (-not $sbl -or -not $sbl.EnableScriptBlockLogging -or $sbl.EnableScriptBlockLogging -eq 0) {
107+
Add-Finding -Severity "INFO" -Category "DefenseEvasion" `
108+
-Title "PowerShell Script Block Logging Not Enabled" `
109+
-Description "Script block logging is not enabled via policy. When enabled, PowerShell logs all executed script blocks to the event log (Event ID 4104), which is valuable for detecting obfuscated or malicious scripts." `
110+
-Remediation "Set-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -Name EnableScriptBlockLogging -Value 1" `
111+
-Details @{ Key = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging\EnableScriptBlockLogging"; Value = "absent or 0" } `
112+
-MITRE @("T1562.002")
113+
}
114+
} catch {}
115+
92116
# ── 3. Windows Defender Real-Time Protection ─────────────────────────
93117

94118
Write-Status "Checking Defender real-time protection..."

modules/Check-Processes.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ function Invoke-ProcessesChecks {
3030
if (-not $procPath) { continue }
3131
if ($procPath -match "^C:\\Windows\\System32" -and $whitelist -contains $procName) { continue }
3232
if ($procPath -match '^C:\\Program Files\\WindowsApps\\') { continue }
33+
if ($script:Config.TrustedAppDirs) {
34+
$inTrustedDir = $false
35+
foreach ($dir in $script:Config.TrustedAppDirs) {
36+
if ($procPath -like "*$dir*") { $inTrustedDir = $true; break }
37+
}
38+
if ($inTrustedDir) { continue }
39+
}
3340

3441
$pathKey = "$($proc.Name)|$procPath"
3542
if ($seenProcessPaths.ContainsKey($pathKey)) {

0 commit comments

Comments
 (0)