diff --git a/.github/workflows/wmi-probe.yml b/.github/workflows/wmi-probe.yml index adc50ab8..5b6efa7c 100644 --- a/.github/workflows/wmi-probe.yml +++ b/.github/workflows/wmi-probe.yml @@ -8,7 +8,7 @@ permissions: contents: read jobs: wmi-probe: - timeout-minutes: 15 + timeout-minutes: 20 strategy: fail-fast: false matrix: diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index fbb5f432..a6ab89ad 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,8 @@ **改善:** +- `wmi-probe` の親プロセスとワーカーの操作時刻を Windows の高精度 UTC 時計に統一しました。時計情報と起動・完了時刻を検証し、イベントの許容時間範囲を広げずに正確な照合を維持します。ミリ秒未満の境界テストとネイティブ公開 CLI の反復テストを追加しました。(@Shirofune-Security) + - 読み取り専用の `channel-read` を追加し、実際の実行トークンで選択したWindows標準ローカルログを確認します。メタデータやACLとは独立して拒否・不在・空・イベント読取を区別し、トークンと環境の変化を検出して保護された限定レポートに記録します。Windows設定の変更やSigma利用可能数への加算は行いません。使い捨て標準ユーザーでServer 2022/2025・PowerShell 5.1/7の拒否と読取を検証します。 (#432) (@Shirofune-Security) - 専用 CA 監査設定の再起動待ちを確認し、明示的に再開する `adcs-resume` を追加。元の記録・結果、出典、現在の CA と実行者、永続的な意図記録、変更直前・最終確認により古い計画の再実行を拒否し、観測した設定を保持します。ドライランと失敗を区別し、イベントや Sigma の証明は加算しません。使い捨て CA テストでは最初の再起動拒否のみを注入し、公開 CLI による実際の再起動と要求イベントを検証します。既存の専用 CA Configure のドライラン引数制限も修正しました。 (#431) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f47af61..c13c6111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Improvements:** +- Fixed `wmi-probe` operation timing to use the native precise UTC clock consistently in the parent and worker. Explicit clock receipts and launch/completion bounds preserve exact event correlation; sub-millisecond boundary tests and repeated native public-CLI runs cover timing failures without widening the accepted interval. (@Shirofune-Security) + - Added read-only `channel-read` to test actual current-token access to selected built-in local logs. Native query outcomes distinguish denied, missing, empty and observed-event reads independently of metadata/ACL inspection; token/context checks and bounded private reports preserve uncertainty without changing Windows settings or granting Sigma credit. Disposable standard-user denial/read tests cover Server 2022/2025 and PowerShell 5.1/7. (#432) (@Shirofune-Security) - Added `adcs-resume` to review and explicitly resume a dedicated pending CA auditing restart. Original journal/results, source and current CA/operator fingerprints, durable intent receipts and fresh/final checks prevent stale-plan replay and preserve observed settings. Dry-run and failed attempts remain explicit, with no event/Sigma credit. Disposable CA tests inject the initial refusal then verify an actual public-CLI restart and request events. Also fixed the existing dedicated CA Configure dry-run CLI guard. (#431) (@Shirofune-Security) diff --git a/docs/wmi-probe.md b/docs/wmi-probe.md index 0ac5bfb8..9b22b8d7 100644 --- a/docs/wmi-probe.md +++ b/docs/wmi-probe.md @@ -17,7 +17,7 @@ Use native 64-bit Windows PowerShell 5.1 or PowerShell7 on a reviewed Windows11/ The process token observation includes user SID/name, logon-session LUID, authentication/impersonation information, group SIDs with native attributes, and privilege LUIDs/attributes. Disabled and deny-only groups do not establish a matching success audit ACE. The shared descriptor reader temporarily enables an already assigned `SeSecurityPrivilege` and restores it; the probe verifies its token is unchanged afterward. It does not assign rights. A runtime-created self-impersonation token is accepted only when its SID, logon LUID, complete group attributes and privilege attributes equal the process token; its source/type remain recorded. Different or restricted tokens are refused before a child is launched. No token is reverted or replaced. An ACE match alone does not prove effective namespace access; the fixed read and event observations are separate. -The worker uses the same PowerShell executable as WELA with `-NoProfile -NonInteractive`. It connects only to `\\.\` and executes `SELECT Name FROM __Namespace WHERE Name='WelaReadProbe_'`. It must return zero rows. This avoids retrieving a namespace inventory, creating an instance or invoking a provider method. The child has a twenty-second limit; a stuck owned child is terminated. The configured 1–30-second timeout is the subsequent event-arrival polling limit, not a deadline for all host/descriptor observations. Ordinary native prerequisite APIs can still wait on WMI/Windows availability. +The worker uses the same PowerShell executable as WELA with `-NoProfile -NonInteractive`. It connects only to `\\.\` and executes `SELECT Name FROM __Namespace WHERE Name='WelaReadProbe_'`. It must return zero rows. This avoids retrieving a namespace inventory, creating an instance or invoking a provider method. Parent launch/observation and worker start/completion timestamps use Windows [GetSystemTimePreciseAsFileTime](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtimepreciseasfiletime). The worker records that clock contract; missing/coarse-clock receipts, times preceding launch, backwards intervals and future completion times are refused. This avoids mixing a coarse .NET Framework clock with higher-resolution event timestamps. The event interval remains exact, with no positive-match padding; a clock change or missing event stays unverified. The child has a twenty-second limit; a stuck owned child is terminated. The configured 1–30-second timeout is the subsequent event-arrival polling limit, not a deadline for all host/descriptor observations. Ordinary native prerequisite APIs can still wait on WMI/Windows availability. Source/helper and PowerShell executable fingerprints are recorded and checked before/after, alongside full descriptor, host, channel and policy state. The worker records its own before/after token; its user/logon/group context must match the parent, and its privilege state must remain unchanged. A changed state, denied read, failed child, missing evidence, unknown schema or query cap remains `Unverified` with exit1 and available recovery evidence. `LocalNamespaceAccessObserved` requires successful verification; it grants **zero usable Sigma-rule credit**. @@ -33,7 +33,7 @@ The Security query accepts at most255 candidates; reaching the256-record cap fai The fixture suite exercises the public report flow with native boundaries explicitly mocked, including source/event mismatches, denied reads, missing prerequisites, state drift, caps and protected new output. Public CLI guards are checked separately. Native code is never dot-sourced from those mock fixtures. -`tests/WmiProbe.Windows.Tests.ps1 -AllowDisposableNamespaceWrite` requires a disposable GitHub-hosted workgroup Server2022/2025. It creates exactly one random `root\WelaReadTest_` namespace with CreateOnly, uses the real existing SACL writer with the ASD root-default definition on that owned namespace, temporarily enables Other Object Access success auditing and precedence, and invokes the public CLI. It requires correlated raw native4662 evidence and unchanged namespace security. It then restores the original subcategory and exact typed precedence, verifies all59 audit masks, and deletes only the namespace it created. Failures preserve the primary exception and a private cleanup receipt; restoration failures fail the job. Private temporary evidence remains on the disposable runner until that VM is discarded. +`tests/WmiProbe.Windows.Tests.ps1 -AllowDisposableNamespaceWrite` requires a disposable GitHub-hosted workgroup Server2022/2025. It creates exactly one random `root\WelaReadTest_` namespace with CreateOnly, uses the real existing SACL writer with the ASD root-default definition on that owned namespace, temporarily enables Other Object Access success auditing and precedence, and invokes the public CLI three independent times by default. Every run must pass; there are no success-on-retry semantics. It requires correlated raw native4662 evidence and unchanged namespace security. It then restores the original subcategory and exact typed precedence, verifies all59 audit masks, and deletes only the namespace it created. Failures preserve the primary exception and a private cleanup receipt; restoration failures fail the job. Private temporary evidence remains on the disposable runner until that VM is discarded. The native matrix passed on Server 2022 and Server 2025 under both Windows PowerShell 5.1 and PowerShell 7 in [run 35539528097](https://github.com/Yamato-Security/WELA/actions/runs/35539528097), at implementation commit `89aa345`. Each combination passed the then-current 108 fixture assertions, 10 public CLI checks and 19 native assertions, including two matching WMI namespace-read records and exact cleanup. The raw records use `ObjectType=WMI Namespace`, `ObjectServer=WMI`, read mask `0x1` and event version 0. This evidence verifies those disposable cases; inspect the final-head workflow before merging later changes. Windows11, production namespaces, domain/DC/CA token behavior, remote WMI, inheritance propagation, forwarding and backend execution remain separate acceptance work. No domain infrastructure is required by this fixture. diff --git a/scripts/WmiProbe.ps1 b/scripts/WmiProbe.ps1 index 52d06d28..7d2bd03f 100644 --- a/scripts/WmiProbe.ps1 +++ b/scripts/WmiProbe.ps1 @@ -59,6 +59,12 @@ function Get-WelaWmiProbeWatermark { $record=Get-WinEvent -LogName Security -MaxEvents 1 -ErrorAction Stop try{if($null -eq $record.RecordId -or $record.RecordId -lt 1){throw 'Unknown Security record boundary.'};[long]$record.RecordId}finally{$record.Dispose()} } +function Assert-WelaWmiProbeInterval { + param($Operation,[DateTimeOffset]$LaunchedUtc,[DateTimeOffset]$ObservedUtc) + $start=ConvertTo-WelaArrivalUtc $Operation.StartedUtc;$end=ConvertTo-WelaArrivalUtc $Operation.CompletedUtc + if($Operation.Clock -cne 'GetSystemTimePreciseAsFileTime' -or $LaunchedUtc -gt $ObservedUtc -or $start -lt $LaunchedUtc -or $start -gt $end -or ($end-$start).TotalSeconds -gt 20 -or $end -gt $ObservedUtc){throw 'Invalid precise fixed worker time interval.'} + [pscustomobject]@{Start=$start;End=$end} +} function Start-WelaWmiProbeRead { param($State) $fresh=Get-WelaWmiProbeState $State.Namespace @@ -71,6 +77,7 @@ function Start-WelaWmiProbeRead { $info.StandardOutputEncoding=[Text.UTF8Encoding]::new($false,$true);$info.StandardErrorEncoding=[Text.UTF8Encoding]::new($false,$true) $process=$null try{ + $launch=[DateTimeOffset][Wela.WmiProbe.Native]::UtcNow() $process=[Diagnostics.Process]::Start($info);$output=$process.StandardOutput.ReadToEndAsync();$errors=$process.StandardError.ReadToEndAsync() if(-not $process.WaitForExit(20000)){$process.Kill();$null=$process.WaitForExit(1000);throw 'The fixed WMI read worker exceeded twenty seconds.'} if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($output,$errors),1000)){throw 'The fixed read output did not complete.'} @@ -79,8 +86,8 @@ function Start-WelaWmiProbeRead { if($process.ExitCode -ne 0 -or $diagnostic){throw ('Fixed local WMI read failed: '+$diagnostic)} $operation=ConvertFrom-WelaArrivalJson $text if($operation.Namespace -cne $State.Namespace -or $operation.ProcessId -ne $process.Id -or $operation.ExpectedAccessMask -ne 1 -or $operation.ReturnedRows -ne 0 -or $operation.Query -cnotmatch "^SELECT Name FROM __Namespace WHERE Name='WelaReadProbe_[a-f0-9]{32}'$"){throw 'Unexpected fixed worker response.'} - $start=ConvertTo-WelaArrivalUtc $operation.StartedUtc;$end=ConvertTo-WelaArrivalUtc $operation.CompletedUtc - if($start -gt $end -or ($end-$start).TotalSeconds -gt 20 -or $end -gt [DateTimeOffset]::UtcNow){throw 'Invalid fixed worker time interval.'} + $interval=Assert-WelaWmiProbeInterval $operation $launch ([DateTimeOffset][Wela.WmiProbe.Native]::UtcNow()) + $start=$interval.Start;$end=$interval.End # Older PowerShell7 JSON readers can materialize UTC strings as DateTime. $operation.StartedUtc=$start.UtcDateTime.ToString('o');$operation.CompletedUtc=$end.UtcDateTime.ToString('o') if((Get-WelaWmiProbeTokenKey $operation.BeforeToken) -cne (Get-WelaWmiProbeTokenKey $operation.AfterToken) -or (Get-WelaWmiProbeTokenKey $operation.BeforeToken -AuthorizationOnly) -cne (Get-WelaWmiProbeTokenKey $State.Token -AuthorizationOnly)){throw 'Worker token differs from the observed caller or changed during access.'} diff --git a/scripts/WmiProbeNative.cs b/scripts/WmiProbeNative.cs index 93d21ded..0594f0f3 100644 --- a/scripts/WmiProbeNative.cs +++ b/scripts/WmiProbeNative.cs @@ -12,6 +12,8 @@ namespace Wela.WmiProbe { public Group[] Groups; public Privilege[] Privileges; } public static class Native { + [DllImport("kernel32.dll",ExactSpelling=true)] static extern void GetSystemTimePreciseAsFileTime(out long value); + public static DateTime UtcNow() {long value;GetSystemTimePreciseAsFileTime(out value);return DateTime.FromFileTimeUtc(value);} [StructLayout(LayoutKind.Sequential)] struct Luid {public uint Low; public int High;} [StructLayout(LayoutKind.Sequential)] struct Statistics {public Luid TokenId,AuthenticationId;public long Expiration;public int Type,Level;public uint Charged,Available,Groups,Privileges;public Luid Modified;} [StructLayout(LayoutKind.Sequential)] struct SidAndAttributes {public IntPtr Sid;public uint Attributes;} diff --git a/scripts/WmiProbeWorker.ps1 b/scripts/WmiProbeWorker.ps1 index e3625f95..8be27e35 100644 --- a/scripts/WmiProbeWorker.ps1 +++ b/scripts/WmiProbeWorker.ps1 @@ -16,7 +16,7 @@ $options.Impersonation=[System.Management.ImpersonationLevel]::Impersonate $options.Timeout=[TimeSpan]::FromSeconds(10) $scope=New-Object System.Management.ManagementScope -ArgumentList ('\\.\'+$Namespace),$options $searcher=$null;$rows=$null -$started=[DateTime]::UtcNow +$started=[Wela.WmiProbe.Native]::UtcNow() try { $scope.Connect() $enumeration=New-Object System.Management.EnumerationOptions @@ -24,8 +24,8 @@ try { $searcher=New-Object System.Management.ManagementObjectSearcher -ArgumentList $scope,([System.Management.ObjectQuery]::new($query)),$enumeration $rows=$searcher.Get();$count=0 foreach($row in $rows){try{$count++;if($count -gt 0){throw 'The random nonexistent namespace filter unexpectedly matched an instance.'}}finally{$row.Dispose()}} - $completed=[DateTime]::UtcNow + $completed=[Wela.WmiProbe.Native]::UtcNow() $after=[Wela.WmiProbe.Native]::Snapshot() if((Get-WelaWmiProbeTokenKey $before) -cne (Get-WelaWmiProbeTokenKey $after)){throw 'Worker token changed during the fixed query.'} - [pscustomobject]@{Namespace=$Namespace;Query=$query;ExpectedAccessMask=1;ProcessId=$PID;StartedUtc=$started.ToString('o');CompletedUtc=$completed.ToString('o');BeforeToken=$before;AfterToken=$after;ReturnedRows=$count;Operation='Fixed local read; provider completion is separate from audited namespace access'}|ConvertTo-Json -Depth 10 -Compress + [pscustomobject]@{Namespace=$Namespace;Query=$query;ExpectedAccessMask=1;ProcessId=$PID;StartedUtc=$started.ToString('o');CompletedUtc=$completed.ToString('o');Clock='GetSystemTimePreciseAsFileTime';BeforeToken=$before;AfterToken=$after;ReturnedRows=$count;Operation='Fixed local read; provider completion is separate from audited namespace access'}|ConvertTo-Json -Depth 10 -Compress }finally{if($rows){$rows.Dispose()};if($searcher){$searcher.Dispose()}} diff --git a/tests/WmiProbe.Tests.ps1 b/tests/WmiProbe.Tests.ps1 index d818dc5d..796ec1ed 100644 --- a/tests/WmiProbe.Tests.ps1 +++ b/tests/WmiProbe.Tests.ps1 @@ -35,8 +35,32 @@ $token=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1001';Name='LAB\Reader';Authenticat $descriptor=[pscustomobject]@{ControlFlags=32788;Owner=$null;Group=$null;DACL=@();SACL=@([pscustomobject]@{AceType=2;AceFlags=64;AccessMask=1;Trustee=[pscustomobject]@{SIDString='S-1-1-0'}})} $state=[pscustomobject][ordered]@{Namespace='root\default';Computer='LAB';Service='Running';Host=[pscustomobject]@{Status='Observed';Build=26100;ProductType=3;DomainJoined=$false};Token=$token;Descriptor=[pscustomobject]@{Namespace='root\default';DescriptorJson=($descriptor|ConvertTo-Json -Depth 10 -Compress);DescriptorMof='fixture descriptor'};AuditMask=1;Precedence=[pscustomobject]@{ValueExists=$true;Type='DWord';Value=1};Channel=[pscustomobject]@{Name='Security';Enabled=$true;SecurityDescriptor='O:SYG:SYD:(A;;0x1;;;SY)'};Engine='/fixture';EngineHash=('a'*64);Sources='fixture-sources'} $operation=[pscustomobject]@{Namespace='root\default';StartedUtc='2025-01-02T03:04:05.1234500Z';CompletedUtc='2025-01-02T03:04:06.1234500Z';ExpectedAccessMask=1;SecurityRecordIdBefore=100;BeforeToken=$token;AfterToken=$token} +# Clock evidence and exact bounds: coarse or padded intervals cannot authorize events. +$timed=Clone $operation;$timed|Add-Member NoteProperty Clock 'GetSystemTimePreciseAsFileTime' +$launch=[DateTimeOffset]'2025-01-02T03:04:05Z';$observed=[DateTimeOffset]'2025-01-02T03:04:07Z' +$interval=Assert-WelaWmiProbeInterval $timed $launch $observed +Assert ($interval.Start.UtcDateTime.Ticks -eq ([DateTimeOffset]'2025-01-02T03:04:05.1234500Z').UtcDateTime.Ticks) 'Precise fractional timestamp survives normalization.' +foreach($case in @('MissingClock','CoarseClock','Reversed','BeforeLaunch','Future','Overlong','ParentReversed')){ + $bad=Clone $timed;$l=$launch;$o=$observed + switch($case){ + MissingClock {$bad.PSObject.Properties.Remove('Clock')} + CoarseClock {$bad.Clock='DateTime.UtcNow'} + Reversed {$bad.CompletedUtc='2025-01-02T03:04:05Z'} + BeforeLaunch {$bad.StartedUtc='2025-01-02T03:04:04.9999999Z'} + Future {$bad.CompletedUtc='2025-01-02T03:04:07.0000001Z'} + Overlong {$bad.CompletedUtc='2025-01-02T03:04:25.1234501Z';$o=[DateTimeOffset]'2025-01-02T03:05:00Z'} + ParentReversed {$l=$observed;$o=$launch} + } + Reject {Assert-WelaWmiProbeInterval $bad $l $o} 'precise fixed worker time interval' +} +$clockImport=[Wela.WmiProbe.Native].GetMethod('GetSystemTimePreciseAsFileTime',[Reflection.BindingFlags]'NonPublic,Static').GetCustomAttributes([Runtime.InteropServices.DllImportAttribute],$false)[0] +Assert ($clockImport.ExactSpelling -and $clockImport.EntryPoint -ceq 'GetSystemTimePreciseAsFileTime') 'The native UTC clock is bound exactly.' $xml='466200x8020000000000000101SecurityLABS-1-5-21-1-2-3-10010x123WMIroot\default0x1' Assert (Test-WelaWmiProbeEvent $xml $operation $state) 'Exact synthetic WMI namespace event matches.' +foreach($edge in @(@('03:04:05.1234500Z',$true),@('03:04:06.1234500Z',$true),@('03:04:05.1234499Z',$false),@('03:04:06.1234501Z',$false))){ + Assert ((Test-WelaWmiProbeEvent $xml.Replace('03:04:05.5000000Z',$edge[0]) $operation $state) -eq $edge[1]) ('Exact 100ns boundary without positive time padding: '+$edge[0]) +} + $mutations=@( @('4662','4663'),@('>0','>1'),@('>WMI<','>DS<'),@('root\default','root\cimv2'),@('0x1','0x2'),@('0x123','0x124'),@('S-1-5-21-1-2-3-1001','S-1-5-21-1-2-3-1002'),@('>LAB<','>OTHER<'),@('>Security<','>Application<'),@('0x8020000000000000','0x8010000000000000'),@('>101<','>100<'),@('03:04:05.5000000Z','03:04:05.1000000Z'),@('03:04:05.5000000Z','03:04:06.5000000Z'),@('54849625-5478-4994-a5ba-3e3b0328c30d','54849625-5478-4994-a5ba-3e3b0328c30e'),@('Name="AccessMask"','Name="SubjectLogonId"')) foreach($pair in $mutations){$changed=$xml.Replace($pair[0],$pair[1]);Assert ($changed -cne $xml) 'Mutation changed the fixture.';Assert (-not(Test-WelaWmiProbeEvent $changed $operation $state)) ('Reject mismatched '+$pair[0])} diff --git a/tests/WmiProbe.Windows.Tests.ps1 b/tests/WmiProbe.Windows.Tests.ps1 index c320797f..7710653e 100644 --- a/tests/WmiProbe.Windows.Tests.ps1 +++ b/tests/WmiProbe.Windows.Tests.ps1 @@ -1,5 +1,5 @@ # Real native APIs and public CLI. Never dot-source mocked fixture functions. -param([switch]$AllowDisposableNamespaceWrite) +param([switch]$AllowDisposableNamespaceWrite,[ValidateRange(1,5)][int]$ProbeRuns=3) $ErrorActionPreference='Stop' if(-not $AllowDisposableNamespaceWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted' -or $env:OS -ne 'Windows_NT'){throw 'Explicit disposable GitHub-hosted Windows opt-in is required.'} $repo=Split-Path $PSScriptRoot -Parent @@ -43,21 +43,24 @@ try{ Assert (Test-WelaWmiDescriptorPreserved ($before.DescriptorJson|ConvertFrom-Json) ($after.DescriptorJson|ConvertFrom-Json)) 'Owner/group/DACL and existing SACL entries survived.' Set-ItemProperty -LiteralPath $path -Name $name -Type DWord -Value 1 Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 1 -Mode minimum - $out=Join-Path $private 'probe' - $ErrorActionPreference='Continue' - $cli=& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') wmi-probe -WmiProbeAction Run -WmiProbeNamespace $namespace -WmiProbeOutputPath $out -WmiProbeTimeoutSeconds 20 2>&1|Out-String - $code=$LASTEXITCODE;$ErrorActionPreference='Stop' - $manifest=ConvertFrom-WelaArrivalJson ([IO.File]::ReadAllText((Join-Path $out 'manifest.json'))) - # Bounded raw native diagnostics are useful when an unreviewed OS schema differs. - Write-Host ($manifest|ConvertTo-Json -Depth 18) - foreach($file in @(Get-ChildItem -LiteralPath $out -Filter '*.xml' -ErrorAction Stop)){Write-Host ([IO.File]::ReadAllText($file.FullName))} - Assert ($code -eq 0 -and $manifest.Status -eq 'LocalNamespaceAccessObserved' -and $manifest.ExitCode -eq 0) ('Public native probe failed: '+$manifest.Diagnostic+' '+$cli) - Assert ($manifest.Matches -ge 1 -and $manifest.Matches -le 16 -and $manifest.ReadyRuleCredit -eq 0 -and $manifest.PolicyChanges -eq 0 -and $manifest.NamespaceChanges -eq 0) 'Bounded native evidence grants no policy or Sigma claim.' - foreach($artifact in $manifest.Artifacts){Assert ($artifact.Sha256 -ceq (Get-FileHash -LiteralPath (Join-Path $out $artifact.Name)).Hash.ToLowerInvariant()) 'Protected artifact hash verifies.'} - foreach($file in @(Get-ChildItem -LiteralPath $out -Filter 'event-*.xml')){Assert (Test-WelaWmiProbeEvent ([IO.File]::ReadAllText($file.FullName)) $manifest.Operation $manifest.Before) 'Real WMI event passes exact source/namespace/token/mask/time checks.'} - Assert ((Get-WelaWmiNamespaceSnapshot $namespace).DescriptorJson -ceq $after.DescriptorJson) 'Public probe made no namespace security changes.' - Assert ((Get-WelaWmiProbeTokenKey ([Wela.WmiProbe.Native]::Snapshot())) -ceq (Get-WelaWmiProbeTokenKey $originalToken)) 'Native descriptor reads/writes restored caller token state.' - Assert ((Get-Acl -LiteralPath $out).AreAccessRulesProtected) 'Evidence directory blocks inherited broad access.' + for($trial=1;$trial -le $ProbeRuns;$trial++){ + $out=Join-Path $private ('probe-'+$trial) + $ErrorActionPreference='Continue' + $cli=& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') wmi-probe -WmiProbeAction Run -WmiProbeNamespace $namespace -WmiProbeOutputPath $out -WmiProbeTimeoutSeconds 20 2>&1|Out-String + $code=$LASTEXITCODE;$ErrorActionPreference='Stop' + $manifest=ConvertFrom-WelaArrivalJson ([IO.File]::ReadAllText((Join-Path $out 'manifest.json'))) + # Bounded raw native diagnostics are useful when an unreviewed OS schema differs. + Write-Host ($manifest|ConvertTo-Json -Depth 18) + foreach($file in @(Get-ChildItem -LiteralPath $out -Filter '*.xml' -ErrorAction Stop)){Write-Host ([IO.File]::ReadAllText($file.FullName))} + Assert ($code -eq 0 -and $manifest.Status -eq 'LocalNamespaceAccessObserved' -and $manifest.ExitCode -eq 0) ('Public native probe failed: '+$manifest.Diagnostic+' '+$cli) + Assert ($manifest.Operation.Clock -ceq 'GetSystemTimePreciseAsFileTime') 'Actual worker identifies the native precise UTC clock.' + Assert ($manifest.Matches -ge 1 -and $manifest.Matches -le 16 -and $manifest.ReadyRuleCredit -eq 0 -and $manifest.PolicyChanges -eq 0 -and $manifest.NamespaceChanges -eq 0) 'Bounded native evidence grants no policy or Sigma claim.' + foreach($artifact in $manifest.Artifacts){Assert ($artifact.Sha256 -ceq (Get-FileHash -LiteralPath (Join-Path $out $artifact.Name)).Hash.ToLowerInvariant()) 'Protected artifact hash verifies.'} + foreach($file in @(Get-ChildItem -LiteralPath $out -Filter 'event-*.xml')){Assert (Test-WelaWmiProbeEvent ([IO.File]::ReadAllText($file.FullName)) $manifest.Operation $manifest.Before) 'Real WMI event passes exact source/namespace/token/mask/time checks.'} + Assert ((Get-WelaWmiNamespaceSnapshot $namespace).DescriptorJson -ceq $after.DescriptorJson) 'Public probe made no namespace security changes.' + Assert ((Get-WelaWmiProbeTokenKey ([Wela.WmiProbe.Native]::Snapshot())) -ceq (Get-WelaWmiProbeTokenKey $originalToken)) 'Native descriptor reads/writes restored caller token state.' + Assert ((Get-Acl -LiteralPath $out).AreAccessRulesProtected) 'Evidence directory blocks inherited broad access.' + } }catch{$failure=$_} finally{ try{Set-WelaEffectiveAuditPolicy -Guid $guid -Mask $originalPolicies[$guid] -Mode exact}catch{$cleanupErrors+='Audit restoration: '+$_.Exception.Message} @@ -68,5 +71,5 @@ finally{ [pscustomobject]@{Namespace=$namespace;Created=$created;Failure=$(if($failure){$failure.Exception.Message}else{$null});CleanupErrors=$cleanupErrors;Evidence=$private;Complete=($null -eq $failure -and $cleanupErrors.Count -eq 0)}|ConvertTo-Json|Set-Content -LiteralPath (Join-Path $private 'cleanup.json') -Encoding UTF8 } if($failure){throw $failure};if($cleanupErrors.Count){throw ($cleanupErrors -join '; ')} -Write-Host "PASS: $script:count actual native WMI4662/public CLI assertions, original policies restored and owned namespace removed. No remote or Sigma claim." +Write-Host "PASS: $script:count actual native WMI4662/public CLI assertions across $ProbeRuns independent public runs, original policies restored and owned namespace removed. No remote or Sigma claim." $global:LASTEXITCODE=0 diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index b91d1c7d..2756f6c3 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,8 @@ **改善:** +- `wmi-probe` の親プロセスとワーカーの操作時刻を Windows の高精度 UTC 時計に統一しました。時計情報と起動・完了時刻を検証し、イベントの許容時間範囲を広げずに正確な照合を維持します。ミリ秒未満の境界テストとネイティブ公開 CLI の反復テストを追加しました。(@Shirofune-Security) + - 読み取り専用の `channel-read` を追加し、実際の実行トークンで選択したWindows標準ローカルログを確認します。メタデータやACLとは独立して拒否・不在・空・イベント読取を区別し、トークンと環境の変化を検出して保護された限定レポートに記録します。Windows設定の変更やSigma利用可能数への加算は行いません。使い捨て標準ユーザーでServer 2022/2025・PowerShell 5.1/7の拒否と読取を検証します。 (#432) (@Shirofune-Security) - 専用 CA 監査設定の再起動待ちを確認し、明示的に再開する `adcs-resume` を追加。元の記録・結果、出典、現在の CA と実行者、永続的な意図記録、変更直前・最終確認により古い計画の再実行を拒否し、観測した設定を保持します。ドライランと失敗を区別し、イベントや Sigma の証明は加算しません。使い捨て CA テストでは最初の再起動拒否のみを注入し、公開 CLI による実際の再起動と要求イベントを検証します。既存の専用 CA Configure のドライラン引数制限も修正しました。 (#431) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index ccaa1794..91d49131 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,8 @@ **Improvements:** +- Fixed `wmi-probe` operation timing to use the native precise UTC clock consistently in the parent and worker. Explicit clock receipts and launch/completion bounds preserve exact event correlation; sub-millisecond boundary tests and repeated native public-CLI runs cover timing failures without widening the accepted interval. (@Shirofune-Security) + - Added read-only `channel-read` to test actual current-token access to selected built-in local logs. Native query outcomes distinguish denied, missing, empty and observed-event reads independently of metadata/ACL inspection; token/context checks and bounded private reports preserve uncertainty without changing Windows settings or granting Sigma credit. Disposable standard-user denial/read tests cover Server 2022/2025 and PowerShell 5.1/7. (#432) (@Shirofune-Security) - Added `adcs-resume` to review and explicitly resume a dedicated pending CA auditing restart. Original journal/results, source and current CA/operator fingerprints, durable intent receipts and fresh/final checks prevent stale-plan replay and preserve observed settings. Dry-run and failed attempts remain explicit, with no event/Sigma credit. Disposable CA tests inject the initial refusal then verify an actual public-CLI restart and request events. Also fixed the existing dedicated CA Configure dry-run CLI guard. (#431) (@Shirofune-Security)