Reject firewall log path drift before configuration

This commit is contained in:
Shirofune-Security committed 2026-09-19 02:33:53 +09:00
1 parent 84f263ed1f
commit a10e1d6e84
3 files changed
+42 -4

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ The default `Preserve` path mode never writes `LogFileName`. `CisV4` explicitly
Reports retain `ActiveStore` (resultant effective policy) and `PersistentStore` (local policy), plus differing logging fields. Configure writes only PersistentStore, verifies ActiveStore after each change, and checks it again at completion. A local write overridden by GPO/MDM fails verification. A previously compliant setting that changes before the final check becomes `Overridden`; the run exits 1. These observations do not establish the current policy writer or guarantee persistence after a later policy refresh. An already compliant effective policy is accepted without manufacturing a local override.
Recovery snapshots include both stores. WELA rechecks them after confirmation and journaling, and refuses the write if logging settings changed meanwhile. There is no atomic transaction with Group Policy; subsequent races remain detectable only through the following reads. A failed control does not stop the remaining profiles.
Recovery snapshots include both stores. Preserve mode rejects an effective path that changed after planning, even before the initial recovery snapshot, and verifies access to the actual effective destination. Rerun the plan to assess a changed path. WELA rechecks both stores after confirmation and journaling, and refuses the write if logging settings changed meanwhile. Explicit `CisV4` migration instead checks the selected new destination. There is no atomic transaction with Group Policy; subsequent races remain detectable only through the following reads. A failed control does not stop the remaining profiles.
## Service permissions and operational limits
+12 -2
View File
@@ -141,7 +141,13 @@ function Set-WelaFirewallLoggingControls {
$read = {
param($state)
$snapshot = Get-WelaFirewallLoggingState -Name $state.Name
$targetAccess = if ($snapshot.Effective.LogFileName -eq $state.Desired.LogFileName) { $snapshot.Access } else { Get-WelaFirewallLogAccess -Path $state.Desired.LogFileName }
if ($state.Desired.PathMode -eq 'Preserve' -and
[Environment]::ExpandEnvironmentVariables($snapshot.Effective.LogFileName) -ne [Environment]::ExpandEnvironmentVariables($state.Desired.LogFileName)) {
throw 'Effective firewall log path changed after planning; rerun the plan to assess and preserve the current destination.'
}
$targetAccess = if ($state.Desired.PathMode -eq 'Preserve' -or $snapshot.Effective.LogFileName -eq $state.Desired.LogFileName) {
$snapshot.Access
} else { Get-WelaFirewallLogAccess -Path $state.Desired.LogFileName }
if ($targetAccess.State -ne 'VerifiedExplicitGrant') { throw "Log path access is $($targetAccess.State): $($targetAccess.Diagnostic)" }
$state.Observed = $snapshot
return $snapshot
@@ -156,7 +162,11 @@ function Set-WelaFirewallLoggingControls {
if ($fresh.$store.$property -ne $state.Observed.$store.$property) { throw "Firewall $store $property changed after the recovery snapshot; retry after reviewing policy." }
}
}
$access = Get-WelaFirewallLogAccess -Path $state.Desired.LogFileName
if ($state.Desired.PathMode -eq 'Preserve' -and $fresh.Access.State -ne 'VerifiedExplicitGrant') {
throw "Effective log path access is $($fresh.Access.State): $($fresh.Access.Diagnostic)"
}
$targetPath = if ($state.Desired.PathMode -eq 'Preserve') { $fresh.Effective.LogFileName } else { $state.Desired.LogFileName }
$access = Get-WelaFirewallLogAccess -Path $targetPath
if ($access.State -ne 'VerifiedExplicitGrant') { throw "Log path access is $($access.State): $($access.Diagnostic)" }
$size = [Math]::Max([double]$state.Desired.MinimumSizeKiB, [Math]::Max([double]$fresh.Effective.LogMaxSizeKilobytes, [double]$fresh.Local.LogMaxSizeKilobytes))
$parameters = @{ Name = $state.Name; PolicyStore = 'PersistentStore'; LogAllowed = 'True'; LogBlocked = 'True'; LogMaxSizeKilobytes = [uint64]$size; ErrorAction = 'Stop' }
+29 -1
View File
@@ -21,6 +21,7 @@ function Reset-Mocks {
$script:writes = 0; $script:writeArguments = @(); $script:blockedAccess = $false
$script:failRead = $false; $script:failWrite = $false; $script:gpo = $false; $script:onPrompt = $null
$script:accessCalls = 0
$script:unverifiedPaths = @()
}
function Get-NetFirewallProfile {
param($Name, $PolicyStore, $ErrorAction)
@@ -47,7 +48,7 @@ function Set-NetFirewallProfile {
function Get-WelaFirewallLogAccess {
param($Path)
$script:accessCalls++
[pscustomobject]@{ State = $(if ($script:blockedAccess) { 'Unknown' } else { 'VerifiedExplicitGrant' }); Path = $Path; Diagnostic = 'Mock service ACL'; ServiceAccount = 'NT AUTHORITY\LocalService'; ServiceStatus = 'Running' }
[pscustomobject]@{ State = $(if ($script:blockedAccess -or $script:unverifiedPaths -contains $Path) { 'Unknown' } else { 'VerifiedExplicitGrant' }); Path = $Path; Diagnostic = 'Mock service ACL'; ServiceAccount = 'NT AUTHORITY\LocalService'; ServiceStatus = 'Running' }
}
function Read-Host {
param($Prompt)
@@ -146,6 +147,33 @@ try {
Set-WelaFirewallLoggingControls $context @((Get-WelaFirewallLoggingPlan)[0])
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Operator changes during prompt prevent stale recovery snapshot writes'
foreach ($unverified in @($true, $false)) {
Reset-Mocks
$plan = @((Get-WelaFirewallLoggingPlan)[0])
$newPath = 'C:\ChangedAfterPlan\Domain.log'
$script:effectiveProfiles.Domain.LogFileName = $newPath
$script:localProfiles.Domain.LogFileName = $newPath
if ($unverified) { $script:unverifiedPaths = @($newPath) }
$context = New-TestContext
Set-WelaFirewallLoggingControls $context $plan
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') "Preserve mode refuses a path changed between planning and initial read (unverified=$unverified)"
Assert ($context.Results[0].Diagnostic -match 'changed after planning') 'Stale planned path produces an actionable failure'
Assert (-not (Test-Path -LiteralPath (Join-Path $context.BackupPath 'before.jsonl'))) 'Path drift is refused before creating a mutation journal entry'
}
Reset-Mocks
$plan = @((Get-WelaFirewallLoggingPlan)[0])
$script:onPrompt = { $script:unverifiedPaths = @($script:effectiveProfiles.Domain.LogFileName) }
$context = New-TestContext -Prompt
Set-WelaFirewallLoggingControls $context $plan
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Newly unknown actual destination permissions prevent a write after confirmation'
Reset-Mocks
$script:unverifiedPaths = @($script:effectiveProfiles.Domain.LogFileName)
$context = New-TestContext
Set-WelaFirewallLoggingControls $context @((Get-WelaFirewallLoggingPlan -PathMode CisV4)[0])
Assert ($script:writes -eq 1 -and $context.Results[0].Status -eq 'Applied') 'Explicit CIS migration can replace an unverified old destination with a verified new destination'
Reset-Mocks
$plan = @(Get-WelaFirewallLoggingPlan)
$script:blockedAccess = $true