mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-11-09 22:49:58 +01:00
Reject firewall log path drift before configuration
This commit is contained in:
1 parent
84f263ed1f
commit
a10e1d6e84
3 files changed
+42
-4
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user