From b92c044dd96180275d4380d1cb9c4b52f0a9bfde Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:50:09 +0900 Subject: [PATCH] Verify public Security warning policy configuration on native Windows --- .github/workflows/native-security-warning.yml | 47 +++++++++ CHANGELOG-Japanese.md | 1 + CHANGELOG.md | 1 + docs/audit-notifications.md | 13 ++- ...SecurityWarningConfigure.Windows.Tests.ps1 | 95 +++++++++++++++++++ website/docs/resources/changelog.ja.md | 1 + website/docs/resources/changelog.md | 1 + 7 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/native-security-warning.yml create mode 100644 tests/SecurityWarningConfigure.Windows.Tests.ps1 diff --git a/.github/workflows/native-security-warning.yml b/.github/workflows/native-security-warning.yml new file mode 100644 index 00000000..f905426f --- /dev/null +++ b/.github/workflows/native-security-warning.yml @@ -0,0 +1,47 @@ +name: Native Security warning configuration +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + native-security-warning: + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + engine: [powershell, pwsh] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Fixtures and public guards in Windows PowerShell5.1 + if: matrix.engine == 'powershell' + shell: powershell + run: | + ./tests/AuditNotifications.Tests.ps1 + ./tests/AuditNotifications.Windows.Tests.ps1 + - name: Native Security warning configuration in Windows PowerShell5.1 + if: matrix.engine == 'powershell' + shell: powershell + run: ./tests/SecurityWarningConfigure.Windows.Tests.ps1 -AllowDisposableWarningWrite + - name: Fixtures and public guards in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: | + ./tests/AuditNotifications.Tests.ps1 + ./tests/AuditNotifications.Windows.Tests.ps1 + - name: Native Security warning configuration in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: ./tests/SecurityWarningConfigure.Windows.Tests.ps1 -AllowDisposableWarningWrite + - name: Retain owned fixture evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: native-security-warning-${{ matrix.os }}-${{ matrix.engine }} + path: ${{ runner.temp }}/wela-security-warning-*/ + if-no-files-found: warn + retention-days: 7 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index aef86c3e..defe8483 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,7 @@ **改善:** +- Server 2022/2025 と Windows PowerShell 5.1/PowerShell 7 の破棄可能な環境で、Securityログ警告設定の公開CLIを検証します。未設定・0・高いしきい値、早い警告値の維持、DryRun、再実行、不正型の拒否と完全な復元を確認し、無関係な設定は保持します。ログ枯渇や警告イベント生成は検証範囲外です。 (@Shirofune-Security) - `wec-listener` の Plan/Apply を追加し、割り当て済みIPv4に限定した新規HTTP5985リスナーを作成できるようにしました。ホスト・実行ユーザー・ソース・WinRMとファイアウォールの状態、計画ハッシュ、実行前記録とネイティブ再読取で変更を検証します。両PowerShellホストから固定のWindows PowerShell 5.1ワーカーを使用し、既存リスナーや状態変化を検出した場合は拒否します。転送到着やSigma対応は別途検証が必要です。 (@Shirofune-Security) - 明示的な`file-access-probe` Plan/Runを追加し、既存のReadData成功監査SACLが適用される通常のローカルファイルから1バイトだけ読み取ります。実装・実行中エンジンの選択をハッシュ処理前に拒否し、同じハンドルのDOS/NTパスと実体、読み取りと実体再確認の実測区間、実際のワーカー・トークン・ハンドル、ポリシー・セキュリティ・実装の一致を確認し、ローカルSecurity4663と永続化した専用の証拠を必要とします。内容は保持せず、ポリシー・ACL・ファイルデータを変更しません。失敗監査・転送・Sigma利用可能性は未検証です。Server 2022/2025と両PowerShellの使い捨てテストで実イベントと正確な復元を検証します。 (関連 #373) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4555ae93..a81cd871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Improvements:** +- Verify public Security-log warning configuration on disposable Server 2022/2025 hosts under Windows PowerShell 5.1/PowerShell 7: absent/zero/higher thresholds, earlier-threshold preservation, dry run, idempotence, wrong-type refusal and exact cleanup. Preserve unrelated registry values, channel configuration, audit masks and CrashOnAuditFail; no log-exhaustion or warning-event claim. (@Shirofune-Security) - Added opt-in `wec-listener` Plan/Apply for one new assigned-IPv4 HTTP5985 listener. Reviewed host/operator/source and WinRM/firewall snapshots, explicit plan hashes, pending evidence and native readback guard creation and preserve existing settings. A fixed native Windows PowerShell 5.1 worker provides the creation adapter under both PowerShell host versions. Existing listeners and drift require review; forwarding arrival and Sigma readiness are not inferred. (@Shirofune-Security) - Added explicit `file-access-probe` Plan/Run for one byte read from one existing ordinary local leaf with a matching pre-existing ReadData success SACL. Source/engine targets are refused before hashing. Same-handle DOS/NT identity, exact worker/token/handle attribution over the measured read and identity-readback phase, full policy/security/source guards and durable private evidence require an actual local Security4663. No content is retained and no policy, ACL or file-data changes are made; failure, forwarding and Sigma readiness remain unverified. Disposable Server 2022/2025 tests cover both PowerShell engines and exact cleanup. (Related #373) (@Shirofune-Security) diff --git a/docs/audit-notifications.md b/docs/audit-notifications.md index 3f16df0a..595521b7 100644 --- a/docs/audit-notifications.md +++ b/docs/audit-notifications.md @@ -65,9 +65,16 @@ behavior or policy persistence. Fixture tests cover absent/typed values, threshold preservation, role/source and ADMX/channel gates, stale plans, races, failed/ignored writes, final drift, -idempotence, dry-run and command dispatch. Windows Server 2022/2025 CI observes -native registry/CIM/channel state without changing policy, under PowerShell 5.1 -and 7. This is not a Windows 11, DC or AD CS event-generation test. +idempotence, dry-run and command dispatch. The original Windows smoke observes native registry/CIM/channel state read-only. +A separate explicitly opted-in disposable Server 2022/2025 fixture runs public +SecurityWarning Plan, DryRun and Configure under PowerShell 5.1/7. It exercises +absent, zero and higher thresholds, preserves an earlier threshold, verifies +idempotence and refuses a real non-DWORD value. It checks typed original journals, +readback and exact cleanup while preserving other Security-key values/ACL, +channel enablement/size/retention, Event Log service state, OneSettings, +CrashOnAuditFail and all 59 audit masks. It never fills or clears a log, changes +retention, tests warning generation, or supplies OneSettings/Windows 11/DC/AD CS +acceptance. Before closing issue #378, retain isolated Windows 11 and Server 2022 evidence of an authorized benign OneSettings attempt with exact build/patch, policy, channel, diff --git a/tests/SecurityWarningConfigure.Windows.Tests.ps1 b/tests/SecurityWarningConfigure.Windows.Tests.ps1 new file mode 100644 index 00000000..928064f8 --- /dev/null +++ b/tests/SecurityWarningConfigure.Windows.Tests.ps1 @@ -0,0 +1,95 @@ +param([switch]$AllowDisposableWarningWrite) +$ErrorActionPreference='Stop' +if(-not $AllowDisposableWarningWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit opt-in on a disposable GitHub-hosted Windows runner is required.'} +$repo=Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force +Import-Module (Join-Path $repo 'modules/NativeProviders.psm1') -Force +. (Join-Path $repo 'scripts/Configuration.ps1') +$engine=(Get-Process -Id $PID).Path +$root=Join-Path $env:RUNNER_TEMP ('wela-security-warning-'+[guid]::NewGuid().ToString('N')) +$null=New-Item -ItemType Directory -Path $root +$path='HKLM:\SYSTEM\CurrentControlSet\Services\Eventlog\Security';$name='WarningLevel' +$count=0;$failure=$null;$cleanupErrors=@() +function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++} +function Save($Name,$Value){ConvertTo-Json -InputObject $Value -Depth 25|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8} +function Key($Value){ConvertTo-Json -InputObject $Value -Depth 25 -Compress} +function Masks($Value){@($Value.Keys|Sort-Object|ForEach-Object{"$_=$($Value[$_])"}) -join ';'} +function Warning {Get-WelaRegistryState $path $name} +function Unselected { + $key=Get-Item -LiteralPath $path + try{ + $values=@(foreach($n in @($key.GetValueNames()|Sort-Object)){ + if($n -ine $name){[pscustomobject][ordered]@{Name=$n;Type=[string]$key.GetValueKind($n);Value=$key.GetValue($n,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}} + }) + $subkeys=@($key.GetSubKeyNames()|Sort-Object) + }finally{$key.Dispose()} + [pscustomobject][ordered]@{OtherSecurityValues=$values;SecuritySubkeys=$subkeys;SecurityAcl=(Get-Acl -LiteralPath $path).Sddl;SecurityChannel=Get-WelaNativeChannel Security;ApplicationChannel=Get-WelaNativeChannel Application;OneSettings=Get-WelaRegistryState 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' EnableOneSettingsAuditing;CrashOnAuditFail=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' CrashOnAuditFail;EventLogService=[string](Get-Service EventLog).Status} +} +function Public([string]$Label,[string[]]$Arguments,[int]$Expected=0){ + $prior=$ErrorActionPreference + try{$ErrorActionPreference='Continue';$output=& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') audit-notifications @Arguments 2>&1|Out-String;$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior} + $output|Set-Content -LiteralPath (Join-Path $root ($Label+'.txt')) -Encoding UTF8 + Assert ($code -eq $Expected) "Public $Label exited $code, expected $Expected : $output" +} +$before=Warning;$unselected=Unselected;$masks=Get-WelaEffectiveAuditPolicy +Assert $before.KeyExists 'Existing Security registry key is required; fixture never creates/removes it.' +Save 'original.json' @{Warning=$before;Unselected=$unselected;Masks=$masks;Engine=$PSVersionTable.PSVersion.ToString();OS=[Environment]::OSVersion.VersionString} +$base=@('-NotificationControl','SecurityWarning') +try{ + # Remove only the selected value to exercise absence rather than a fabricated default. + if((Warning).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop} + $seed=Warning + Public 'plan' ($base+@('-NotificationAction','Plan','-WarningPercent','90','-ResultsPath',(Join-Path $root 'plan.json'))) + $plan=Get-Content (Join-Path $root 'plan.json') -Raw|ConvertFrom-Json + Assert ($plan.Plan.Count -eq 1 -and $plan.Plan[0].Definition.Id -ceq 'SecurityWarning' -and $plan.Plan[0].Desired -eq 90 -and -not $plan.Plan[0].Before.Policy.ValueExists) 'Public Plan retains actual absence and exactly one selected control.' + $dryBackup=Join-Path $root 'dry-backup' + Public 'dry' ($base+@('-NotificationAction','Configure','-WarningPercent','90','-Auto','-DryRun','-BackupPath',$dryBackup,'-ResultsPath',(Join-Path $root 'dry.json'))) + $dry=Get-Content (Join-Path $root 'dry.json') -Raw|ConvertFrom-Json + Assert ($dry.ExitCode -eq 0 -and $dry.DryRun -and $dry.Results[0].Status -ceq 'Skipped' -and -not(Test-Path $dryBackup)) 'Public dry run reports a skipped proposal and creates no journal.' + Assert ((Key (Warning)) -ceq (Key $seed) -and (Key (Unselected)) -ceq (Key $unselected) -and (Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $masks)) 'Plan/DryRun preserve the selected absence, unrelated native state and all59 masks.' + $cases=@( + @{Id='absent';Before=$null;Maximum=90;Desired=90;Status='Applied'}, + @{Id='zero';Before=0;Maximum=80;Desired=80;Status='Applied'}, + @{Id='higher';Before=95;Maximum=70;Desired=70;Status='Applied'}, + @{Id='earlier';Before=25;Maximum=90;Desired=25;Status='AlreadyCompliant'} + ) + foreach($case in $cases){ + if($null -ne $case.Before){$null=New-ItemProperty -LiteralPath $path -Name $name -Value $case.Before -PropertyType DWord -Force} + $prior=Warning;$backup=Join-Path $root ($case.Id+'-backup');$results=Join-Path $root ($case.Id+'.json') + Public $case.Id ($base+@('-NotificationAction','Configure','-WarningPercent',[string]$case.Maximum,'-Auto','-BackupPath',$backup,'-ResultsPath',$results)) + $report=Get-Content $results -Raw|ConvertFrom-Json;$after=Warning + Assert ($report.ExitCode -eq 0 -and $report.Scope -ceq 'audit-notifications' -and $report.Results.Count -eq 1 -and $report.Results[0].Status -ceq $case.Status) 'Each selected native case has one accurate result and narrow scope.' + Assert ($after.Type -ceq 'DWord' -and $after.Value -eq $case.Desired -and $report.Current[0].Before.Policy.Value -eq $case.Desired) 'Native DWORD readback and public current state match the exact intended threshold.' + Assert ($report.PrivacyChannelPlan.Count -eq 0 -and $report.EventGeneration -match 'Not verified') 'No privacy-channel operation or warning event claim is implied.' + $journalPath=Join-Path $backup 'before.jsonl' + if($case.Status -eq 'Applied'){ + $journal=@(Get-Content $journalPath|ConvertFrom-Json) + Assert ($journal.Count -eq 1 -and $journal[0].Target.Name -ceq $name -and $journal[0].Target.Path -ceq $path -and (Key $journal[0].Before.Policy) -ceq (Key $prior)) 'The one native change has exact typed original journal evidence.' + Assert ((Key $report.Results[0].Before.Policy) -ceq (Key $prior) -and (Key $report.Results[0].After.Policy) -ceq (Key $after)) 'Applied result binds exact native before and after policy.' + }else{Assert (-not(Test-Path $journalPath)) 'An earlier existing warning is preserved without a write journal.'} + Assert ((Key (Unselected)) -ceq (Key $unselected) -and (Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $masks)) 'Each public Configure preserves siblings, ACL, channels, service, audit masks, OneSettings and CrashOnAuditFail.' + } + $repeatPath=Join-Path $root 'repeat.json';$repeatBackup=Join-Path $root 'repeat-backup' + Public 'repeat' ($base+@('-NotificationAction','Configure','-WarningPercent','90','-Auto','-BackupPath',$repeatBackup,'-ResultsPath',$repeatPath)) + $repeat=Get-Content $repeatPath -Raw|ConvertFrom-Json + Assert ($repeat.Results[0].Status -ceq 'AlreadyCompliant' -and (Warning).Value -eq 25 -and -not(Test-Path (Join-Path $repeatBackup 'before.jsonl'))) 'Repeated Configure is idempotent and preserves the earlier threshold.' + # Fixture-owned wrong type must remain wrong rather than being coerced and overwritten. + Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop + $null=New-ItemProperty -LiteralPath $path -Name $name -Value 'fixture-not-a-dword' -PropertyType String + $invalid=Warning;$invalidPath=Join-Path $root 'invalid.json';$invalidBackup=Join-Path $root 'invalid-backup' + Public 'invalid' ($base+@('-NotificationAction','Configure','-WarningPercent','90','-Auto','-BackupPath',$invalidBackup,'-ResultsPath',$invalidPath)) 1 + $refused=Get-Content $invalidPath -Raw|ConvertFrom-Json + Assert ($refused.ExitCode -eq 1 -and $refused.Results[0].Status -ceq 'Failed' -and (Key (Warning)) -ceq (Key $invalid) -and -not(Test-Path (Join-Path $invalidBackup 'before.jsonl'))) 'Actual wrong type yields failure and is preserved without a native write journal.' + Save 'completed.json' @{Status='Passed';Assertions=$count;Scope='Actual named policy configuration only. No warning generation, log exhaustion, retention changes, GPO refresh, ingestion or Sigma proof.'} +}catch{$failure=$_.ToString();throw}finally{ + try{ + if((Warning).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop} + if($before.ValueExists){$null=New-ItemProperty -LiteralPath $path -Name $name -Value $before.Value -PropertyType $before.Type} + }catch{$cleanupErrors+=$_.ToString()} + $warningOk=$false;$otherOk=$false;$masksOk=$false + try{$warningOk=(Key (Warning)) -ceq (Key $before);$otherOk=(Key (Unselected)) -ceq (Key $unselected);$masksOk=(Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $masks)}catch{$cleanupErrors+=$_.ToString()} + Save 'cleanup.json' @{Failure=$failure;Errors=$cleanupErrors;WarningRestored=$warningOk;UnselectedPreserved=$otherOk;All59MasksPreserved=$masksOk;Complete=($warningOk -and $otherOk -and $masksOk -and -not $cleanupErrors.Count)} + if(-not $warningOk -or -not $otherOk -or -not $masksOk -or $cleanupErrors.Count){throw 'Native warning fixture cleanup failed; inspect retained evidence.'} +} +Write-Host "PASS: $count native public Security warning assertions and exact cleanup." +exit 0 diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index b7c56a3d..7347fbf2 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,7 @@ **改善:** +- Server 2022/2025 と Windows PowerShell 5.1/PowerShell 7 の破棄可能な環境で、Securityログ警告設定の公開CLIを検証します。未設定・0・高いしきい値、早い警告値の維持、DryRun、再実行、不正型の拒否と完全な復元を確認し、無関係な設定は保持します。ログ枯渇や警告イベント生成は検証範囲外です。 (@Shirofune-Security) - `wec-listener` の Plan/Apply を追加し、割り当て済みIPv4に限定した新規HTTP5985リスナーを作成できるようにしました。ホスト・実行ユーザー・ソース・WinRMとファイアウォールの状態、計画ハッシュ、実行前記録とネイティブ再読取で変更を検証します。両PowerShellホストから固定のWindows PowerShell 5.1ワーカーを使用し、既存リスナーや状態変化を検出した場合は拒否します。転送到着やSigma対応は別途検証が必要です。 (@Shirofune-Security) - 明示的な`file-access-probe` Plan/Runを追加し、既存のReadData成功監査SACLが適用される通常のローカルファイルから1バイトだけ読み取ります。実装・実行中エンジンの選択をハッシュ処理前に拒否し、同じハンドルのDOS/NTパスと実体、読み取りと実体再確認の実測区間、実際のワーカー・トークン・ハンドル、ポリシー・セキュリティ・実装の一致を確認し、ローカルSecurity4663と永続化した専用の証拠を必要とします。内容は保持せず、ポリシー・ACL・ファイルデータを変更しません。失敗監査・転送・Sigma利用可能性は未検証です。Server 2022/2025と両PowerShellの使い捨てテストで実イベントと正確な復元を検証します。 (関連 #373) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 2e489bf6..ce04db07 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,7 @@ **Improvements:** +- Verify public Security-log warning configuration on disposable Server 2022/2025 hosts under Windows PowerShell 5.1/PowerShell 7: absent/zero/higher thresholds, earlier-threshold preservation, dry run, idempotence, wrong-type refusal and exact cleanup. Preserve unrelated registry values, channel configuration, audit masks and CrashOnAuditFail; no log-exhaustion or warning-event claim. (@Shirofune-Security) - Added opt-in `wec-listener` Plan/Apply for one new assigned-IPv4 HTTP5985 listener. Reviewed host/operator/source and WinRM/firewall snapshots, explicit plan hashes, pending evidence and native readback guard creation and preserve existing settings. A fixed native Windows PowerShell 5.1 worker provides the creation adapter under both PowerShell host versions. Existing listeners and drift require review; forwarding arrival and Sigma readiness are not inferred. (@Shirofune-Security) - Added explicit `file-access-probe` Plan/Run for one byte read from one existing ordinary local leaf with a matching pre-existing ReadData success SACL. Source/engine targets are refused before hashing. Same-handle DOS/NT identity, exact worker/token/handle attribution over the measured read and identity-readback phase, full policy/security/source guards and durable private evidence require an actual local Security4663. No content is retained and no policy, ACL or file-data changes are made; failure, forwarding and Sigma readiness remain unverified. Disposable Server 2022/2025 tests cover both PowerShell engines and exact cleanup. (Related #373) (@Shirofune-Security)