Verify public Security warning policy configuration on native Windows

This commit is contained in:
Shirofune-Security committed 2026-09-22 07:50:09 +09:00
1 parent 39e8ce1d70
commit b92c044dd9
7 files changed
+156 -3

No files matched your search

@@ -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
+1
View File
@@ -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)
+1
View File
@@ -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)
+10 -3
View File
@@ -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,
@@ -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
+1
View File
@@ -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)
+1
View File
@@ -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)