diff --git a/.gitattributes b/.gitattributes index 585a8a2b..04e16552 100644 --- a/.gitattributes +++ b/.gitattributes @@ -97,6 +97,8 @@ tests/NativeProviderConfigure.Windows.Tests.ps1 text eol=lf /tests/WecCollectorObservation* text eol=lf /tests/WecSubscriptionInventory* text eol=lf +# Disposable public OneSettings fixture source identity. +/tests/OneSettingsConfigure*.ps1 text eol=lf # Scoped NTLM source and native evidence retain stable bytes. /scripts/NtlmAudit.ps1 text eol=lf /tests/NtlmAudit* text eol=lf diff --git a/.github/workflows/native-onesettings.yml b/.github/workflows/native-onesettings.yml new file mode 100644 index 00000000..460dd51f --- /dev/null +++ b/.github/workflows/native-onesettings.yml @@ -0,0 +1,47 @@ +name: Native public OneSettings configuration +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + native-onesettings: + 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/OneSettingsConfigure.Cli.Tests.ps1 + - name: Native public OneSettings configuration in Windows PowerShell5.1 + if: matrix.engine == 'powershell' + shell: powershell + run: ./tests/OneSettingsConfigure.Windows.Tests.ps1 -AllowDisposableOneSettingsWrite + - name: Fixtures and public guards in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: | + ./tests/AuditNotifications.Tests.ps1 + ./tests/OneSettingsConfigure.Cli.Tests.ps1 + - name: Native public OneSettings configuration in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: ./tests/OneSettingsConfigure.Windows.Tests.ps1 -AllowDisposableOneSettingsWrite + - name: Retain owned fixture evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: native-onesettings-${{ matrix.os }}-${{ matrix.engine }} + path: ${{ runner.temp }}/wela-onesettings-native-*/ + if-no-files-found: warn + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10d94795..8dd7c1eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: Copy-Item -Recurse -Path ./scripts -Destination release-binaries/ Copy-Item -Recurse -Path ./modules -Destination release-binaries/ New-Item -ItemType Directory -Path release-binaries/docs -Force | Out-Null - Copy-Item -Path ./docs/gpo-audit-packages.md, ./docs/gpo-package-deployment.md, ./docs/gpo-creation.md, ./docs/wmi-probe.md, ./docs/firewall-logging.md, ./docs/firewall-logging-recovery.md, ./docs/ipsec-prerequisites.md, ./docs/wec-state.md, ./docs/wec-update.md, ./docs/wec-runtime.md, ./docs/wef-deployment.md, ./docs/native-channel-access.md, ./docs/eventlog-settings.md, ./docs/channel-read.md, ./docs/native-rule-eligibility.md, ./docs/native-validation.md, ./docs/wef-arrival.md, ./docs/smb-runtime-activation.md, ./docs/smb-auditing.md, ./docs/wec-ingress.md, ./docs/capi2-probe.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/eventlog-recovery.md, ./docs/failed-logon-probe.md, ./docs/wec-listener.md, ./docs/file-access-probe.md, ./docs/applocker-script-probe.md, ./docs/applocker-probe.md, ./docs/selected-sacl-configuration.md, ./docs/registry-sacl-recovery.md, ./docs/targeted-sacl-planning.md, ./docs/native-registry-sacl-validation.md, ./docs/wec-authorization.md, ./docs/channel-recovery.md, ./docs/outgoing-ntlm.md, ./docs/wec-collector-observation.md, ./docs/native-provider-packs.md, ./docs/native-provider-acceptance.md, ./docs/ntlm-auditing.md, ./docs/wef-query.md, ./docs/native-filesystem-sacl-validation.md -Destination release-binaries/docs/ + Copy-Item -Path ./docs/gpo-audit-packages.md, ./docs/gpo-package-deployment.md, ./docs/gpo-creation.md, ./docs/wmi-probe.md, ./docs/firewall-logging.md, ./docs/firewall-logging-recovery.md, ./docs/ipsec-prerequisites.md, ./docs/wec-state.md, ./docs/wec-update.md, ./docs/wec-runtime.md, ./docs/wef-deployment.md, ./docs/native-channel-access.md, ./docs/eventlog-settings.md, ./docs/channel-read.md, ./docs/native-rule-eligibility.md, ./docs/native-validation.md, ./docs/wef-arrival.md, ./docs/smb-runtime-activation.md, ./docs/smb-auditing.md, ./docs/wec-ingress.md, ./docs/capi2-probe.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/eventlog-recovery.md, ./docs/failed-logon-probe.md, ./docs/wec-listener.md, ./docs/file-access-probe.md, ./docs/applocker-script-probe.md, ./docs/applocker-probe.md, ./docs/selected-sacl-configuration.md, ./docs/registry-sacl-recovery.md, ./docs/targeted-sacl-planning.md, ./docs/native-registry-sacl-validation.md, ./docs/wec-authorization.md, ./docs/channel-recovery.md, ./docs/outgoing-ntlm.md, ./docs/wec-collector-observation.md, ./docs/native-provider-packs.md, ./docs/native-provider-acceptance.md, ./docs/audit-notifications.md, ./docs/native-onesettings-acceptance.md, ./docs/ntlm-auditing.md, ./docs/wef-query.md, ./docs/native-filesystem-sacl-validation.md -Destination release-binaries/docs/ - name: Set Artifact Name if: contains(matrix.info.os, 'windows') == true diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 4dd2ae39..f60ac050 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,8 @@ **改善:** +- OneSettingsポリシーと明示的なPrivacyチャネル設定の公開CLIを実機検証します。Server 2022で実際の適用、型付き復元記録と再読取、冪等性、不正値の拒否を確認し、Server 2025の既存の拒否を維持します。両PowerShellで厳密な後処理を検証し、イベント生成やSigma対応は主張しません。通知コントロール省略時の引数渡しを修正し、既定のAuditは両項目を読み取り、項目未選択のConfigureは非ゼロで失敗します。(関連 #378) (@Shirofune-Security) + - 受信・ドメイン監査を明示的に選択する `ntlm-auditing` Audit/Plan/Configure を追加しました。受信監査 DWORD2 と実際のDC上のドメイン監査 DWORD7 のみを設定し、旧値2の意味を推測せず識別します。不明な値・ホストや設定の変化を拒否し、型付き変更前記録・再読取・部分失敗を区別します。ネイティブテストで受信設定、非DCのスキップ、無関係な設定の保持と復元を検証します。(関連 #363) (@Shirofune-Security) - 読み取り専用の `wef-query` を追加しました。選択したソースのQueryListをそのままネイティブAPIで実行し、Select/Suppressの動作、チャネル別の失敗診断、上限付きの一致イベントXML、実際の操作者・ホスト・ソースの整合性を確認します。空の結果、アクセス拒否、未存在、不正クエリ、上限到達、状態変化を区別し、破棄可能なWindows環境で実イベントの選択・抑制と標準ユーザーの拒否、完全な後片付けを検証します。転送サービスのアクセス権、配送、Sigmaの準備完了は推定しません。 (Related #368) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index db565e63..217f65f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Improvements:** +- Add native public OneSettings policy and explicit Privacy-channel configuration acceptance: actual apply, typed journals/readback, idempotence and invalid-value refusals on Server 2022; preserve the existing Server 2025 refusal. Both PowerShell engines verify exact fixture cleanup without event or Sigma claims. Fix omitted notification-control dispatch so default Audit reads both controls and Configure without a selection fails with a nonzero exit. (Related #378) (@Shirofune-Security) + - Add `ntlm-auditing` Audit/Plan/Configure with explicit Incoming/Domain/Both selection. Configure only incoming audit DWORD2 and actual-DC domain audit DWORD7, identify legacy domain2 without invented semantics, refuse unknown values and host/state drift, and retain separate typed journals/readback/partial outcomes. Native tests verify incoming changes, non-DC skips and exact preservation of unrelated settings. (Related #363) (@Shirofune-Security) - Added read-only `wef-query` preflight for one exact selected source QueryList, with strict native Select/Suppress execution, separate per-channel failure diagnostics, bounded matching XML and actual caller/host/source guards. Empty, denied, missing, invalid, capped and drifted results remain distinct; disposable native tests cover real record selection/suppression and standard-user denial with exact cleanup. No forwarding-service access, delivery or Sigma credit is inferred. (Related #368) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index 2c61fecf..7ece9a25 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -2610,7 +2610,11 @@ switch ($Cmd.ToLower()) { if ($Help) { Write-Host 'Usage: ./WELA.ps1 audit-notifications [-NotificationAction Audit|Plan|Configure] [-NotificationControl OneSettings,SecurityWarning] [-WarningPercent 1..90] [-EnablePrivacyChannel] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath report.json]. See docs/audit-notifications.md.'; return } if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath) { throw 'audit-notifications uses actual host context and -ResultsPath; profile/role/build overrides and HTML are unsupported.' } if ($NotificationAction -eq 'Configure' -and -not (TestAdministrator)) { throw 'Notification Configure requires Administrator privileges.' } - $report=Invoke-WelaNotificationCommand -Action $NotificationAction -Control $NotificationControl -WarningPercent $WarningPercent -EnablePrivacyChannel:$EnablePrivacyChannel -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $notificationArguments=@{Action=$NotificationAction;WarningPercent=$WarningPercent;EnablePrivacyChannel=$EnablePrivacyChannel;Auto=$Auto;DryRun=$DryRun;BackupPath=$BackupPath;ResultsPath=$ResultsPath} + # Omit an unspecified ValidateSet array: explicit null fails binding before default Audit + # selection or Configure's required-selection guard, and can leave a zero process exit. + if($PSBoundParameters.ContainsKey('NotificationControl')){$notificationArguments.Control=$NotificationControl} + $report=Invoke-WelaNotificationCommand @notificationArguments $report if ($report.ExitCode) { exit $report.ExitCode } } diff --git a/docs/audit-notifications.md b/docs/audit-notifications.md index 595521b7..230e5c9c 100644 --- a/docs/audit-notifications.md +++ b/docs/audit-notifications.md @@ -76,6 +76,8 @@ 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. +A separate [native OneSettings acceptance fixture](native-onesettings-acceptance.md) exercises public policy and dependent-channel configuration on Server 2022 and the existing refusal on Server 2025, under both PowerShell engines. It verifies typed journals, idempotence, actual invalid-value refusals and exact cleanup. This remains settings evidence only. + Before closing issue #378, retain isolated Windows 11 and Server 2022 evidence of an authorized benign OneSettings attempt with exact build/patch, policy, channel, native event XML and collection result. Do not assume an EventID without inspecting diff --git a/docs/native-onesettings-acceptance.md b/docs/native-onesettings-acceptance.md new file mode 100644 index 00000000..acc2d93a --- /dev/null +++ b/docs/native-onesettings-acceptance.md @@ -0,0 +1,30 @@ +# Native public OneSettings configuration acceptance + +The disposable acceptance fixture invokes the actual `WELA.ps1 audit-notifications` command under Windows PowerShell 5.1 and PowerShell 7. It verifies local configuration and the explicit Privacy channel dependency. It does not generate a OneSettings event or claim effective producer behavior, policy persistence, forwarding or Sigma readiness. Sysmon is excluded. + +```powershell +./WELA.ps1 audit-notifications -NotificationAction Plan -NotificationControl OneSettings -EnablePrivacyChannel +./WELA.ps1 audit-notifications -NotificationAction Configure -NotificationControl OneSettings -EnablePrivacyChannel -DryRun +./WELA.ps1 audit-notifications -NotificationAction Configure -NotificationControl OneSettings -EnablePrivacyChannel -Auto -BackupPath C:\Evidence\onesettings-before -ResultsPath C:\Evidence\onesettings.json +``` + +Configure requires elevation and explicit control selection. Audit without a selection reads both notification controls. The CLI omits an unspecified control argument so these defaults and the required-selection error reach the command; passing an explicit null into the validated control parameter previously produced a binding error with a zero process exit. + +Server 2022 requires the real installed `DataCollection.admx` machine mapping for `EnableOneSettingsAuditing` DWORD1, its existing native registry key and readable `Microsoft-Windows-Privacy-Auditing/Operational` metadata. Server 2025 remains unverified by the reviewed source and must refuse configuration. Tests preserve this gate; installing an ADMX alone does not establish support. + +On Server 2022, `tests/OneSettingsConfigure.Windows.Tests.ps1 -AllowDisposableOneSettingsWrite` verifies: + +- Plan and DryRun against an actually absent value and disabled channel, with no write journal or mutation. +- Policy-only Configure creates exactly DWORD1 and leaves the disabled channel unchanged. +- Explicit `-EnablePrivacyChannel` enables the channel after the producer policy is verified, preserving its existing larger buffer, retention, complete descriptor and other native configuration. +- A combined call from DWORD0 and a disabled channel writes policy then channel, retaining exact typed original records and native readback. +- Repeated configuration produces only AlreadyCompliant results and no write journal. +- Actual string and unreviewed DWORD2 values are preserved; failed producer prerequisites prevent the dependent channel action. Unsupported preview arguments are rejected before dispatch. + +On Server 2025, Plan, Configure, DryRun and an explicit channel request exercise the existing unsupported-source refusal. They must not create the selected value, change a channel or write a pre-change journal. This is refusal evidence, not positive Server 2025 support. + +The fixture is restricted to explicitly opted-in disposable GitHub-hosted standalone servers. Only the fixture temporarily prepares the selected value and channel. It retains original and restored typed policy, full channel XML, unrelated DataCollection values/descendants and owner/group/DACL, Security/System/Application/CAPI2 settings, service status/start type, Security warning/CrashOnAuditFail and all59 audit masks. Each cleanup check runs independently and records errors. Event records produced during testing are not restored. The fixture never clears a log, requests GPO refresh, modifies diagnostic-data upload policy or invokes a OneSettings download. + +Owned public child processes have a three-minute deadline, bounded output, a bounded drain and confirmed termination before cleanup. The evidence manifest records commit, host/engine, source fingerprints and artifact hashes. These local hashes detect altered evidence; they are not signed attestation. Review the current four-way `Native public OneSettings configuration` workflow artifacts before relying on native acceptance. + +The broader [audit-notifications guide](audit-notifications.md) describes prerequisites and remaining Windows11, DC/AD CS, event-generation and intended-reader/collector evidence for issue #378. diff --git a/tests/OneSettingsConfigure.Cli.Tests.ps1 b/tests/OneSettingsConfigure.Cli.Tests.ps1 new file mode 100644 index 00000000..883b139b --- /dev/null +++ b/tests/OneSettingsConfigure.Cli.Tests.ps1 @@ -0,0 +1,26 @@ +# Public option isolation; these tests do not claim native configuration evidence. +$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent;$engine=(Get-Process -Id $PID).Path;$count=0 +$unused=Join-Path ([IO.Path]::GetTempPath()) ('wela-onesettings-cli-'+[guid]::NewGuid().ToString('N')) +$cases=@( + @{Args=@('audit-notifications','-Help');Code=0;Pattern='EnablePrivacyChannel'}, + @{Args=@('audit-notifications','-NotificationAction','Configure','-NotificationControl','OneSettings','-EnablePrivacyChannel','-Auto','-BackupPath',$unused,'-WhatIf');Code=1;Pattern='No command was run'}, + @{Args=@('audit-notifications','-NotificationAction','Configure','-NotificationControl','OneSettings','-EnablePrivacyChannel','-Auto','-UnexpectedOption');Code=1;Pattern='No command was run'}, + @{Args=@('audit-notifications','-NotificationAction','Configure','-Auto');Code=1;Pattern='explicit NotificationControl'}, + @{Args=@('audit-notifications','-NotificationControl','SecurityWarning','-EnablePrivacyChannel');Code=1;Pattern='requires the OneSettings'}, + @{Args=@('audit-notifications','-NotificationAction','Plan','-DryRun');Code=1;Pattern='DryRun'}, + @{Args=@('audit-notifications','-Help','-Build','20348');Code=1;Pattern='accepts only notification'}, + @{Args=@('audit-notifications','-Help','-Profile','wela-2.2.0');Code=1;Pattern='accepts only notification'}, + @{Args=@('channel-settings','-Help','-EnablePrivacyChannel');Code=1;Pattern='Notification options require'}, + @{Args=@('configure','-NotificationControl','OneSettings','-Auto');Code=1;Pattern='Notification options require'} +) +foreach($case in $cases){$old=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$output=&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" @($case.Args) 2>&1|Out-String;$code=$LASTEXITCODE}finally{$ErrorActionPreference=$old};if($code -ne $case.Code -or $output -notmatch $case.Pattern){throw "Public option case failed: $($case.Args -join ' ') [$code] $output"};$count++} +if(Test-Path -LiteralPath $unused){throw 'Unsupported preview created a journal directory.'};$count++ +$defaultPath=$unused+'.json' +try{ + $old=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$output=&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" audit-notifications -ResultsPath $defaultPath 2>&1|Out-String;$code=$LASTEXITCODE}finally{$ErrorActionPreference=$old} + if(-not(Test-Path -LiteralPath $defaultPath)){throw ('Default public Audit failed to create its requested report: '+$output)} + $report=Get-Content -LiteralPath $defaultPath -Raw|ConvertFrom-Json + if($report.Action -cne 'Audit' -or $report.Current.Count -ne 2 -or (@($report.Current.Definition.Id|Sort-Object) -join ',') -cne 'OneSettings,SecurityWarning' -or $report.ExitCode -ne $code){throw 'Default Audit did not select both controls and preserve its truthful host-specific exit.'};$count++ +}finally{if(Test-Path -LiteralPath $defaultPath){Remove-Item -LiteralPath $defaultPath}} +Write-Host "PASS: $count public OneSettings option guards." +$global:LASTEXITCODE=0 diff --git a/tests/OneSettingsConfigure.Windows.Tests.ps1 b/tests/OneSettingsConfigure.Windows.Tests.ps1 new file mode 100644 index 00000000..2a9a6a72 --- /dev/null +++ b/tests/OneSettingsConfigure.Windows.Tests.ps1 @@ -0,0 +1,144 @@ +# Only this opted-in disposable fixture may prepare/restore the selected policy and channel. +param([switch]$AllowDisposableOneSettingsWrite) +$ErrorActionPreference='Stop' +if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess -or -not $AllowDisposableOneSettingsWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit disposable native64 GitHub-hosted Windows opt-in required.'} +$repo=Split-Path $PSScriptRoot -Parent;$script:ScriptRoot=$repo +foreach($module in @('AuditProfiles','EventLogSettings','NativeProviders','NativeChannelAccess')){Import-Module "$repo/modules/$module.psm1" -Force} +foreach($scriptName in @('Configuration','NativeChannelConfiguration','AuditNotifications','WefArrival')){. "$repo/scripts/$scriptName.ps1"} +$count=0;$errors=@();$primary=$null;$policyTouched=$false;$channelTouched=$false +function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++} +function Save($Name,$Value){ConvertTo-Json -InputObject $Value -Depth 32|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8} +function Key($Value){ConvertTo-Json -InputObject $Value -Depth 32 -Compress} +function Masks($Value){@($Value.Keys|Sort-Object|ForEach-Object{"$_=$($Value[$_])"}) -join ';'} +$path='HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection';$valueName='EnableOneSettingsAuditing';$channelName='Microsoft-Windows-Privacy-Auditing/Operational' +function Policy {Get-WelaRegistryState $path $valueName} +function Read-Raw([string]$Name){$native=Invoke-WelaNative wevtutil.exe @('gl',$Name,'/f:xml');$doc=[Xml.XmlDocument]::new();$doc.XmlResolver=$null;$doc.LoadXml(($native.Output -join "`n"));$doc.OuterXml} +function Services {@(Get-Service Winmgmt,EventLog,DiagTrack -ErrorAction Stop|Sort-Object Name|ForEach-Object{[pscustomobject]@{Name=$_.Name;Status=[string]$_.Status;StartType=[string]$_.StartType}})} +function Read-PolicyKey($Key,[int]$Depth=0){ + if($Depth -gt 8 -or ++$script:registryCount -gt 256){throw 'DataCollection fixture inventory exceeds depth/key bounds.'} + $values=@(foreach($n in @($Key.GetValueNames()|Sort-Object)){ + if($Depth -eq 0 -and $n -ieq $valueName){continue} + [pscustomobject][ordered]@{Name=$n;Type=[string]$Key.GetValueKind($n);Value=$Key.GetValue($n,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)} + }) + $acl=if($PSVersionTable.PSVersion.Major -ge 6){[Microsoft.Win32.RegistryAclExtensions]::GetAccessControl($Key)}else{$Key.GetAccessControl()} + $children=@(foreach($n in @($Key.GetSubKeyNames()|Sort-Object)){$child=$Key.OpenSubKey($n);try{if(-not $child){throw 'DataCollection child disappeared.'};[pscustomobject]@{Name=$n;State=Read-PolicyKey $child ($Depth+1)}}finally{if($child){$child.Dispose()}}}) + [pscustomobject][ordered]@{Values=$values;OwnerGroupDacl=$acl.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Owner -bor [Security.AccessControl.AccessControlSections]::Group -bor [Security.AccessControl.AccessControlSections]::Access);Children=$children} +} +function Unselected { + $baseKey=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64);$key=$null + try{$key=$baseKey.OpenSubKey('SOFTWARE\Policies\Microsoft\Windows\DataCollection');$script:registryCount=0;$tree=if($key){Read-PolicyKey $key}else{$null}}finally{if($key){$key.Dispose()};$baseKey.Dispose()} + $state=[pscustomobject][ordered]@{OtherDataCollection=$tree;SecurityChannel=Read-Raw 'Security';SystemChannel=Read-Raw 'System';ApplicationChannel=Read-Raw 'Application';Capi2Channel=Read-Raw 'Microsoft-Windows-CAPI2/Operational';SecurityWarning=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Services\Eventlog\Security' WarningLevel;CrashOnAuditFail=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' CrashOnAuditFail;Services=Services} + if((Key $state).Length -gt 4194304){throw 'Unselected fixture inventory exceeds four MiB characters.'};$state +} +Add-Type -TypeDefinition @' +using System;using System.IO;using System.Text;using System.Threading.Tasks; +public static class WelaOneSettingsFixturePipe { + public static async Task Read(TextReader reader) { + var text=new StringBuilder();var buffer=new char[2048];while(true){int n=await reader.ReadAsync(buffer,0,buffer.Length).ConfigureAwait(false);if(n==0)return text.ToString();if(n>1048576-text.Length)throw new InvalidDataException("Public fixture output exceeded one Mi characters.");text.Append(buffer,0,n);} + } +} +'@ +$engine=(Get-Process -Id $PID).Path +foreach($service in @('Winmgmt','EventLog')){if((Get-Service $service).Status -ne 'Running'){throw 'Fixture observation dependencies must already be running.'}} +$hostState=Get-WelaNotificationHost +if($hostState.Status -cne 'Supported' -or $hostState.ProductType -ne 3 -or $hostState.DomainRole -ne 2 -or $hostState.Build -notin @(20348,26100)){throw 'Only disposable standalone Server2022/2025 is supported.'} +$root=New-WelaArrivalOutput (Join-Path $env:RUNNER_TEMP ('wela-onesettings-native-'+[guid]::NewGuid().ToString('N'))) $PSScriptRoot +function Public([string]$Label,[string[]]$Arguments,[int]$Expected=0,[switch]$NoReport){ + $all=@('-NoLogo','-NoProfile','-NonInteractive','-File',"$repo/WELA.ps1",'audit-notifications')+$Arguments + if(-not $NoReport){$all+=@('-ResultsPath',"$root/$Label.json")} + foreach($a in $all){if($a.Contains('"') -or $a.EndsWith('\') -or $a -match '[\x00-\x1f]'){throw 'Ambiguous fixture argument.'}} + $info=[Diagnostics.ProcessStartInfo]::new();$info.FileName=$engine;$info.Arguments=(@($all|ForEach-Object{'"'+$_+'"'}) -join ' ');$info.UseShellExecute=$false;$info.CreateNoWindow=$true;$info.RedirectStandardOutput=$true;$info.RedirectStandardError=$true + $process=[Diagnostics.Process]::new();$process.StartInfo=$info;$started=$false + try{ + if(-not $process.Start()){throw 'Owned public child did not start.'};$started=$true + $stdout=[WelaOneSettingsFixturePipe]::Read($process.StandardOutput);$stderr=[WelaOneSettingsFixturePipe]::Read($process.StandardError) + if(-not $process.WaitForExit(180000)){throw "Public $Label exceeded three minutes."} + if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($stdout,$stderr),5000)){throw 'Public output drain exceeded five seconds.'} + $text=$stdout.Result+"`n"+$stderr.Result;[IO.File]::WriteAllText((Join-Path $root ($Label+'.txt')),$text) + Assert ($process.ExitCode -eq $Expected) "Public $Label exit $($process.ExitCode), expected $Expected : $text" + if(-not $NoReport){$report=Get-Content -LiteralPath "$root/$Label.json" -Raw|ConvertFrom-Json;Assert ($report.ExitCode -eq $Expected -and $report.Scope -ceq 'audit-notifications' -and $report.EventGeneration -match 'Not verified') 'Actual public result agrees with process exit and keeps event generation unverified.';return $report} + }finally{ + if($started){$exited=$false;try{$exited=$process.HasExited}catch{$script:errors+=$_.Exception.Message};if(-not $exited){try{$process.Kill()}catch{$script:errors+=$_.Exception.Message};try{$exited=$process.WaitForExit(5000)}catch{$script:errors+=$_.Exception.Message}};if(-not $exited){$script:errors+='Owned process termination is unconfirmed.'}} + try{$process.Dispose()}catch{$script:errors+=$_.Exception.Message} + } +} +$before=Policy;$channelBefore=Get-WelaNativeChannel $channelName;$rawBefore=if(Test-WelaNativeChannelSnapshot $channelBefore){Read-Raw $channelName}else{$null};$unselected=Unselected;$masks=Get-WelaEffectiveAuditPolicy +Assert ($masks.Count -eq 59) 'Original59 native audit masks are observed.' +Save 'original.json' ([ordered]@{Policy=$before;Channel=$channelBefore;ChannelXml=$rawBefore;Unselected=$unselected;Masks=$masks;Host=$hostState;Engine=$PSVersionTable.PSVersion.ToString()}) +$base=@('-NotificationControl','OneSettings') +function Preserve($PolicyState,$ChannelXml){Assert ((Key (Policy)) -ceq (Key $PolicyState)) 'Selected typed policy preserved.';if($ChannelXml){Assert ((Read-Raw $channelName) -ceq $ChannelXml) 'Selected channel complete XML preserved.'};Assert ((Key (Unselected)) -ceq (Key $unselected)) 'Other typed policy values, descendants, owner/group/DACL, channels, warning/fail policy and services preserved.';Assert ((Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $masks)) 'All59 effective native audit masks preserved.'} +try{ + if($hostState.Build -eq 26100){ + $plan=Public 'unsupported-plan' ($base+@('-NotificationAction','Plan')) 1 + Assert ($plan.Plan.Count -eq 1 -and $plan.Plan[0].Status -ceq 'Unknown' -and $plan.Plan[0].Before.Diagnostic -match 'lacks reviewed source support') 'Server2025 uses the explicit reviewed-source refusal.' + foreach($dry in @($false,$true)){$label=if($dry){'unsupported-dry'}else{'unsupported-configure'};$options=$base+@('-NotificationAction','Configure','-Auto','-BackupPath',"$root/$label-backup");if($dry){$options+='-DryRun'};$report=Public $label $options 1;Assert ($report.Results.Count -eq 1 -and $report.Results[0].Status -ceq 'Failed' -and -not(Test-Path "$root/$label-backup/before.jsonl")) 'Unsupported native Configure/DryRun writes no selected policy or journal.';Preserve $before $rawBefore} + Public 'unsupported-dependent' ($base+@('-NotificationAction','Configure','-EnablePrivacyChannel','-Auto','-BackupPath',"$root/unsupported-dependent-backup")) 1 -NoReport + Assert (-not(Test-Path "$root/unsupported-dependent-backup")) 'Unsupported dependent-channel request refuses before output journal.' + Preserve $before $rawBefore + }else{ + $definition=@(Get-WelaNotificationDefinitions|Where-Object Id -CEQ OneSettings)[0];$initial=Get-WelaNotificationSnapshot $definition + Assert ($initial.Status -ceq 'Supported' -and $before.KeyExists -and $rawBefore) 'Real2022 ADMX, existing key and channel prerequisites are required.' + Save 'definition.json' $initial.DefinitionEvidence + $policyTouched=$true;if((Policy).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $valueName} + $channelTouched=$true;$null=Invoke-WelaNative wevtutil.exe @('sl',$channelName,'/e:false','/ms:1048576') + $seed=Policy;$seedChannel=Get-WelaNativeChannel $channelName;$seedXml=Read-Raw $channelName + Save 'prepared.json' @{Policy=$seed;Channel=$seedChannel;ChannelXml=$seedXml} + Assert ($seedChannel.MaximumSizeInBytes -ge 1048576) 'Native prepared buffer remains above the technical minimum; use actual rounded readback.' + $plan=Public 'plan' ($base+@('-NotificationAction','Plan','-EnablePrivacyChannel')) + Assert ($plan.Plan.Count -eq 1 -and $plan.Plan[0].Status -ceq 'ChangeRequired' -and -not $plan.Plan[0].Before.Policy.ValueExists -and $plan.PrivacyChannelPlan.Count -eq 1) 'Public Plan observes actual absence and the explicit channel dependency.' + $dry=Public 'dry' ($base+@('-NotificationAction','Configure','-EnablePrivacyChannel','-Auto','-DryRun','-BackupPath',"$root/dry-backup")) + Assert ($dry.DryRun -and $dry.Results.Count -eq 2 -and @($dry.Results|Where-Object Status -CNE Skipped).Count -eq 0 -and -not(Test-Path "$root/dry-backup")) 'DryRun previews both selected operations without a journal.' + Public 'whatif' ($base+@('-NotificationAction','Configure','-EnablePrivacyChannel','-Auto','-BackupPath',"$root/whatif-backup",'-WhatIf')) 1 -NoReport + Assert (-not(Test-Path "$root/whatif-backup")) 'Unrecognized preview refuses before any command dispatch.' + Preserve $seed $seedXml + $plain=Public 'policy' ($base+@('-NotificationAction','Configure','-Auto','-BackupPath',"$root/policy-backup")) + $policyAfter=Policy + Assert ($plain.Results.Count -eq 1 -and $plain.Results[0].Status -ceq 'Applied' -and $policyAfter.Type -ceq 'DWord' -and $policyAfter.Value -eq 1 -and (Key $plain.Results[0].Before.Policy) -ceq (Key $seed) -and (Key $plain.Results[0].After.Policy) -ceq (Key $policyAfter)) 'Plain public Configure writes only the exact OneSettingsDWORD1 and binds native before/after.' + Assert ($plain.PrivacyChannelPlan.Count -eq 0 -and (Read-Raw $channelName) -ceq $seedXml) 'Policy-only Configure leaves the disabled channel unchanged.' + $journal=@(Get-Content "$root/policy-backup/before.jsonl"|ConvertFrom-Json);Assert ($journal.Count -eq 1 -and $journal[0].Target.Path -ceq $path -and $journal[0].Target.Name -ceq $valueName -and (Key $journal[0].Before.Policy) -ceq (Key $seed)) 'Original missing value is preserved exactly in the durable journal.' + $dependent=Public 'dependent' ($base+@('-NotificationAction','Configure','-EnablePrivacyChannel','-Auto','-BackupPath',"$root/dependent-backup")) + $enabled=Get-WelaNativeChannel $channelName;$enabledXml=Read-Raw $channelName + Save 'enabled.json' @{Policy=Policy;Channel=$enabled;ChannelXml=$enabledXml} + Assert ($dependent.Results.Count -eq 2 -and $dependent.Results[0].Status -ceq 'AlreadyCompliant' -and $dependent.Results[1].Status -ceq 'Applied') 'Verified producer policy precedes one actual dependent channel change.' + $expected=[xml]$seedXml;$expected.DocumentElement.SetAttribute('enabled','true') + Assert ($enabled.IsEnabled -and $enabled.MaximumSizeInBytes -eq $seedChannel.MaximumSizeInBytes -and $enabledXml -ceq $expected.OuterXml) 'Only channel Enabled changes; existing larger buffer, retention, full descriptor and metadata survive.' + $journal=@(Get-Content "$root/dependent-backup/before.jsonl"|ConvertFrom-Json);Assert ($journal.Count -eq 1 -and $journal[0].Kind -ceq 'NativeChannel' -and $journal[0].Target.Channel -ceq $channelName -and (Key $journal[0].Before) -ceq (Key $seedChannel)) 'Channel-only journal contains its exact native original configuration.' + Preserve $policyAfter $enabledXml + $repeat=Public 'repeat' ($base+@('-NotificationAction','Configure','-EnablePrivacyChannel','-Auto','-BackupPath',"$root/repeat-backup")) + Assert ($repeat.Results.Count -eq 2 -and @($repeat.Results|Where-Object Status -CNE AlreadyCompliant).Count -eq 0 -and -not(Test-Path "$root/repeat-backup/before.jsonl")) 'Repeated public Configure is idempotent with no native write journal.' + Preserve $policyAfter $enabledXml + $null=New-ItemProperty -LiteralPath $path -Name $valueName -Value 0 -PropertyType DWord -Force + $null=Invoke-WelaNative wevtutil.exe @('sl',$channelName,'/e:false') + $zero=Policy;$disabled=Get-WelaNativeChannel $channelName + $combined=Public 'combined' ($base+@('-NotificationAction','Configure','-EnablePrivacyChannel','-Auto','-BackupPath',"$root/combined-backup")) + Assert ($combined.Results.Count -eq 2 -and @($combined.Results|Where-Object Status -CNE Applied).Count -eq 0 -and (Policy).Value -eq 1 -and (Read-Raw $channelName) -ceq $enabledXml) 'Actual combined call applies policy then channel fromDWORD0/disabled.' + $journal=@(Get-Content "$root/combined-backup/before.jsonl"|ConvertFrom-Json) + Assert ($journal.Count -eq 2 -and $journal[0].Kind -ceq 'Registry' -and $journal[1].Kind -ceq 'NativeChannel' -and (Key $journal[0].Before.Policy) -ceq (Key $zero) -and (Key $journal[1].Before) -ceq (Key $disabled)) 'Combined durable originals prove the exact two-control ordering and typed preparation.' + foreach($case in @(@{Id='wrong-type';Type='String';Value='owned-invalid-dword'},@{Id='unknown-value';Type='DWord';Value=2})){ + Remove-ItemProperty -LiteralPath $path -Name $valueName;$null=New-ItemProperty -LiteralPath $path -Name $valueName -PropertyType $case.Type -Value $case.Value + $null=Invoke-WelaNative wevtutil.exe @('sl',$channelName,'/e:false');$invalid=Policy;$invalidXml=Read-Raw $channelName + $refused=Public $case.Id ($base+@('-NotificationAction','Configure','-Auto','-BackupPath',"$root/$($case.Id)-backup")) 1 + Assert ($refused.Results.Count -eq 1 -and $refused.Results[0].Status -ceq 'Failed' -and -not(Test-Path "$root/$($case.Id)-backup/before.jsonl")) 'Actual unreviewed type/value fails without coercion or journal.' + Public ($case.Id+'-dependent') ($base+@('-NotificationAction','Configure','-EnablePrivacyChannel','-Auto','-BackupPath',"$root/$($case.Id)-dependent-backup")) 1 -NoReport + Assert (-not(Test-Path "$root/$($case.Id)-dependent-backup")) 'Unsupported producer prevents dependent channel action.' + Preserve $invalid $invalidXml + } + } + Save 'completed.json' @{Assertions=$count;Build=$hostState.Build;ActualAppliedControls=$(if($hostState.Build -eq 20348){4}else{0});EventGenerationVerified=$false;Scope='Public selected policy/channel settings only; no telemetry, forwarding or Sigma credit.'} +}catch{$primary=$_} +finally{ + if($policyTouched){try{if((Policy).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $valueName};if($before.ValueExists){$null=New-ItemProperty -LiteralPath $path -Name $valueName -Value $before.Value -PropertyType $before.Type}}catch{$errors+='Policy restoration: '+$_.Exception.Message}} + if($channelTouched){try{$null=Invoke-WelaNative wevtutil.exe @('sl',$channelName,('/e:'+$channelBefore.IsEnabled.ToString().ToLowerInvariant()),('/ms:'+$channelBefore.MaximumSizeInBytes),('/ca:'+$channelBefore.SecurityDescriptor))}catch{$errors+='Channel restoration: '+$_.Exception.Message}} + $policyOk=$false;$channelOk=$false;$otherOk=$false;$masksOk=$false;$after=$null;$rawAfter=$null;$otherAfter=$null;$maskAfter=$null + try{$after=Policy;$policyOk=(Key $after) -ceq (Key $before)}catch{$errors+='Policy readback: '+$_.Exception.Message} + try{$rawAfter=if($rawBefore){Read-Raw $channelName}else{$null};$channelOk=$rawAfter -ceq $rawBefore -and (Key (Get-WelaNativeChannel $channelName)) -ceq (Key $channelBefore)}catch{$errors+='Channel readback: '+$_.Exception.Message} + try{$otherAfter=Unselected;$otherOk=(Key $otherAfter) -ceq (Key $unselected)}catch{$errors+='Unselected readback: '+$_.Exception.Message} + try{$maskAfter=Get-WelaEffectiveAuditPolicy;$masksOk=(Masks $maskAfter) -ceq (Masks $masks)}catch{$errors+='Audit mask readback: '+$_.Exception.Message} + Save 'cleanup.json' @{Failure=[string]$primary;Errors=$errors;PolicyRestored=$policyOk;ChannelRestored=$channelOk;UnselectedPreserved=$otherOk;All59MasksPreserved=$masksOk;Complete=($policyOk -and $channelOk -and $otherOk -and $masksOk -and -not $errors.Count);Policy=$after;ChannelXml=$rawAfter;Unselected=$otherAfter;Masks=$maskAfter} +} +$artifacts=@(Get-ChildItem -LiteralPath $root -File -Recurse|ForEach-Object{[ordered]@{Path=$_.FullName.Substring($root.Length+1);Sha256=(Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()}}) +$sources=@('WELA.ps1','scripts/AuditNotifications.ps1','scripts/Configuration.ps1','scripts/NativeChannelConfiguration.ps1','scripts/WefArrival.ps1','modules/NativeProviders.psm1','modules/NativeChannelAccess.psm1','modules/EventLogSettings.psm1','modules/AuditProfiles.psm1','tests/OneSettingsConfigure.Windows.Tests.ps1')|ForEach-Object{[ordered]@{Path=$_;Sha256=(Get-FileHash -LiteralPath (Join-Path $repo $_) -Algorithm SHA256).Hash.ToLowerInvariant()}} +Save 'manifest.json' @{Status=$(if($primary -or -not $policyOk -or -not $channelOk -or -not $otherOk -or -not $masksOk -or $errors.Count){'Failed'}else{'Passed'});Commit=$env:GITHUB_SHA;Host=$hostState;Engine=$PSVersionTable.PSVersion.ToString();Assertions=$count;Artifacts=$artifacts;Sources=@($sources);EventGenerationVerified=$false;ForwardingVerified=$false;ReadyRuleCredit=0} +if($primary){throw $primary};if(-not $policyOk -or -not $channelOk -or -not $otherOk -or -not $masksOk -or $errors.Count){throw ('OneSettings native cleanup was not verified: '+($errors -join '; '))} +Write-Host "PASS: $count public native OneSettings assertions; selected typed policy and full channel restored, unrelated state preserved." +exit 0 diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 06e10ead..967055bd 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,8 @@ **改善:** +- OneSettingsポリシーと明示的なPrivacyチャネル設定の公開CLIを実機検証します。Server 2022で実際の適用、型付き復元記録と再読取、冪等性、不正値の拒否を確認し、Server 2025の既存の拒否を維持します。両PowerShellで厳密な後処理を検証し、イベント生成やSigma対応は主張しません。通知コントロール省略時の引数渡しを修正し、既定のAuditは両項目を読み取り、項目未選択のConfigureは非ゼロで失敗します。(関連 #378) (@Shirofune-Security) + - 受信・ドメイン監査を明示的に選択する `ntlm-auditing` Audit/Plan/Configure を追加しました。受信監査 DWORD2 と実際のDC上のドメイン監査 DWORD7 のみを設定し、旧値2の意味を推測せず識別します。不明な値・ホストや設定の変化を拒否し、型付き変更前記録・再読取・部分失敗を区別します。ネイティブテストで受信設定、非DCのスキップ、無関係な設定の保持と復元を検証します。(関連 #363) (@Shirofune-Security) - 読み取り専用の `wef-query` を追加しました。選択したソースのQueryListをそのままネイティブAPIで実行し、Select/Suppressの動作、チャネル別の失敗診断、上限付きの一致イベントXML、実際の操作者・ホスト・ソースの整合性を確認します。空の結果、アクセス拒否、未存在、不正クエリ、上限到達、状態変化を区別し、破棄可能なWindows環境で実イベントの選択・抑制と標準ユーザーの拒否、完全な後片付けを検証します。転送サービスのアクセス権、配送、Sigmaの準備完了は推定しません。 (Related #368) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index f8aae221..c3d77b63 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,8 @@ **Improvements:** +- Add native public OneSettings policy and explicit Privacy-channel configuration acceptance: actual apply, typed journals/readback, idempotence and invalid-value refusals on Server 2022; preserve the existing Server 2025 refusal. Both PowerShell engines verify exact fixture cleanup without event or Sigma claims. Fix omitted notification-control dispatch so default Audit reads both controls and Configure without a selection fails with a nonzero exit. (Related #378) (@Shirofune-Security) + - Add `ntlm-auditing` Audit/Plan/Configure with explicit Incoming/Domain/Both selection. Configure only incoming audit DWORD2 and actual-DC domain audit DWORD7, identify legacy domain2 without invented semantics, refuse unknown values and host/state drift, and retain separate typed journals/readback/partial outcomes. Native tests verify incoming changes, non-DC skips and exact preservation of unrelated settings. (Related #363) (@Shirofune-Security) - Added read-only `wef-query` preflight for one exact selected source QueryList, with strict native Select/Suppress execution, separate per-channel failure diagnostics, bounded matching XML and actual caller/host/source guards. Empty, denied, missing, invalid, capped and drifted results remain distinct; disposable native tests cover real record selection/suppression and standard-user denial with exact cleanup. No forwarding-service access, delivery or Sigma credit is inferred. (Related #368) (@Shirofune-Security)