Skip to content

Commit 486dc10

Browse files
committed
feat: firewall integrity guard + 3rd-party AV pre-flight + undo manifest
Adds three safety features to Disable/Enable: 1. Firewall integrity guard: both operations snapshot Get-NetFirewallProfile state (Domain/Private/Public enabled) plus the mpssvc and BFE service state before the first change (new Phase 0), then verify after the last change. Any divergence surfaces as an ERROR in the log. The "firewall untouched" promise is now machine-checked, not just documented. 2. Third-party AV pre-flight: Phase 0 of Disable queries root\SecurityCenter2\AntivirusProduct. When zero non-Microsoft products are registered, the operation logs a prominent warning about leaving the system with no real-time AV before proceeding. Still proceeds so air-gapped and sandbox use cases aren't blocked. 3. Undo / audit manifest: every Disable/Enable writes a JSON manifest to %ProgramData%\DefenderControl\manifests\<op>-<timestamp>.json with schemaVersion=1, operation, dryRun flag, timestamps, firewall before/after, diff list, intact flag, thirdPartyAV list, and phases completed. Manifest capture is additive — the existing operation flow is unchanged. CLI: -Mode Manifest prints the latest manifest; -Json emits raw JSON. Manifest dir is created on first run and survives across reboots. Disable total phases goes 10 -> 11, Enable 7 -> 8 (both add the firewall verification phase at the end).
1 parent d7ff37a commit 486dc10

3 files changed

Lines changed: 211 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@ All notable changes to DefenderControl will be documented in this file.
55
## Unreleased
66

77
### Added
8-
- CLI mode with `-Mode Status|Health|Verify` for read-only state enumeration
8+
- CLI mode with `-Mode Status|Health|Verify|Manifest` for read-only state
99
- `-Json` flag emits stable JSON (single object) for automation pipelines
1010
- `-Silent`, `-DryRun`, `-NoRestorePoint`, `-NoReboot`, `-Help` CLI flags
1111
- Stable CLI exit codes: 0 success / 1 partial / 2 tamper-blocked / 3 safe-mode / 4 usage
1212
- `Get-DefenderState` shared query function used by GUI dashboard and CLI
1313
- Extended Health mode: per-service PPL flag enumeration, scheduled task state,
1414
policy-key values, third-party AV detection via Security Center
15+
- **Firewall integrity guard**: Disable/Enable both snapshot Get-NetFirewallProfile
16+
state + mpssvc/BFE service state before the first change and verify after the
17+
last change. Any divergence is logged as an ERROR so the "firewall untouched"
18+
guarantee is now machine-checked, not just documented.
19+
- **Third-party AV pre-flight**: Phase 0 of Disable queries the Security Center
20+
WMI namespace (`root\SecurityCenter2`) and warns prominently when no
21+
non-Microsoft AV is registered. The operation still proceeds so air-gapped /
22+
sandbox use cases aren't blocked.
23+
- **Undo/audit manifest**: every Disable/Enable persists a JSON manifest under
24+
`%ProgramData%\DefenderControl\manifests\<operation>-<timestamp>.json` with
25+
schema version, dry-run flag, firewall before/after, third-party AV list,
26+
phases completed. `-Mode Manifest` prints the latest manifest; `-Json` emits
27+
raw.
1528

1629
### Changed
1730
- Self-elevation now forwards all original arguments through the UAC re-launch

DefenderControl.ps1

Lines changed: 193 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787

8888
[CmdletBinding()]
8989
param(
90-
[ValidateSet('Disable','Enable','Status','Health','Verify')]
90+
[ValidateSet('Disable','Enable','Status','Health','Verify','Manifest')]
9191
[string]$Mode,
9292

9393
[switch]$DryRun,
@@ -121,6 +121,8 @@ Defender Control - CLI Usage
121121
DefenderControl.ps1 -Mode Status -Json Emit state as JSON
122122
DefenderControl.ps1 -Mode Health Extended read-only state enum
123123
DefenderControl.ps1 -Mode Health -Json Extended state as JSON
124+
DefenderControl.ps1 -Mode Manifest Print the latest undo manifest
125+
DefenderControl.ps1 -Mode Manifest -Json Latest manifest as JSON
124126
DefenderControl.ps1 -Help Show this usage
125127
126128
Flags:
@@ -481,6 +483,29 @@ function Invoke-CliMode {
481483
exit $script:EXIT_OK
482484
}
483485

486+
'Manifest' {
487+
$dir = Join-Path $env:ProgramData 'DefenderControl\manifests'
488+
if (-not (Test-Path $dir)) {
489+
Write-CliLine "No manifests directory yet. Run a Disable/Enable first." -ErrorStream
490+
exit $script:EXIT_USAGE
491+
}
492+
$latest = Get-ChildItem -Path $dir -Filter '*.json' -ErrorAction SilentlyContinue |
493+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
494+
if (-not $latest) {
495+
Write-CliLine "No manifests found in $dir" -ErrorStream
496+
exit $script:EXIT_USAGE
497+
}
498+
$text = Get-Content -Raw -Path $latest.FullName
499+
if ($Json.IsPresent) {
500+
[Console]::Out.WriteLine($text)
501+
} else {
502+
Write-CliLine ("Latest manifest: " + $latest.FullName)
503+
Write-CliLine '---'
504+
[Console]::Out.WriteLine($text)
505+
}
506+
exit $script:EXIT_OK
507+
}
508+
484509
'Disable' {
485510
Write-CliLine "DefenderControl: -Mode Disable is reserved. Use the GUI for mutating operations." -ErrorStream
486511
exit $script:EXIT_USAGE
@@ -1293,6 +1318,96 @@ function Set-ServicePPL {
12931318
else { Queue-Warn " `$ServiceName : Could not change LaunchProtected (may need Safe Mode)" }
12941319
return `$result
12951320
}
1321+
function Get-FirewallSnapshot {
1322+
# Returns a plain hashtable of firewall profile state + protected service state.
1323+
# Used as before/after pair to prove this tool did not touch the firewall.
1324+
`$snap = [ordered]@{}
1325+
try {
1326+
Get-NetFirewallProfile -ErrorAction Stop | ForEach-Object {
1327+
`$snap["Profile_`$(`$_.Name)_Enabled"] = [bool]`$_.Enabled
1328+
}
1329+
} catch {
1330+
Queue-Verbose " Firewall snapshot: Get-NetFirewallProfile failed (`$(`$_.Exception.Message))"
1331+
}
1332+
foreach (`$svc in @('mpssvc','BFE')) {
1333+
`$o = Get-Service -Name `$svc -ErrorAction SilentlyContinue
1334+
if (`$o) {
1335+
`$snap["Service_`${svc}_Status"] = "`$(`$o.Status)"
1336+
`$snap["Service_`${svc}_StartType"] = "`$(`$o.StartType)"
1337+
} else {
1338+
`$snap["Service_`${svc}_Status"] = 'NotFound'
1339+
`$snap["Service_`${svc}_StartType"] = 'NotFound'
1340+
}
1341+
}
1342+
return `$snap
1343+
}
1344+
function Test-FirewallIntact {
1345+
# Before/After are ordered-dict-or-hashtable. Use .Contains for cross-compat.
1346+
param(`$Before, `$After)
1347+
`$diffs = @()
1348+
if (`$null -eq `$Before -or `$null -eq `$After) { return ,`$diffs }
1349+
foreach (`$k in @(`$Before.Keys)) {
1350+
if (`$After.Contains(`$k) -and `$After[`$k] -ne `$Before[`$k]) {
1351+
`$diffs += "`${k}: `$(`$Before[`$k]) -> `$(`$After[`$k])"
1352+
}
1353+
}
1354+
return ,`$diffs # force array return
1355+
}
1356+
function Get-ThirdPartyAVList {
1357+
# Query Security Center for registered AV products; exclude Microsoft Defender.
1358+
try {
1359+
`$products = Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName 'AntivirusProduct' -ErrorAction Stop
1360+
if (-not `$products) { return @() }
1361+
return @(`$products |
1362+
Where-Object { `$_.displayName -notmatch 'Windows Defender|Microsoft Defender' } |
1363+
ForEach-Object { `$_.displayName })
1364+
} catch {
1365+
Queue-Verbose " Third-party AV detection failed: `$(`$_.Exception.Message)"
1366+
return `$null
1367+
}
1368+
}
1369+
function New-DefenderControlManifest {
1370+
param([string]`$Operation, [bool]`$DryRunFlag)
1371+
`$dir = Join-Path `$env:ProgramData 'DefenderControl\manifests'
1372+
if (-not (Test-Path `$dir)) {
1373+
try { New-Item -Path `$dir -ItemType Directory -Force -ErrorAction Stop | Out-Null } catch {
1374+
Queue-Verbose " Manifest dir create failed: `$(`$_.Exception.Message)"
1375+
return `$null
1376+
}
1377+
}
1378+
`$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
1379+
`$file = Join-Path `$dir ("`$Operation-`$ts.json")
1380+
return [ordered]@{
1381+
schemaVersion = 1
1382+
operation = `$Operation
1383+
dryRun = `$DryRunFlag
1384+
startedAt = (Get-Date).ToString('o')
1385+
host = `$env:COMPUTERNAME
1386+
osBuild = `$OSBuild
1387+
firewallBefore = `$null
1388+
firewallAfter = `$null
1389+
firewallIntact = `$null
1390+
firewallDiffs = @()
1391+
thirdPartyAV = @()
1392+
phasesCompleted = @()
1393+
finishedAt = `$null
1394+
path = `$file
1395+
}
1396+
}
1397+
function Save-DefenderControlManifest {
1398+
param(`$Manifest)
1399+
if (`$null -eq `$Manifest) { return `$null }
1400+
`$Manifest.finishedAt = (Get-Date).ToString('o')
1401+
try {
1402+
`$json = (`$Manifest | ConvertTo-Json -Depth 8)
1403+
[System.IO.File]::WriteAllText(`$Manifest.path, `$json, [System.Text.Encoding]::UTF8)
1404+
Queue-Info " Undo manifest saved: `$(`$Manifest.path)"
1405+
return `$Manifest.path
1406+
} catch {
1407+
Queue-Warn " Manifest save failed: `$(`$_.Exception.Message)"
1408+
return `$null
1409+
}
1410+
}
12961411
"@
12971412

12981413
# ==================================================================================
@@ -1518,8 +1633,9 @@ function Invoke-DisableDefender {
15181633
$systrayPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Systray"
15191634
$runPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
15201635
$explorerPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run"
1521-
$totalPhases = 10
1636+
$totalPhases = 11
15221637
$phase = 0
1638+
$manifest = New-DefenderControlManifest -Operation 'Disable' -DryRunFlag $DryRun
15231639

15241640
if ($DryRun) {
15251641
Queue-Warn "========== DRY RUN MODE - No changes will be made =========="
@@ -1529,6 +1645,28 @@ function Invoke-DisableDefender {
15291645
Queue-Info "============================================"
15301646
Start-Sleep -Milliseconds 100
15311647

1648+
# -- Phase 0: Safety pre-flight (firewall snapshot + third-party AV) ----------
1649+
Queue-Phase "--- Phase 0 : Safety Pre-Flight ---"
1650+
$fwBefore = Get-FirewallSnapshot
1651+
if ($manifest) { $manifest.firewallBefore = $fwBefore }
1652+
Queue-Verbose " Firewall snapshot captured ($($fwBefore.Count) fields)"
1653+
1654+
$tpAv = Get-ThirdPartyAVList
1655+
if ($manifest) {
1656+
if ($null -eq $tpAv) { $manifest.thirdPartyAV = @() }
1657+
else { $manifest.thirdPartyAV = @($tpAv) }
1658+
}
1659+
if ($null -eq $tpAv) {
1660+
Queue-Warn " Could not query Security Center for third-party AV"
1661+
} elseif ((@($tpAv)).Count -eq 0) {
1662+
Queue-Warn " No third-party antivirus detected. After disabling Defender, this system will have"
1663+
Queue-Warn " NO real-time malware protection. Consider installing one (e.g. ESET, Bitdefender,"
1664+
Queue-Warn " Malwarebytes) before rebooting, unless this is an air-gapped / sandbox system."
1665+
} else {
1666+
Queue-Success " Third-party AV detected: $((@($tpAv)) -join ', ')"
1667+
}
1668+
Start-Sleep -Milliseconds 60
1669+
15321670
# -- Phase 1: System Restore Point -----------------------------------------------
15331671
$phase++
15341672
Queue-Status -StatusText "DISABLING..." -StatusColor "#e67e22" -TamperText "" -TamperColor "#7f8c8d" -DisableBtn $false -EnableBtn $false -Progress ([int]($phase / $totalPhases * 100)) -RunningText "Phase $phase/$totalPhases - System Restore Point"
@@ -1839,6 +1977,30 @@ function Invoke-DisableDefender {
18391977
} catch { Queue-Info " WinDefend service stop blocked (PPL) - will not restart after reboot" }
18401978
}
18411979

1980+
# -- Phase 11: Firewall Integrity Verification ----------------------------------
1981+
$phase++
1982+
Queue-Status -StatusText "DISABLING..." -StatusColor "#e67e22" -TamperText "" -TamperColor "#7f8c8d" -DisableBtn $false -EnableBtn $false -Progress ([int]($phase / $totalPhases * 100)) -RunningText "Phase $phase/$totalPhases - Firewall integrity check"
1983+
Queue-Phase "--- Phase $phase/$totalPhases : Firewall Integrity Verification ---"
1984+
$fwAfter = Get-FirewallSnapshot
1985+
$fwDiffs = Test-FirewallIntact -Before $fwBefore -After $fwAfter
1986+
if ($manifest) {
1987+
$manifest.firewallAfter = $fwAfter
1988+
$manifest.firewallDiffs = @($fwDiffs)
1989+
$manifest.firewallIntact = ((@($fwDiffs)).Count -eq 0)
1990+
$manifest.phasesCompleted = @('Pre-flight','RestorePoint','TamperCheck','Preferences','GroupPolicy','Notifications','ScheduledTasks','Services','ContextMenus','Additional','Processes','FirewallVerify')
1991+
}
1992+
if ((@($fwDiffs)).Count -eq 0) {
1993+
Queue-Success " Firewall state unchanged - tool honored the firewall-untouched guarantee"
1994+
} else {
1995+
Queue-Err " Firewall state diverged from pre-flight snapshot!"
1996+
foreach ($d in @($fwDiffs)) { Queue-Err " $d" }
1997+
Queue-Warn " This should never happen. Please file an issue with the operation log."
1998+
}
1999+
Start-Sleep -Milliseconds 60
2000+
2001+
# -- Manifest: persist undo/audit manifest --------------------------------------
2002+
$null = Save-DefenderControlManifest -Manifest $manifest
2003+
18422004
# -- Final Status ----------------------------------------------------------------
18432005
Queue-Info "============================================"
18442006
if ($DryRun) {
@@ -1878,8 +2040,9 @@ function Invoke-EnableDefender {
18782040
Start-BackgroundWork -AutoRefresh -Work {
18792041
$runPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
18802042
$explorerPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run"
1881-
$totalPhases = 7
2043+
$totalPhases = 8
18822044
$phase = 0
2045+
$manifest = New-DefenderControlManifest -Operation 'Enable' -DryRunFlag $DryRun
18832046

18842047
if ($DryRun) {
18852048
Queue-Warn "========== DRY RUN MODE - No changes will be made =========="
@@ -1889,6 +2052,11 @@ function Invoke-EnableDefender {
18892052
Queue-Info "============================================"
18902053
Start-Sleep -Milliseconds 100
18912054

2055+
# -- Phase 0: Firewall snapshot (for post-enable integrity check) --------------
2056+
$fwBefore = Get-FirewallSnapshot
2057+
if ($manifest) { $manifest.firewallBefore = $fwBefore }
2058+
Queue-Verbose "Firewall snapshot captured ($($fwBefore.Count) fields)"
2059+
18922060
# -- Phase 1: Remove Policy Overrides -------------------------------------------
18932061
$phase++
18942062
Queue-Status -StatusText "ENABLING..." -StatusColor "#e67e22" -TamperText "" -TamperColor "#7f8c8d" -DisableBtn $false -EnableBtn $false -Progress ([int]($phase / $totalPhases * 100)) -RunningText "Phase $phase/$totalPhases - Removing policies"
@@ -2154,6 +2322,28 @@ function Invoke-EnableDefender {
21542322
if ($st.RealTimeProtectionEnabled -and $st.AntivirusEnabled) { $ok = $true }
21552323
} catch { Queue-Warn " Verify failed (reboot needed): $($_.Exception.Message)" }
21562324

2325+
# -- Phase 8: Firewall Integrity Verification -----------------------------------
2326+
$phase++
2327+
Queue-Status -StatusText "ENABLING..." -StatusColor "#e67e22" -TamperText "" -TamperColor "#7f8c8d" -DisableBtn $false -EnableBtn $false -Progress ([int]($phase / $totalPhases * 100)) -RunningText "Phase $phase/$totalPhases - Firewall integrity check"
2328+
Queue-Phase "--- Phase $phase/$totalPhases : Firewall Integrity Verification ---"
2329+
$fwAfter = Get-FirewallSnapshot
2330+
$fwDiffs = Test-FirewallIntact -Before $fwBefore -After $fwAfter
2331+
if ($manifest) {
2332+
$manifest.firewallAfter = $fwAfter
2333+
$manifest.firewallDiffs = @($fwDiffs)
2334+
$manifest.firewallIntact = ((@($fwDiffs)).Count -eq 0)
2335+
$manifest.phasesCompleted = @('FirewallSnapshot','RemovePolicies','RestorePreferences','RestoreServices','ScheduledTasks','ContextMenusSystray','SignatureUpdate','Verify','FirewallVerify')
2336+
}
2337+
if ((@($fwDiffs)).Count -eq 0) {
2338+
Queue-Success " Firewall state unchanged - tool honored the firewall-untouched guarantee"
2339+
} else {
2340+
Queue-Err " Firewall state diverged from pre-enable snapshot!"
2341+
foreach ($d in @($fwDiffs)) { Queue-Err " $d" }
2342+
}
2343+
2344+
# -- Manifest: persist undo/audit manifest --------------------------------------
2345+
$null = Save-DefenderControlManifest -Manifest $manifest
2346+
21572347
Queue-Info "============================================"
21582348
if ($DryRun) {
21592349
Queue-Info " DRY RUN COMPLETE - No changes were made"

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Sometimes you need Defender completely out of the way — deploying custom imagi
1414

1515
Defender Control performs a thorough multi-phase disable that persists across reboots by targeting preferences, group policy registry keys, services, scheduled tasks, PPL flags, and more. Everything is fully reversible with a single click.
1616

17-
> **Windows Firewall is completely untouched.** This tool only manages Defender antivirus components.
17+
> **Windows Firewall is completely untouched.** This tool only manages Defender antivirus components. Starting in v3.2.0, this guarantee is machine-checked: every Disable/Enable run snapshots Get-NetFirewallProfile and the mpssvc/BFE service state before the first change and verifies it after the last change. Any divergence is logged as an error.
1818
1919
---
2020

@@ -33,6 +33,9 @@ Defender Control performs a thorough multi-phase disable that persists across re
3333
- **OS Build Awareness** — Detects Win10/11, warns on deprecated GP keys (Win11 22H2+), blocks unsupported versions
3434
- **Self-Elevation** — Automatically requests Administrator via UAC
3535
- **Orphan Cleanup** — Removes leftover scheduled tasks from interrupted previous runs
36+
- **Firewall Integrity Guard** — Snapshots firewall profile state + mpssvc/BFE service state before Phase 1; verifies no divergence after Phase 10
37+
- **Third-Party AV Pre-Flight** — Warns via Security Center WMI when no non-Microsoft AV is registered before disabling
38+
- **Undo / Audit Manifest** — Every Disable/Enable writes a JSON audit record to `%ProgramData%\DefenderControl\manifests\`; view with `-Mode Manifest`
3639

3740
---
3841

0 commit comments

Comments
 (0)