diff --git a/.github/workflows/firewall-logging.yml b/.github/workflows/firewall-logging.yml new file mode 100644 index 00000000..c2b3218a --- /dev/null +++ b/.github/workflows/firewall-logging.yml @@ -0,0 +1,31 @@ +name: Firewall text logging regressions +on: + push: + branches: ['**'] + paths: + - 'WELA.ps1' + - 'scripts/Configuration.ps1' + - 'scripts/FirewallLogging.ps1' + - 'tests/FirewallLogging*' + - '.github/workflows/firewall-logging.yml' + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + firewall-logging: + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Mocked logging regressions in Windows PowerShell 5.1 + shell: powershell + run: ./tests/FirewallLogging.Tests.ps1 + - name: Actual Windows read-only smoke in Windows PowerShell 5.1 + shell: powershell + run: ./tests/FirewallLogging.Windows.Tests.ps1 + - name: Mocked logging regressions in PowerShell 7 + shell: pwsh + run: ./tests/FirewallLogging.Tests.ps1 + - name: Actual Windows read-only smoke in PowerShell 7 + shell: pwsh + run: ./tests/FirewallLogging.Windows.Tests.ps1 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 47edb38e..099eeffb 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,7 @@ **改善:** +- ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (issue #375) (@Shirofune-Security) - `audit-settings`、`plan`、`configure`で共有するバージョン付きの詳細監査ポリシープロファイルを追加した。59のサブカテゴリと14のプロファイルで、WELA、文書に基づくWindows既定値、Microsoft、確認済みのCIS v4.0.0、ASDのWindows標準機能向け監査ガイドに対応する。ホストの役割とビルドの検証、オフラインでの設定計画、出典と前提条件を含むJSON出力をサポートする。完全一致、最低限、任意、変更なし、未構成、適用対象外を区別する。Windows既定値は参照専用で、プロファイルの対象はSecurityログの詳細監査ポリシーに限定される。 (#390) (@Shirofune-Security) - WELAのプロファイルにWindows標準の監査サブカテゴリを6つ追加した。Group MembershipとAuthorization Policy Changeは成功、Application Group Management、MPSSVC Rule-Level Policy Change、IPsec Driver、Kernel Objectは成功と失敗を監査する。各ガイドに対応するプロファイルでは、それぞれの監査設定と前提条件を維持する。Kernel Objectのイベント生成には対象オブジェクトに適切なSACLが必要であり、この変更ではそのSACLを作成しない。 (#391) (@Shirofune-Security) - `configure`と`configure -Profile`に`-DryRun`と`-ResultsPath`を追加し、Windows設定を変更せずに変更内容を確認し、設定項目ごとの結果をJSONで出力できるようにした。`-DryRun`に対応していないコマンドは、実行前にエラーで停止する。 (#392) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index a61df55f..357ca124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Improvements:** +- Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (issue #375) (@Shirofune-Security) - Added versioned advanced audit-policy profiles shared by `audit-settings`, `plan` and `configure`: 59 subcategories and 14 profiles covering WELA, documented Windows defaults, Microsoft, reviewed CIS v4.0.0 and ASD native guidance. Profiles support role/build validation, offline planning and JSON exports with sources and prerequisites. Exact, minimum, optional, unchanged, Not Configured and not-applicable settings remain distinct. Windows defaults are reference-only; profile scope is advanced Security audit policy. (#390) (@Shirofune-Security) - Added six native Windows audit subcategories to WELA's profile: Group Membership and Authorization Policy Change (Success), plus Application Group Management, MPSSVC Rule-Level Policy Change, IPsec Driver and Kernel Object (Success and Failure). Source-specific profiles retain their own audit settings and prerequisites; Kernel Object events require matching object SACLs, which this change does not create. (#391) (@Shirofune-Security) - Added `-DryRun` and `-ResultsPath` to `configure` and `configure -Profile` to preview changes without modifying Windows settings and export per-control results as JSON. Commands that do not support `-DryRun` reject it before running. (#392) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index 2b78f3df..4dd3164a 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -14,6 +14,9 @@ [switch]$DryRun, [string]$BackupPath, [string]$ResultsPath, + [ValidateSet('Audit', 'Plan', 'Configure')][string]$FirewallAction = 'Audit', + [ValidateSet('Preserve', 'CisV4')][string]$FirewallPathMode = 'Preserve', + [ValidateRange(16384, 32767)][int]$FirewallMinimumSizeKiB = 16384, [switch]$Help ) @@ -28,6 +31,7 @@ $EidMappingPath = Join-Path $ScriptRoot "config/eid_subcategory_mapping.csv" $AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt" $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" . (Join-Path $ScriptRoot "scripts/Configuration.ps1") +. (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1") Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop # 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。 @@ -1751,6 +1755,10 @@ function Get-WelaUserProfiles { $usage = @" Usage: + ./WELA.ps1 firewall-logging -FirewallAction Audit -ResultsPath firewall.json + ./WELA.ps1 firewall-logging -FirewallAction Plan -FirewallPathMode CisV4 + ./WELA.ps1 firewall-logging -FirewallAction Configure -DryRun + # Firewall text logging is opt-in; it does not change firewall enforcement or rules. ./WELA.ps1 profiles # List versioned advanced audit-policy profiles ./WELA.ps1 plan -Profile wela-2.2.0 -Role Client -Build 26100 -PlanPath plan.json ./WELA.ps1 audit-settings -Profile microsoft-sct-win11-24h2 -PlanPath audit.json @@ -1776,8 +1784,8 @@ Write-Host "WELA v$WELAVersion - $WELAReleaseName" Write-Host "" # Reject unsupported dry-run requests before reaching any command's mutation path. -if ($DryRun -and $Cmd -ne 'configure') { - throw "-DryRun is supported only by configure (including configure -Profile). No command was run." +if ($DryRun -and $Cmd -ne 'configure' -and -not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure')) { + throw "-DryRun is supported only by configure (including configure -Profile) and firewall-logging -FirewallAction Configure. No command was run." } if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'configure') -and -not $Help) { @@ -1786,6 +1794,19 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi } switch ($Cmd.ToLower()) { + 'firewall-logging' { + if ($Help) { + Write-Host 'Usage: ./WELA.ps1 firewall-logging [-FirewallAction Audit|Plan|Configure] [-FirewallPathMode Preserve|CisV4] [-FirewallMinimumSizeKiB 16384..32767] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath file.json]' + Write-Host 'Audits/plans Domain, Private and Public text logs. Configure enables allowed/dropped logging, preserves larger sizes and existing paths by default, and verifies effective policy. CisV4 explicitly selects domainfw.log/privatefw.log/publicfw.log. See docs/firewall-logging.md.' + return + } + if ($Profile -or $Baseline) { throw 'firewall-logging uses its own options; -Profile and -Baseline apply to Security audit settings.' } + try { + $report = Invoke-WelaFirewallLoggingCommand -Action $FirewallAction -PathMode $FirewallPathMode -MinimumSizeKiB $FirewallMinimumSizeKiB -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $report + if ($report.ExitCode) { exit $report.ExitCode } + } catch { Write-Host "[Failed] Firewall logging: $_" -ForegroundColor Red; exit 1 } + } "profiles" { (Import-WelaAuditProfiles).profiles | Select-Object id, version, scope, appliesTo | Format-List } diff --git a/docs/firewall-logging.md b/docs/firewall-logging.md new file mode 100644 index 00000000..9d8d04da --- /dev/null +++ b/docs/firewall-logging.md @@ -0,0 +1,67 @@ +# Native Windows Firewall text logging + +`firewall-logging` is a separate, opt-in command for the Domain, Private and Public packet/connection text logs. It changes only `LogAllowed`, `LogBlocked`, `LogMaxSizeKilobytes` and, when explicitly selected, `LogFileName`. It does not enable the firewall, change filtering defaults or rules, start/restart services, create directories, modify ACLs, configure WEF/SIEM ingestion, or install Sysmon. `-Profile` and `-Baseline` are rejected because those options select Security audit policy. No Sigma coverage increase is claimed. + +Run in an elevated Windows PowerShell 5.1 or PowerShell 7 session with the built-in NetSecurity module: + +```powershell +# Read effective and local policy, directory/service prerequisites, and the proposed target. +.\WELA.ps1 firewall-logging -FirewallAction Audit -ResultsPath firewall-audit.json +.\WELA.ps1 firewall-logging -FirewallAction Plan -ResultsPath firewall-plan.json + +# Preview and then apply, retaining existing effective paths and larger size limits. +.\WELA.ps1 firewall-logging -FirewallAction Configure -DryRun -ResultsPath preview.json +.\WELA.ps1 firewall-logging -FirewallAction Configure -Auto -BackupPath .\firewall-before -ResultsPath firewall-results.json + +# Explicit path conformance to the reviewed CIS v4.0.0 client/server benchmarks. +.\WELA.ps1 firewall-logging -FirewallAction Configure -FirewallPathMode CisV4 -Auto + +# Select Microsoft's currently documented higher size recommendation if desired. +.\WELA.ps1 firewall-logging -FirewallAction Configure -FirewallMinimumSizeKiB 20480 -Auto +``` + +Audit and Plan both perform current-host, read-only assessment and include the desired configuration; Plan is not an offline plan or a file that can authorize later writes. `-ResultsPath` is the only output file for these commands. Configure dry run never changes Windows or creates a recovery directory; an explicitly requested results file is still written. An unknown/blocked prerequisite gives exit code 1, including in a dry run. A readable noncompliant Audit/Plan has `ChangeRequired` and exit code 0 (the assessment completed). + +## Policy semantics + +Both allowed and dropped logging switches must be exactly True. The default minimum is **16,384 KiB**, taken from the reviewed CIS Windows 11 Enterprise and Windows Server 2022 **v4.0.0** benchmarks. Larger effective or local limits are retained during writes. `-FirewallMinimumSizeKiB` accepts 16,384 through 32,767. Microsoft's logging guide now recommends at least **20,480 KB**; the default here does not claim to implement that higher recommendation. + +The default `Preserve` path mode never writes `LogFileName`. `CisV4` explicitly selects `%SystemRoot%\System32\LogFiles\Firewall\domainfw.log`, `privatefw.log` and `publicfw.log`. This is exact path conformance, with environment variables expanded for comparison, and minimum size conformance. Microsoft's guide uses different per-profile filenames. Neither mode reduces a larger log size. + +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. + +## Service permissions and operational limits + +The log path must expand to an absolute local drive path. WELA checks the **mpssvc** service account is the documented LocalService account, that the service SID is enabled, and that the service is running. It inspects the destination directory and any existing log file for Modify rights assigned to the **NT SERVICE\mpssvc** SID, including inheritance to newly created files. It rejects reparse points and conservatively reports `Unknown` when deny ACEs, group-only grants, unexpected service identity, or read errors prevent that static check. Missing directories or a stopped service are `Blocked`. Both states prevent configuration. + +WELA does not attempt to broaden ACLs, resolve arbitrary group membership, impersonate the service, or silently provision directories. Have an administrator provision an approved directory and service permissions, then rerun the plan. `VerifiedExplicitGrant` means the conservative static ACL check passed; it is **not** proof of effective token access, file creation, rotation, disk capacity or successful ingestion. The complete service token, filesystem filters and concurrent policy/ACL changes can still affect writes. + +Each snapshot also reports the firewall profile's `Enabled` value. A compliant logging configuration on a disabled/inactive profile is preparation for that profile, not proof of traffic events. WELA never changes that enforcement state. Text logs and Security EVTX audit events are separate sources; increasing an EVTX buffer does not configure these text logs, and a WEF subscription alone does not collect arbitrary text files. + +## Manual recovery + +There is no automatic rollback. Preserve `before.jsonl` and the results JSON. Before recovery, review failed versus applied controls, concurrent operator changes and GPO/MDM ownership. Restore the **local** snapshot, not the effective snapshot; applied policy may continue overriding it. Example for one reviewed journal entry: + +```powershell +$entries = @(Get-Content -LiteralPath .\firewall-before\before.jsonl | ConvertFrom-Json) +$entry = $entries | Where-Object { $_.Kind -eq 'FirewallTextLog' -and $_.Target.Name -eq 'Domain' } | Select-Object -First 1 +if (-not $entry) { throw 'No Domain firewall recovery entry found' } +$old = $entry.Before.Local +Set-NetFirewallProfile -Name $entry.Target.Name -PolicyStore PersistentStore ` + -LogAllowed $old.LogAllowed -LogBlocked $old.LogBlocked ` + -LogMaxSizeKilobytes $old.LogMaxSizeKilobytes -LogFileName $old.LogFileName -ErrorAction Stop +Get-NetFirewallProfile -Name $entry.Target.Name -PolicyStore PersistentStore +Get-NetFirewallProfile -Name $entry.Target.Name -PolicyStore ActiveStore +``` + +Do not blindly replay a journal: a failed write can have left the old state untouched, and a later administrator change may be intentional. Restore other profiles individually after the same review. No enforcement or ACL restoration is needed because this command does not change them. + +## Validation and remaining integration evidence + +The automated suite uses mocked firewall writes and temporary recovery files to check all profiles, larger limits, path preservation/CIS selection, effective-versus-local conflicts, idempotence, journal ordering, unknown permissions, read/write errors, prompt races and final drift. Windows CI runs these checks under PowerShell 5.1 and 7, plus actual read-only ActiveStore/PersistentStore and ACL inspection and a dry run. It does not alter runner firewall policy or generate traffic. + +Before closing issue #375, capture evidence from an isolated Windows client/server lab: OS build, PowerShell version, WELA commit, before/after JSON, effective/local settings and service ACLs. On each applicable active network profile, generate one benign allowed connection and one controlled blocked connection against a disposable endpoint, confirm corresponding `ALLOW`/`DROP` text records and timestamps, and confirm the expected source path and parser in the actual collector. Test log creation and rotation under the actual service token, policy refresh/override behavior, and manual recovery. Do not weaken production filtering to create this evidence. These traffic/rotation/ingestion tests remain unperformed; no end-to-end detection claim is made. + +Sources: [Microsoft firewall logging configuration](https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/configure-logging), [Get-NetFirewallProfile policy stores](https://learn.microsoft.com/en-us/powershell/module/netsecurity/get-netfirewallprofile?view=windowsserver2025-ps), [Set-NetFirewallProfile logging parameters](https://learn.microsoft.com/en-us/powershell/module/netsecurity/set-netfirewallprofile?view=windowsserver2025-ps), and the version-pinned CIS references in [issue #375](https://github.com/Yamato-Security/WELA/issues/375). diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 index d8baddde..25a97520 100644 --- a/scripts/Configuration.ps1 +++ b/scripts/Configuration.ps1 @@ -94,7 +94,7 @@ function Invoke-WelaConfigurationControl { function Complete-WelaConfiguration { param($Context, [string]$ResultsPath, $Plan, - [ValidateSet("native-windows-configuration", "advanced-audit-policy-only")] + [ValidateSet("native-windows-configuration", "advanced-audit-policy-only", "firewall-text-logging-only")] [string]$Scope = "native-windows-configuration") # A second read detects a value that was compliant earlier but changed during # this run. It does not establish whether GPO or another writer caused drift. diff --git a/scripts/FirewallLogging.ps1 b/scripts/FirewallLogging.ps1 new file mode 100644 index 00000000..f986d283 --- /dev/null +++ b/scripts/FirewallLogging.ps1 @@ -0,0 +1,189 @@ +# Built-in firewall text logs only. Dot-sourced beside Configuration.ps1 (PowerShell 5.1+). +function ConvertTo-WelaFirewallLoggingSnapshot { + param($Profile) + foreach ($property in @('Name', 'LogAllowed', 'LogBlocked', 'LogMaxSizeKilobytes', 'LogFileName')) { + if ($null -eq $Profile.$property) { throw "Firewall profile is missing $property." } + } + if ([string]$Profile.LogAllowed -notin @('True', 'False', 'NotConfigured') -or + [string]$Profile.LogBlocked -notin @('True', 'False', 'NotConfigured')) { + throw 'Unrecognized firewall logging switch value.' + } + $size = [uint64]$Profile.LogMaxSizeKilobytes + [pscustomobject][ordered]@{ + Name = [string]$Profile.Name; LogAllowed = [string]$Profile.LogAllowed + LogBlocked = [string]$Profile.LogBlocked; LogMaxSizeKilobytes = $size + LogFileName = [string]$Profile.LogFileName; Enabled = [string]$Profile.Enabled + } +} + +function Test-WelaFirewallServiceAcl { + param([array]$Rules, [string]$ServiceSid, [switch]$Directory) + # Group deny ACEs cannot be discounted without the complete service token. + if (@($Rules | Where-Object AccessControlType -eq Deny).Count) { return $false } + $required = [int][Security.AccessControl.FileSystemRights]::Modify + $selfRights = 0; $childRights = 0 + foreach ($rule in $Rules) { + if ($rule.IdentityReference.Value -ne $ServiceSid -or $rule.AccessControlType -ne 'Allow') { continue } + if (-not ($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly)) { + $selfRights = $selfRights -bor [int]$rule.FileSystemRights + } + if ($rule.InheritanceFlags -band [Security.AccessControl.InheritanceFlags]::ObjectInherit) { + $childRights = $childRights -bor [int]$rule.FileSystemRights + } + } + return ($selfRights -band $required) -eq $required -and (-not $Directory -or ($childRights -band $required) -eq $required) +} + +function Get-WelaFirewallLogAccess { + param([string]$Path) + $result = [pscustomobject]@{ State = 'Unknown'; Path = $Path; Service = 'mpssvc'; ServiceAccount = $null; ServiceStatus = $null; Diagnostic = '' } + try { + $expanded = [Environment]::ExpandEnvironmentVariables($Path) + if ($expanded -notmatch '^[A-Za-z]:\\' -or $expanded -match '%' -or $expanded.Substring(2).Contains(':')) { + throw 'Log path must resolve to an absolute local drive path without environment placeholders or alternate data streams.' + } + $result.Path = $expanded + $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='mpssvc'" -ErrorAction Stop + if (-not $service) { throw 'Firewall service could not be read.' } + $result.ServiceAccount = [string]$service.StartName + $result.ServiceStatus = [string]$service.State + if ($service.StartName -notin @('NT AUTHORITY\LocalService', 'NT AUTHORITY\Local Service')) { + throw 'Firewall service account differs from the documented LocalService configuration; effective token access is unknown.' + } + $sidType = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Services\mpssvc' -Name ServiceSidType -ErrorAction Stop).ServiceSidType + if ($sidType -notin @(1, 3)) { throw 'Firewall service SID is not enabled; effective token access is unknown.' } + $sid = (New-Object Security.Principal.NTAccount('NT SERVICE', 'mpssvc')).Translate([Security.Principal.SecurityIdentifier]).Value + $parent = Split-Path -Path $expanded -Parent + if (-not (Test-Path -LiteralPath $parent -PathType Container -ErrorAction Stop)) { + $result.State = 'Blocked'; throw 'Log directory is missing. Provision its service permissions explicitly before configuring logging.' + } + # Never follow a directory junction to an unreviewed destination. + $ancestor = $parent + while ($ancestor) { + $item = Get-Item -LiteralPath $ancestor -Force -ErrorAction Stop + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Reparse point prevents a conservative ACL check: $ancestor" } + $next = Split-Path -Path $ancestor -Parent + if ($next -eq $ancestor) { break } + $ancestor = $next + } + $paths = @([pscustomobject]@{ Path = $parent; Directory = $true }) + if (Test-Path -LiteralPath $expanded -ErrorAction Stop) { + $file = Get-Item -LiteralPath $expanded -Force -ErrorAction Stop + if ($file.PSIsContainer -or ($file.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Log target is a directory or reparse point.' } + $paths += [pscustomobject]@{ Path = $expanded; Directory = $false } + } + foreach ($target in $paths) { + $acl = Get-Acl -LiteralPath $target.Path -ErrorAction Stop + $rules = @($acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) + if (-not (Test-WelaFirewallServiceAcl -Rules $rules -ServiceSid $sid -Directory:$target.Directory)) { + throw "No unambiguous mpssvc Modify grant at $($target.Path), including directory file inheritance. Deny ACEs or group-based access require effective-token review." + } + } + if ($service.State -ne 'Running') { $result.State = 'Blocked'; throw 'Firewall service is not running. WELA will not start it or change firewall enforcement.' } + $result.State = 'VerifiedExplicitGrant' + $result.Diagnostic = 'Static ACL check found mpssvc Modify rights on the directory and existing file, including file inheritance. Actual log creation and rotation still require traffic validation.' + } catch { $result.Diagnostic = $_.Exception.Message } + return $result +} + +function Get-WelaFirewallLoggingState { + param([ValidateSet('Domain', 'Private', 'Public')][string]$Name) + $effectiveProfiles = @(Get-NetFirewallProfile -Name $Name -PolicyStore ActiveStore -ErrorAction Stop) + $localProfiles = @(Get-NetFirewallProfile -Name $Name -PolicyStore PersistentStore -ErrorAction Stop) + if ($effectiveProfiles.Count -ne 1 -or $localProfiles.Count -ne 1 -or $effectiveProfiles[0].Name -ne $Name -or $localProfiles[0].Name -ne $Name) { + throw "Expected exactly one $Name profile from each firewall policy store." + } + $effective = ConvertTo-WelaFirewallLoggingSnapshot $effectiveProfiles[0] + $local = ConvertTo-WelaFirewallLoggingSnapshot $localProfiles[0] + $differences = @('LogAllowed', 'LogBlocked', 'LogMaxSizeKilobytes', 'LogFileName' | Where-Object { $effective.$_ -ne $local.$_ }) + [pscustomobject]@{ + Effective = $effective; Local = $local; DifferentFromLocal = $differences + PolicySource = 'ActiveStore is resultant policy; PersistentStore is local policy. Differences can reflect GPO/MDM. The current policy writer and future persistence are not established.' + Access = Get-WelaFirewallLogAccess -Path $effective.LogFileName + } +} + +function Get-WelaFirewallLoggingPlan { + param([ValidateSet('Preserve', 'CisV4')][string]$PathMode = 'Preserve', + [ValidateRange(16384, 32767)][int]$MinimumSizeKiB = 16384) + foreach ($name in @('Domain', 'Private', 'Public')) { + $before = $null; $desired = $null + try { + $before = Get-WelaFirewallLoggingState -Name $name + $path = if ($PathMode -eq 'CisV4') { '%SystemRoot%\System32\LogFiles\Firewall\' + $name.ToLowerInvariant() + 'fw.log' } else { $before.Effective.LogFileName } + $desired = [pscustomobject]@{ LogAllowed = 'True'; LogBlocked = 'True'; MinimumSizeKiB = $MinimumSizeKiB; LogFileName = $path; PathMode = $PathMode } + $access = if ($path -eq $before.Effective.LogFileName) { $before.Access } else { Get-WelaFirewallLogAccess -Path $path } + $status = if ($access.State -ne 'VerifiedExplicitGrant') { $access.State } + elseif (Test-WelaFirewallLoggingCompliance -Snapshot $before -Desired $desired) { 'Compliant' } else { 'ChangeRequired' } + [pscustomobject]@{ Name = $name; Status = $status; Before = $before; Desired = $desired; TargetAccess = $access; Diagnostic = $access.Diagnostic } + } catch { + [pscustomobject]@{ Name = $name; Status = 'Unknown'; Before = $before; Desired = $desired; TargetAccess = $null; Diagnostic = $_.Exception.Message } + } + } +} + +function Test-WelaFirewallLoggingCompliance { + param($Snapshot, $Desired) + $pathMatches = [Environment]::ExpandEnvironmentVariables($Snapshot.Effective.LogFileName) -eq [Environment]::ExpandEnvironmentVariables($Desired.LogFileName) + return $Snapshot.Access.State -eq 'VerifiedExplicitGrant' -and $Snapshot.Effective.LogAllowed -eq 'True' -and + $Snapshot.Effective.LogBlocked -eq 'True' -and $Snapshot.Effective.LogMaxSizeKilobytes -ge $Desired.MinimumSizeKiB -and $pathMatches +} + +function Set-WelaFirewallLoggingControls { + param($Context, [array]$Plan) + foreach ($entry in $Plan) { + $id = "FirewallTextLog/$($entry.Name)" + if ($entry.Status -in @('Unknown', 'Blocked')) { + $Context.Results.Add([pscustomobject]@{ Id = $id; Kind = 'FirewallTextLog'; Target = $entry.Name; Desired = $entry.Desired; Before = $entry.Before; After = $null; Status = 'Failed'; Diagnostic = "Logging prerequisite $($entry.Status): $($entry.Diagnostic)" }) + continue + } + $callback = @{ Name = $entry.Name; Desired = $entry.Desired; Observed = $null } + $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 ($targetAccess.State -ne 'VerifiedExplicitGrant') { throw "Log path access is $($targetAccess.State): $($targetAccess.Diagnostic)" } + $state.Observed = $snapshot + return $snapshot + } + $test = { param($snapshot, $state) Test-WelaFirewallLoggingCompliance -Snapshot $snapshot -Desired $state.Desired } + $apply = { + param($state) + # Refuse races after a prompt/journal rather than overwrite an operator's changes. + $fresh = Get-WelaFirewallLoggingState -Name $state.Name + foreach ($store in @('Effective', 'Local')) { + foreach ($property in @('LogAllowed', 'LogBlocked', 'LogMaxSizeKilobytes', 'LogFileName')) { + 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 ($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' } + if ($state.Desired.PathMode -eq 'CisV4') { $parameters.LogFileName = $state.Desired.LogFileName } + Set-NetFirewallProfile @parameters + 'Local logging settings were written. Effective ActiveStore read-back follows; GPO/MDM may override local values now or later.' + } + Invoke-WelaConfigurationControl -Context $Context -Id $id -Kind FirewallTextLog -Target @{ Name = $entry.Name; PolicyStore = 'PersistentStore' } ` + -Desired $entry.Desired -Read $read -Compliant $test -Apply $apply -CallbackState $callback ` + -Description 'Enable allowed/dropped text logging and its minimum size; preserve enforcement and rules.' + } +} + +function Invoke-WelaFirewallLoggingCommand { + param([ValidateSet('Audit', 'Plan', 'Configure')][string]$Action = 'Audit', + [ValidateSet('Preserve', 'CisV4')][string]$PathMode = 'Preserve', + [ValidateRange(16384, 32767)][int]$MinimumSizeKiB = 16384, + [switch]$Auto, [switch]$DryRun, [string]$BackupPath, [string]$ResultsPath) + if ($env:OS -ne 'Windows_NT') { throw 'Firewall text logging requires Windows and the NetSecurity module.' } + if ($DryRun -and $Action -ne 'Configure') { throw '-DryRun applies only to FirewallAction Configure; Audit and Plan are read-only.' } + $plan = @(Get-WelaFirewallLoggingPlan -PathMode $PathMode -MinimumSizeKiB $MinimumSizeKiB) + if ($Action -eq 'Configure') { + $context = New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath + Set-WelaFirewallLoggingControls -Context $context -Plan $plan + return Complete-WelaConfiguration -Context $context -ResultsPath $ResultsPath -Scope 'firewall-text-logging-only' + } + $report = [pscustomobject]@{ Scope = 'firewall-text-logging-only'; Action = $Action; PathMode = $PathMode; MinimumSizeKiB = $MinimumSizeKiB; Profiles = $plan; ExitCode = $(if (@($plan | Where-Object Status -in @('Unknown', 'Blocked')).Count) { 1 } else { 0 }) } + if ($ResultsPath) { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } + return $report +} diff --git a/tests/FirewallLogging.Tests.ps1 b/tests/FirewallLogging.Tests.ps1 new file mode 100644 index 00000000..e9114a90 --- /dev/null +++ b/tests/FirewallLogging.Tests.ps1 @@ -0,0 +1,183 @@ +$ErrorActionPreference = 'Stop' +$repo = Split-Path $PSScriptRoot -Parent +$script:ScriptRoot = $repo +. (Join-Path $repo 'scripts/Configuration.ps1') +. (Join-Path $repo 'scripts/FirewallLogging.ps1') +$script:assertions = 0 +$script:paths = @() +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:assertions++ +} +function New-Entry($Name, $Size = 4096) { + [pscustomobject]@{ Name = $Name; LogAllowed = 'False'; LogBlocked = 'False'; LogMaxSizeKilobytes = $Size; LogFileName = "C:\Operator\$Name.log"; Enabled = 'False' } +} +function Reset-Mocks { + $script:localProfiles = @{}; $script:effectiveProfiles = @{} + foreach ($name in @('Domain', 'Private', 'Public')) { + $script:localProfiles[$name] = New-Entry $name + $script:effectiveProfiles[$name] = New-Entry $name + } + $script:writes = 0; $script:writeArguments = @(); $script:blockedAccess = $false + $script:failRead = $false; $script:failWrite = $false; $script:gpo = $false; $script:onPrompt = $null + $script:accessCalls = 0 +} +function Get-NetFirewallProfile { + param($Name, $PolicyStore, $ErrorAction) + if ($script:failRead) { throw 'Read denied' } + if ($PolicyStore -eq 'ActiveStore') { return $script:effectiveProfiles[$Name] } + if ($PolicyStore -eq 'PersistentStore') { return $script:localProfiles[$Name] } + throw "Unexpected store $PolicyStore" +} +function Set-NetFirewallProfile { + param($Name, $PolicyStore, $LogAllowed, $LogBlocked, $LogMaxSizeKilobytes, $LogFileName, $ErrorAction) + Assert ($PolicyStore -eq 'PersistentStore') 'Only local persistent policy is written' + $entries = @(Get-Content -LiteralPath (Join-Path $script:context.BackupPath 'before.jsonl') | ConvertFrom-Json) + Assert ($entries[-1].Target.Name -eq $Name) 'Matching recovery snapshot exists before each write' + Assert ($entries[-1].Before.Local.Name -eq $Name -and $entries[-1].Before.Effective.Name -eq $Name) 'Journal includes local and effective snapshots' + if ($script:failWrite) { throw 'Mock policy write failed' } + $script:writes++ + $script:writeArguments += $PSBoundParameters + foreach ($store in @($script:localProfiles, $script:effectiveProfiles)) { + if ($script:gpo -and [object]::ReferenceEquals($store, $script:effectiveProfiles)) { continue } + $store[$Name].LogAllowed = $LogAllowed; $store[$Name].LogBlocked = $LogBlocked; $store[$Name].LogMaxSizeKilobytes = $LogMaxSizeKilobytes + if ($PSBoundParameters.ContainsKey('LogFileName')) { $store[$Name].LogFileName = $LogFileName } + } +} +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' } +} +function Read-Host { + param($Prompt) + if ($script:onPrompt) { & $script:onPrompt } + 'Y' +} +function New-TestContext([switch]$DryRun, [switch]$Prompt) { + $path = Join-Path ([IO.Path]::GetTempPath()) ('wela-firewall-' + [guid]::NewGuid().ToString('N')) + $script:paths += $path + $script:context = New-WelaConfigurationContext -Auto:(-not $Prompt) -DryRun:$DryRun -BackupPath $path + return $script:context +} +function New-Ace($Sid, $Rights, $Inheritance = 'ObjectInherit', $Propagation = 'None', $Type = 'Allow') { + [pscustomobject]@{ IdentityReference = [pscustomobject]@{ Value = $Sid }; FileSystemRights = [Security.AccessControl.FileSystemRights]$Rights; InheritanceFlags = [Security.AccessControl.InheritanceFlags]$Inheritance; PropagationFlags = [Security.AccessControl.PropagationFlags]$Propagation; AccessControlType = $Type } +} +try { + $sid = 'S-1-5-80-123' + $good = New-Ace $sid 'Modify' + Assert (Test-WelaFirewallServiceAcl @($good) $sid -Directory) 'Service Modify grant covers directory creation/rotation and inherited files' + Assert (-not (Test-WelaFirewallServiceAcl @((New-Ace $sid 'Read')) $sid -Directory)) 'Read-only service permission is unknown' + Assert (-not (Test-WelaFirewallServiceAcl @((New-Ace 'S-1-1-0' 'FullControl')) $sid -Directory)) 'Broad group grant is not mistaken for proven service-token access' + Assert (-not (Test-WelaFirewallServiceAcl @((New-Ace $sid 'Modify' 'None')) $sid -Directory)) 'New log files require inheritable service permission' + Assert (Test-WelaFirewallServiceAcl @((New-Ace $sid 'Modify' 'None')) $sid) 'Existing file needs self access, not inheritance' + Assert (-not (Test-WelaFirewallServiceAcl @((New-Ace $sid 'Modify' 'ObjectInherit' 'InheritOnly')) $sid -Directory)) 'Inherit-only grant does not permit directory access' + Assert (-not (Test-WelaFirewallServiceAcl @($good, (New-Ace 'S-1-1-0' 'Write' 'None' 'None' 'Deny')) $sid -Directory)) 'Any unresolved group deny prevents claiming access' + + Reset-Mocks + $plan = @(Get-WelaFirewallLoggingPlan) + Assert ($plan.Count -eq 3 -and ($plan.Name -join ',') -eq 'Domain,Private,Public') 'All profiles are planned' + Assert (@($plan | Where-Object Status -eq ChangeRequired).Count -eq 3) 'Existing logging gaps are explicit' + Assert ($plan[0].Desired.MinimumSizeKiB -eq 16384 -and $plan[0].Desired.LogFileName -eq 'C:\Operator\Domain.log') 'Default plan retains operator path and minimum semantics' + $context = New-TestContext -DryRun + Set-WelaFirewallLoggingControls $context $plan + Assert ($script:writes -eq 0 -and $context.Results.Count -eq 3) 'Dry run does not change any profile' + Assert (-not (Test-Path -LiteralPath $context.BackupPath)) 'Dry run creates no recovery directory' + + Reset-Mocks + $script:localProfiles.Domain.LogMaxSizeKilobytes = 24576 + $script:effectiveProfiles.Domain.LogMaxSizeKilobytes = 20480 + $context = New-TestContext + Set-WelaFirewallLoggingControls $context @(Get-WelaFirewallLoggingPlan) + $result = Complete-WelaConfiguration $context -Scope firewall-text-logging-only + Assert ($result.ExitCode -eq 0 -and $script:writes -eq 3) 'All three profiles are verified after writing' + Assert ($script:effectiveProfiles.Domain.LogMaxSizeKilobytes -eq 24576) 'Larger local and effective limits are preserved' + Assert ($script:effectiveProfiles.Private.LogMaxSizeKilobytes -eq 16384) 'Smaller limit is raised to minimum' + Assert ($script:effectiveProfiles.Domain.Enabled -eq 'False') 'Disabled firewall enforcement remains untouched' + Assert (@($script:writeArguments | Where-Object { $_.ContainsKey('LogFileName') }).Count -eq 0) 'Default configuration never writes path parameter' + $context = New-TestContext + Set-WelaFirewallLoggingControls $context @(Get-WelaFirewallLoggingPlan) + Assert ($script:writes -eq 3 -and @($context.Results | Where-Object Status -eq AlreadyCompliant).Count -eq 3) 'Verified settings are idempotent' + $script:effectiveProfiles.Public.LogAllowed = 'False' + Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1 -and $context.Results[2].Status -eq 'Overridden') 'Final effective-policy drift fails the run' + + Reset-Mocks + $context = New-TestContext + Set-WelaFirewallLoggingControls $context @(Get-WelaFirewallLoggingPlan -PathMode CisV4 -MinimumSizeKiB 20480) + Assert ($script:writes -eq 3) 'Explicit CIS path configuration writes all profiles' + foreach ($name in @('Domain', 'Private', 'Public')) { + Assert ($script:effectiveProfiles[$name].LogFileName -eq ('%SystemRoot%\System32\LogFiles\Firewall\' + $name.ToLowerInvariant() + 'fw.log')) 'Explicit CIS path is distinct for each profile' + } + Assert ($script:effectiveProfiles.Public.LogMaxSizeKilobytes -eq 20480) 'Operator can choose the higher Microsoft size recommendation' + + Reset-Mocks + $script:gpo = $true + $context = New-TestContext + Set-WelaFirewallLoggingControls $context @(Get-WelaFirewallLoggingPlan) + $result = Complete-WelaConfiguration $context + Assert ($result.ExitCode -eq 1 -and $result.Failed -eq 3) 'Local success with ineffective GPO-overridden settings is failure' + Assert ($context.Results[0].After.DifferentFromLocal -contains 'LogAllowed') 'Results expose effective versus local differences' + + Reset-Mocks + $script:blockedAccess = $true + $plan = @(Get-WelaFirewallLoggingPlan) + Assert (@($plan | Where-Object Status -eq Unknown).Count -eq 3) 'Unverified service access is Unknown' + $context = New-TestContext + Set-WelaFirewallLoggingControls $context $plan + Assert ($script:writes -eq 0 -and (Complete-WelaConfiguration $context).Failed -eq 3) 'Unknown directory permissions block writes without broadening ACLs' + + Reset-Mocks + $script:failRead = $true + $plan = @(Get-WelaFirewallLoggingPlan) + Assert (@($plan | Where-Object Status -eq Unknown).Count -eq 3) 'Read failure never becomes a configured/default value' + $context = New-TestContext -DryRun + Set-WelaFirewallLoggingControls $context $plan + Assert ($script:writes -eq 0 -and (Complete-WelaConfiguration $context).ExitCode -eq 1) 'Unreadable dry run still reports failure' + + Reset-Mocks + $script:failWrite = $true + $context = New-TestContext + Set-WelaFirewallLoggingControls $context @(Get-WelaFirewallLoggingPlan) + Assert ((Complete-WelaConfiguration $context).Failed -eq 3) 'Write failures are aggregated and controls continue' + + Reset-Mocks + $script:onPrompt = { $script:localProfiles.Domain.LogFileName = 'C:\NewOperator\Domain.log' } + $context = New-TestContext -Prompt + 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' + + Reset-Mocks + $plan = @(Get-WelaFirewallLoggingPlan) + $script:blockedAccess = $true + $context = New-TestContext -DryRun + Set-WelaFirewallLoggingControls $context $plan + Assert ($script:writes -eq 0 -and (Complete-WelaConfiguration $context).Failed -eq 3) 'Permission changes after planning fail even a dry run' + + Reset-Mocks + $savedOS = $env:OS + $outputDirectory = Join-Path ([IO.Path]::GetTempPath()) ('wela-firewall-report-' + [guid]::NewGuid().ToString('N')) + $script:paths += $outputDirectory + $null = New-Item -ItemType Directory -Path $outputDirectory + try { + $env:OS = 'Windows_NT' + $json = Join-Path $outputDirectory 'plan.json' + $report = Invoke-WelaFirewallLoggingCommand -Action Plan -ResultsPath $json + $saved = Get-Content -LiteralPath $json -Raw | ConvertFrom-Json + Assert ($report.ExitCode -eq 0 -and $saved.Profiles.Count -eq 3 -and $saved.Scope -eq 'firewall-text-logging-only') 'Public plan entrypoint exports all profiles with an explicit scope' + $dryPath = Join-Path $outputDirectory 'must-not-exist' + $report = Invoke-WelaFirewallLoggingCommand -Action Configure -DryRun -BackupPath $dryPath -ResultsPath $json + Assert ($report.DryRun -and $report.ExitCode -eq 0 -and $script:writes -eq 0 -and -not (Test-Path $dryPath)) 'Public configure entrypoint propagates dry run and results scope' + $rejected = $false + try { Invoke-WelaFirewallLoggingCommand -Action Audit -DryRun } catch { $rejected = $true } + Assert $rejected 'Public entrypoint rejects meaningless audit dry-run combinations' + } finally { $env:OS = $savedOS } + + # Inspect the only writer's AST: no enforcement/rule parameter or ACL mutation may be hidden in a splat. + $tokens = $null; $errors = $null + $ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'scripts/FirewallLogging.ps1'), [ref]$tokens, [ref]$errors) + Assert ($errors.Count -eq 0) 'Firewall helper parses' + Assert (-not ($ast.Extent.Text -match '(?m)^\s*(Set-Acl|New-NetFirewallRule|Set-NetFirewallRule|Start-Service|Restart-Service)\b')) 'Implementation has no ACL/service/enforcement mutator' + Write-Host "PASS: $script:assertions firewall text logging assertions (mocked; no Windows policy changes)." +} finally { + foreach ($path in $script:paths) { if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force } } +} diff --git a/tests/FirewallLogging.Windows.Tests.ps1 b/tests/FirewallLogging.Windows.Tests.ps1 new file mode 100644 index 00000000..e542a305 --- /dev/null +++ b/tests/FirewallLogging.Windows.Tests.ps1 @@ -0,0 +1,24 @@ +# Read-only Windows smoke: no traffic generation, profile changes, ACL writes, or service operations. +$ErrorActionPreference = 'Stop' +if ($env:OS -ne 'Windows_NT') { Write-Host 'SKIP: Windows only'; return } +$repo = Split-Path $PSScriptRoot -Parent +. (Join-Path $repo 'scripts/Configuration.ps1') +. (Join-Path $repo 'scripts/FirewallLogging.ps1') +$before = @(Get-NetFirewallProfile -PolicyStore ActiveStore | Select-Object Name, Enabled, DefaultInboundAction, DefaultOutboundAction, LogAllowed, LogBlocked, LogMaxSizeKilobytes, LogFileName) | ConvertTo-Json -Depth 4 +$plan = @(Get-WelaFirewallLoggingPlan) +if ($plan.Count -ne 3) { throw 'Expected all three firewall profiles.' } +foreach ($entry in $plan) { + if (-not $entry.Before -or $entry.Before.Effective.Name -ne $entry.Name -or $entry.Before.Local.Name -ne $entry.Name) { + throw "Could not read actual effective/local profile $($entry.Name): $($entry.Diagnostic)" + } + if ($entry.TargetAccess.State -notin @('VerifiedExplicitGrant', 'Unknown', 'Blocked')) { throw 'Unexpected ACL observation.' } + Write-Host "$($entry.Name): $($entry.Status); effective size $($entry.Before.Effective.LogMaxSizeKilobytes) KiB; service access $($entry.TargetAccess.State): $($entry.TargetAccess.Diagnostic)" +} +$path = Join-Path ([IO.Path]::GetTempPath()) ('wela-firewall-readonly-' + [guid]::NewGuid().ToString('N')) +$context = New-WelaConfigurationContext -DryRun -Auto -BackupPath $path +Set-WelaFirewallLoggingControls -Context $context -Plan $plan +if (Test-Path -LiteralPath $path) { throw 'Dry run unexpectedly created a recovery directory.' } +if (@($context.Results | Where-Object Status -eq Applied).Count) { throw 'Dry run unexpectedly applied a control.' } +$after = @(Get-NetFirewallProfile -PolicyStore ActiveStore | Select-Object Name, Enabled, DefaultInboundAction, DefaultOutboundAction, LogAllowed, LogBlocked, LogMaxSizeKilobytes, LogFileName) | ConvertTo-Json -Depth 4 +if ($before -ne $after) { throw 'Firewall state changed during the read-only smoke.' } +Write-Host 'PASS: real ActiveStore/PersistentStore reads, conservative service ACL inspection and dry run; effective policy unchanged. Traffic/log generation and forwarding remain untested.' diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 4f073d1f..67e6088b 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,7 @@ **改善:** +- ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (issue #375) (@Shirofune-Security) - `audit-settings`、`plan`、`configure`で共有するバージョン付きの詳細監査ポリシープロファイルを追加した。59のサブカテゴリと14のプロファイルで、WELA、文書に基づくWindows既定値、Microsoft、確認済みのCIS v4.0.0、ASDのWindows標準機能向け監査ガイドに対応する。ホストの役割とビルドの検証、オフラインでの設定計画、出典と前提条件を含むJSON出力をサポートする。完全一致、最低限、任意、変更なし、未構成、適用対象外を区別する。Windows既定値は参照専用で、プロファイルの対象はSecurityログの詳細監査ポリシーに限定される。 (#390) (@Shirofune-Security) - WELAのプロファイルにWindows標準の監査サブカテゴリを6つ追加した。Group MembershipとAuthorization Policy Changeは成功、Application Group Management、MPSSVC Rule-Level Policy Change、IPsec Driver、Kernel Objectは成功と失敗を監査する。各ガイドに対応するプロファイルでは、それぞれの監査設定と前提条件を維持する。Kernel Objectのイベント生成には対象オブジェクトに適切なSACLが必要であり、この変更ではそのSACLを作成しない。 (#391) (@Shirofune-Security) - `configure`と`configure -Profile`に`-DryRun`と`-ResultsPath`を追加し、Windows設定を変更せずに変更内容を確認し、設定項目ごとの結果をJSONで出力できるようにした。`-DryRun`に対応していないコマンドは、実行前にエラーで停止する。 (#392) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 9e7520f5..afdea72e 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,7 @@ **Improvements:** +- Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (issue #375) (@Shirofune-Security) - Added versioned advanced audit-policy profiles shared by `audit-settings`, `plan` and `configure`: 59 subcategories and 14 profiles covering WELA, documented Windows defaults, Microsoft, reviewed CIS v4.0.0 and ASD native guidance. Profiles support role/build validation, offline planning and JSON exports with sources and prerequisites. Exact, minimum, optional, unchanged, Not Configured and not-applicable settings remain distinct. Windows defaults are reference-only; profile scope is advanced Security audit policy. (#390) (@Shirofune-Security) - Added six native Windows audit subcategories to WELA's profile: Group Membership and Authorization Policy Change (Success), plus Application Group Management, MPSSVC Rule-Level Policy Change, IPsec Driver and Kernel Object (Success and Failure). Source-specific profiles retain their own audit settings and prerequisites; Kernel Object events require matching object SACLs, which this change does not create. (#391) (@Shirofune-Security) - Added `-DryRun` and `-ResultsPath` to `configure` and `configure -Profile` to preview changes without modifying Windows settings and export per-control results as JSON. Commands that do not support `-DryRun` reject it before running. (#392) (@Shirofune-Security)