Collect local WMI namespace audit evidence with a fixed read probe (#428)

* Collect bounded local WMI namespace access evidence

* Reference PR428 and preserve UTC worker query timestamps

* Observe equivalent runtime self tokens without reverting caller context

* Test native token equivalence against restricted caller changes

* Diagnose native token differences and package WMI probe guidance

* Limit WMI connections to the explicitly scoped security privilege

* Document verified native WMI events and privilege preservation

* Require an already-running WMI service before namespace reads
This commit is contained in:
田中ザック Isaac Mathis authored and GitHub committed 2026-09-21 09:08:20 +09:00
1 parent f1ed90d189
commit b84b97b358
17 files changed
+543 -3

No files matched your search

+7
View File
@@ -38,3 +38,10 @@ modules/WecSubscriptionXml.cs text eol=lf
/modules/WecSubscriptionXml.cs text eol=lf
scripts/AppLockerProbe.ps1 text eol=lf
tests/AppLockerProbe*.ps1 text eol=lf
# Fixed local WMI probe source/worker fingerprints.
/scripts/WmiProbe*.ps1 text eol=lf
/scripts/WmiProbeNative.cs text eol=lf
/scripts/WmiNamespaceAuditing.ps1 text eol=lf
/scripts/WefArrival.ps1 text eol=lf
/tests/WmiProbe*.ps1 text eol=lf
+1 -1
View File
@@ -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 -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 -Destination release-binaries/docs/
- name: Set Artifact Name
if: contains(matrix.info.os, 'windows') == true
+39
View File
@@ -0,0 +1,39 @@
name: Native local WMI access probe
on:
push:
branches: ['**']
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
wmi-probe:
timeout-minutes: 15
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/WmiProbe.Tests.ps1
./tests/WmiProbe.Cli.Tests.ps1
- name: Native owned namespace and4662 in Windows PowerShell5.1
if: matrix.engine == 'powershell'
shell: powershell
run: ./tests/WmiProbe.Windows.Tests.ps1 -AllowDisposableNamespaceWrite
- name: Fixtures and public guards in PowerShell7
if: matrix.engine == 'pwsh'
shell: pwsh
run: |
./tests/WmiProbe.Tests.ps1
./tests/WmiProbe.Cli.Tests.ps1
- name: Native owned namespace and4662 in PowerShell7
if: matrix.engine == 'pwsh'
shell: pwsh
run: ./tests/WmiProbe.Windows.Tests.ps1 -AllowDisposableNamespaceWrite
+1
View File
@@ -4,6 +4,7 @@
**改善:**
- 固定のローカル名前空間読み取りを行う任意実行の `wmi-probe` を追加しました。実トークン・監査ポリシー・完全な SACL を観測し、WMI Security4662 を厳密に照合して、容量制限付きの非公開 XML とコードの指紋を記録します。本番の名前空間やポリシーは変更せず、Sigma の評価には加算しません。WMI 接続は明示的に管理するセキュリティ特権だけを使用し、意図しないスレッド特権の有効化を防ぎます。両 PowerShell エンジンの使い捨て Server 2022/2025 テストで実際のローカル 4662 と監査設定・名前空間の復元を確認しました。リモートアクセス、プロバイダー処理の成否、個々のクエリへの排他的な帰属は未検証です。 (#428) (@Shirofune-Security)
- 正規バックアップと現在の WELA 監査コンポーネントを照合し、新規・無効・未リンクの GPO のみを作成する `gpo-create` の Review / Plan / Create を追加しました。実ファイルとネイティブレポートの厳密な検証、明示的なドメイン/書き込み可能 DC、変更しない保護付きバックアップコピー、永続 GUID 記録、内容・無効状態・権限・リンク・バージョンの直前/最終確認で既存ポリシーを保護します。Windows テストは Microsoft の固定バックアップの読み取りと対象外ポリシー/ワークグループの拒否を確認し、実 AD/SYSVOL への正常インポートとクライアント/イベントの受け入れ検証は別途必要です。適用や Sigma の有効性は主張しません。(#427) (@Shirofune-Security)
- 既に無効なネイティブ購読のクエリと説明だけを変更する `wec-update` を追加。定義・実ホスト・コードの指紋、レビュー済み計画のハッシュ、永続レシート、直前確認と変更後の読み戻しにより、再作成や有効化をせずに変更を検証します。使い捨て Windows テストは実更新・復元と古い計画の拒否を確認します。稼働中ソースのブックマーク・配送・Sigma 準備状態は未検証です。 (#426) (@Shirofune-Security)
- 任意実行の`dns-analytical`を追加し、DNS Serverの分析ログを監査・計画・明示選択で設定できるようにしました。トレース再設定への個別同意、永続的な変更前記録、容量を制限したネイティブETLの退避とハッシュ検証に対応し、ACL・パス・既存の大きいバッファを保持します。退避失敗で停止した状態を失敗として報告します。使い捨てDNS環境のループバックイベント257と設定復元のテストを追加し、転送・Sigmaの利用可能性は未検証のままです。 (#425) (@Shirofune-Security)
+1
View File
@@ -4,6 +4,7 @@
**Improvements:**
- Added opt-in `wmi-probe` for a fixed local namespace read with observed token, audit-policy and full SACL context, exact WMI Security4662 correlation, bounded private raw XML and source fingerprints. Production makes no namespace/policy changes and grants no Sigma credit. WMI connections now use only the explicitly scoped security privilege, avoiding unintended thread privilege expansion. Disposable Server 2022/2025 tests under both PowerShell engines verify real local 4662 events and exact policy/namespace cleanup. Remote access, provider-operation success and exclusive query attribution remain unverified. (#428) (@Shirofune-Security)
- Added opt-in `gpo-create` review, plan and new disabled/unlinked GPO creation from an exact genuine backup matched to current WELA audit components. Strict payload/native-report validation, explicit domain/writable-DC identity, protected unchanged backup copies, durable GUID receipts and fresh/final content, flags, permissions, link and version checks preserve existing policies. Native Windows tests read a pinned Microsoft backup and exercise broad-payload/workgroup refusal; positive AD/SYSVOL import and client/event acceptance remain pending, with no deployment or Sigma credit. (#427) (@Shirofune-Security)
- Added `wec-update` to review and apply query/description changes to one already disabled native subscription through existing-only WEC handles. Complete definition/context/code fingerprints, a separately reviewed plan hash, durable receipts, fresh checks and preserved-property readback reject drift without recreation or activation. Disposable Windows tests cover actual updates/restoration and stale plans; active-source bookmarks, delivery and Sigma readiness remain unverified. (#426) (@Shirofune-Security)
- Added opt-in `dns-analytical` auditing, planning and selective DNS Server channel configuration with explicit trace-reset consent, durable state records and bounded native ETL archives verified before resets. Preserve ACLs, paths and larger buffers; report stopped partial failures honestly. Added disposable standalone-DNS tests for loopback event 257 and exact configuration restoration; forwarding and Sigma readiness remain unverified. (#425) (@Shirofune-Security)
+14
View File
@@ -40,6 +40,10 @@
[ValidateSet('Audit', 'Plan', 'Import')][string]$AppLockerAction = 'Audit',
[string]$AppLockerPolicyPath,
[ValidateSet('List', 'Audit', 'Plan', 'Configure')][string]$WmiAction = 'List',
[ValidateSet('Plan','Run')][string]$WmiProbeAction = 'Plan',
[string]$WmiProbeNamespace,
[string]$WmiProbeOutputPath,
[ValidateRange(1,30)][int]$WmiProbeTimeoutSeconds = 15,
[string[]]$WmiNamespace,
[switch]$WmiIncludeChildren,
[string]$RuleEvidencePath,
@@ -146,6 +150,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
. (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1")
. (Join-Path $ScriptRoot "scripts/AppLockerProbe.ps1")
. (Join-Path $ScriptRoot "scripts/WmiNamespaceAuditing.ps1")
. (Join-Path $ScriptRoot "scripts/WmiProbe.ps1")
. (Join-Path $ScriptRoot "scripts/PowerShellTranscription.ps1")
Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop
Import-Module (Join-Path $ScriptRoot "modules/RuleEligibility.psm1") -ErrorAction Stop
@@ -1935,6 +1940,7 @@ Usage:
./WELA.ps1 score -Help # Separate configuration compliance and evidence-qualified readiness
./WELA.ps1 intune-export -Help # Offline native audit OMA-URI/Graph artifacts; no tenant changes
./WELA.ps1 wec-update -Help # Review query/description updates on a disabled subscription
./WELA.ps1 wmi-probe -Help # Fixed local read and matched namespace Security4662 evidence
./WELA.ps1 applocker-probe -Help # Collect a fixed native AppLocker EXE event
./WELA.ps1 wef-arrival -Help # Verify exact native probe presence on the local collector
./WELA.ps1 native-validation -Help # Collect a fixed native 4688 probe without changing policy
@@ -2010,6 +2016,8 @@ if ($Cmd -ne 'wec-runtime' -and @($PSBoundParameters.Keys | Where-Object {$_ -li
if ($Cmd -eq 'wec-runtime' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WecRuntimeId','WecRuntimeMaximumSources','ResultsPath','Help')}).Count) {
throw 'wec-runtime accepts only selected runtime IDs, source cap and a new result path. No command was run.'
}
if ($Cmd -ne 'wmi-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WmiProbe*'}).Count) {throw 'WmiProbe options require wmi-probe.'}
if ($Cmd -eq 'wmi-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WmiProbeAction','WmiProbeNamespace','WmiProbeOutputPath','WmiProbeTimeoutSeconds','Help')}).Count) {throw 'wmi-probe accepts only dedicated probe options.'}
if ($Cmd -ne 'applocker-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -in @('AppLockerProbeAction','AppLockerProbeOutputPath','AppLockerProbeTimeoutSeconds')}).Count) {throw 'AppLocker probe options require applocker-probe.'}
if ($Cmd -eq 'applocker-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','AppLockerProbeAction','AppLockerProbeOutputPath','AppLockerProbeTimeoutSeconds','Help')}).Count) {throw 'applocker-probe accepts only its dedicated options.'}
if ($Cmd -ne 'wef-arrival' -and @($PSBoundParameters.Keys | Where-Object {$_ -in @('ArrivalProbePath','ArrivalOutputPath')}).Count) {
@@ -2195,6 +2203,12 @@ switch ($Cmd.ToLower()) {
$report
if($report.ExitCode){exit $report.ExitCode}
}
'wmi-probe' {
if ($Help) {Write-Host 'Usage: wmi-probe [-WmiProbeAction Plan|Run] -WmiProbeNamespace root\default [-WmiProbeOutputPath new-private-directory] [-WmiProbeTimeoutSeconds 1..30]. Fixed local read only; requires existing matching SACL and auditing. No policy changes, remote access or Sigma credit. See docs/wmi-probe.md.';return}
$report=Invoke-WelaWmiProbe -Action $WmiProbeAction -Namespace $WmiProbeNamespace -OutputPath $WmiProbeOutputPath -TimeoutSeconds $WmiProbeTimeoutSeconds
$report
if($report.ExitCode){exit $report.ExitCode}
}
'applocker-probe' {
if ($Help) {Write-Host 'Usage: applocker-probe [-AppLockerProbeAction Plan|Run] [-AppLockerProbeOutputPath new-private-directory] [-AppLockerProbeTimeoutSeconds 1..30]. Requires existing EXE audit-only policy, running AppIDSvc and enabled channel. Run launches a fixed native cmd.exe copy and collects one exact AppLocker event. See docs/applocker-probe.md.';return}
$report=Invoke-WelaAppLockerProbe -Action $AppLockerProbeAction -OutputPath $AppLockerProbeOutputPath -TimeoutSeconds $AppLockerProbeTimeoutSeconds
+3 -1
View File
@@ -33,7 +33,7 @@ The numeric subscription mask includes Execute Methods even though the reference
## Privileges, preservation and results
Run elevated with **SeSecurityPrivilege assigned** for Audit/Plan/Configure. WELA enables this privilege in its process while accessing the descriptor, restores the previous token state afterward, and requests privileges for the local WMI connection. Without it a provider can return a DACL while omitting the SACL; WELA refuses that ambiguous read. List only enumerates the supported root child namespaces and reports Present, NotInstalled or Unknown.
Run elevated with **SeSecurityPrivilege assigned** for Audit/Plan/Configure. WELA enables this privilege in its process while accessing the descriptor, restores the previous token state afterward, and uses that explicitly enabled privilege for the local WMI connection. Automatic connection privilege enabling is disabled: native probe diagnostics found it could leave an unrelated SeBackupPrivilege enabled on a thread token. The shared reader/writer no longer requests that expansion. Without it a provider can return a DACL while omitting the SACL; WELA refuses that ambiguous read. List only enumerates the supported root child namespaces and reports Present, NotInstalled or Unknown.
Each GetSecurityDescriptor and SetSecurityDescriptor return code must be explicitly zero. Exceptions, denied/missing namespaces, incomplete descriptors, nonzero return codes, ineffective writes and failed read-back are failures. The journal stores the complete provider descriptor as JSON and MOF strings before the setter is called; nested entries cannot be truncated by the outer result serializer. Native objects are cloned rather than rebuilt from a shortened permission list. The native setter request clears `SE_DACL_PRESENT` and leaves DACL, owner and group null: the [documented provider contract](https://learn.microsoft.com/en-us/windows/win32/wmisdk/setsecuritydescriptor-method-in-class---systemsecurity) preserves those access fields rather than rewriting them. `SE_SACL_PRESENT` requests the SACL update. Full read-back still verifies DACL order, owner, group, other control flags and every original audit entry against the complete recovery snapshot. Unknown entries are never deliberately simplified or discarded.
@@ -75,3 +75,5 @@ To repeat on a **disposable Windows lab VM** (this is a mutating integration tes
```
The script accepts no target namespace. It uses generated `root\WelaSaclTest_<GUID>` names, creates them with CreateOnly, verifies the returned identity, runs the writer only there, and removes only instances it created. The normal read-only test remains separate. It does not enable audit policy, generate controlled Security 4662 evidence or test forwarding. Namespace lifecycle follows Microsoft's [__Namespace contract](https://learn.microsoft.com/en-us/windows/win32/wmisdk/--namespace); SACL-only updates follow the [SetSecurityDescriptor contract](https://learn.microsoft.com/en-us/windows/win32/wmisdk/setsecuritydescriptor-method-in-class---systemsecurity).
The separate [local WMI probe](wmi-probe.md) can collect bounded namespace-read Security4662 evidence using existing prerequisites. It makes no production namespace/policy changes; its owned-namespace native fixture covers a separate local read case, not remote access or forwarding.
+40
View File
@@ -0,0 +1,40 @@
# Local WMI namespace access evidence
`wmi-probe` runs one fixed, read-only query against an explicitly selected existing local namespace and looks for matching native Security **4662** XML. The default `Plan` only observes prerequisites. Neither action changes audit policy, namespaces, SACLs, DACLs, services, firewall rules or remote access. Sysmon is excluded.
```powershell
# First configure the appropriate audit policy and namespace SACL through their authority.
.\WELA.ps1 wmi-probe -WmiProbeNamespace root\default
.\WELA.ps1 wmi-probe -WmiProbeAction Run -WmiProbeNamespace root\default `
-WmiProbeOutputPath C:\Evidence\new-wmi-probe -WmiProbeTimeoutSeconds 15
```
Only an exact `root\name` hierarchy of ordinary identifiers is accepted. There is no server, credential, query or method parameter. `Run` requires a new directory on a local fixed drive with an existing parent; existing paths, remote paths, alternate streams and reparse points are refused. Its protected DACL grants the current user, SYSTEM and Administrators access. Raw XML, before/after observations, operation details and SHA256 hashes remain together with a final manifest. `Plan` accepts no output path. Dedicated options are rejected on other commands; configuration/Auto/DryRun options are rejected here.
## Prerequisites and actual observations
Use native 64-bit Windows PowerShell 5.1 or PowerShell7 on a reviewed Windows11/Server build. The report records actual build/patch/edition, domain and machine role, selected namespace, full provider descriptor JSON/MOF, Security channel settings, effective **Other Object Access Events** mask and typed `SCENoApplyLegacyAuditPolicy`. A direct SCM check requires `Winmgmt` to be Running before WMI observations and again in the worker; [Microsoft documents that a first namespace connection can otherwise start the service](https://learn.microsoft.com/en-us/windows/win32/wmisdk/winmgmt). These checks do not lock out concurrent service administration. Success auditing, DWORD precedence1, a readable enabled Security channel and an observed success read (`WBEM_ENABLE=0x1`) audit ACE matching the actual caller must already exist. A source profile's method-only ACE does not establish this read prerequisite. Use `wmi-auditing` separately to review ASD namespace definitions.
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 `\\.\<selected-namespace>` and executes `SELECT Name FROM __Namespace WHERE Name='WelaReadProbe_<random-guid>'`. 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.
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**.
## What the event establishes
Microsoft documents WMI4662 as an audited **namespace access check**, distinct from the success/failure of the subsequent provider operation. AD directory-service4662 is a different source. The matcher requires the native Security-Auditing provider/GUID, Security channel, reviewed version0, audit-success keyword, `ObjectServer=WMI`, exact namespace, read access mask1, the worker's user SID and logon LUID, actual local computer, and timestamp within the recorded fixed-query interval. The record ID must be greater than the Security boundary observed before launch. Localized rendered messages are not parsed; original XML is retained.
WMI4662 does not include the query's random name or client PID. Concurrent reads of the same namespace by the same logon session can satisfy these fields. The result therefore establishes matching local namespace-access evidence during the interval, **not exclusive attribution to this one query**. Multiple matching records are retained within the explicit bound. It does not infer remote/local logon type from group names, method success, failure-outcome coverage, provider-WMI-Activity equivalence, complete log continuity, forwarding, backend ingestion or detection readiness.
The Security query accepts at most255 candidates; reaching the256-record cap fails instead of claiming completeness. Each XML input is bounded to128KiB characters and at most16 matches are exported. Up to four bounded candidates may be retained for a missing-match diagnostic. A backwards record boundary fails; absence never proves event loss. No logs are cleared, archived, uploaded or forwarded.
## Disposable native validation
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_<GUID>` 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.
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.
Primary references: [WMI namespace access/auditing semantics](https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces), [namespace object paths](https://learn.microsoft.com/en-us/windows/win32/wmisdk/describing-a-wmi-namespace-object-path), [__Namespace class](https://learn.microsoft.com/en-us/windows/win32/wmisdk/--namespace), [namespace access rights](https://learn.microsoft.com/en-us/windows/win32/wmisdk/namespace-access-rights-constants), [SACL-only descriptor contract](https://learn.microsoft.com/en-us/windows/win32/wmisdk/setsecuritydescriptor-method-in-class---systemsecurity), [token logon LUID](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_statistics), and [token group attributes](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_groups).
+4 -1
View File
@@ -129,7 +129,10 @@ function Get-WelaWmiMissingAces {
function New-WelaWmiConnection {
param([string]$Namespace)
$options = New-Object System.Management.ConnectionOptions
$options.EnablePrivileges = $true
# The caller already enables exactly SeSecurityPrivilege and restores it.
# Automatic WMI privilege enabling can leave unrelated privileges enabled
# on a thread impersonation token (observed SeBackupPrivilege on hosted CI).
$options.EnablePrivileges = $false
$options.Impersonation = [System.Management.ImpersonationLevel]::Impersonate
$scope = New-Object System.Management.ManagementScope -ArgumentList "\\.\$Namespace", $options
$scope.Connect()
+156
View File
@@ -0,0 +1,156 @@
# A fixed local namespace read. Never changes namespace security, policy or services.
function Assert-WelaWmiProbeNamespace {
param([string]$Namespace)
if($Namespace -cnotmatch '^root(\\[A-Za-z_][A-Za-z0-9_]{0,63}){1,5}$' -or $Namespace.Length -gt 256){throw 'Select one exact local root\namespace; remote paths, wildcards and queries are unsupported.'}
}
function Initialize-WelaWmiProbeNative {
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'The WMI probe requires native 64-bit Windows.'}
$source=Join-Path $PSScriptRoot 'WmiProbeNative.cs';$hash=(Get-FileHash -LiteralPath $source -Algorithm SHA256 -ErrorAction Stop).Hash
if(-not ('Wela.WmiProbe.Native' -as [type])){Add-Type -Path $source -ErrorAction Stop;$script:WelaWmiProbeNativeHash=$hash}
if($script:WelaWmiProbeNativeHash -cne $hash){throw 'Loaded WMI probe helper differs from its source; start a fresh session.'}
}
function Get-WelaWmiProbeSources {
$sources=[ordered]@{}
foreach($name in @('scripts/WmiProbe.ps1','scripts/WmiProbeWorker.ps1','scripts/WmiProbeNative.cs','scripts/WmiNamespaceAuditing.ps1','scripts/WefArrival.ps1','scripts/Configuration.ps1','scripts/ControlApplicability.ps1','modules/AuditProfiles.psm1')){$sources[$name]=(Get-FileHash -LiteralPath (Join-Path $PSScriptRoot ('../'+$name)) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
$sources|ConvertTo-Json -Compress
}
function Get-WelaWmiProbeTokenKey {
param($Token,[switch]$AuthorizationOnly)
if($Token.Sid -cnotmatch '^S-1-\d+(-\d+)+$' -or $Token.AuthenticationId -cnotmatch '^0x[0-9a-f]+$' -or -not $Token.Name -or -not $Token.Groups){throw 'Incomplete native token observation.'}
$value=[ordered]@{Sid=$Token.Sid;Name=$Token.Name;AuthenticationId=$Token.AuthenticationId;AuthenticationType=$Token.AuthenticationType;Groups=@($Token.Groups)}
if(-not $AuthorizationOnly){$value.Privileges=@($Token.Privileges)}
$value|ConvertTo-Json -Depth 8 -Compress
}
function Get-WelaWmiProbeState {
param([string]$Namespace)
Assert-WelaWmiProbeNamespace $Namespace
Initialize-WelaWmiProbeNative
$token=[Wela.WmiProbe.Native]::Snapshot()
$service=Get-Service -Name Winmgmt -ErrorAction Stop
if($service.Status -ne 'Running'){throw 'Winmgmt must already be running; connecting could otherwise start the service.'}
$hostState=Get-WelaDefaultContext
if(-not(Test-WelaDefaultContextComplete $hostState)){throw 'Complete actual host/build/patch/role context is required.'}
$snapshot=Get-WelaWmiNamespaceSnapshot $Namespace
$channel=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Security')
try{$log=[ordered]@{Name=$channel.LogName;Enabled=$channel.IsEnabled;SecurityDescriptor=$channel.SecurityDescriptor;MaximumSize=$channel.MaximumSizeInBytes;Mode=[string]$channel.LogMode}}finally{$channel.Dispose()}
$engine=(Get-Process -Id $PID -ErrorAction Stop).Path
$state=[pscustomobject][ordered]@{Namespace=$Namespace;Computer=[Environment]::MachineName;Host=$hostState;Service=[string]$service.Status;Token=$token;Descriptor=$snapshot;AuditMask=(Get-WelaAuditPolicyMask '0CCE9227-69AE-11D9-BED3-505054503030');Precedence=(Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy);Channel=$log;Engine=$engine;EngineHash=(Get-FileHash -LiteralPath $engine -Algorithm SHA256).Hash.ToLowerInvariant();Sources=(Get-WelaWmiProbeSources)}
$finalToken=[Wela.WmiProbe.Native]::Snapshot()
if((Get-WelaWmiProbeTokenKey $token) -cne (Get-WelaWmiProbeTokenKey $finalToken)){throw 'Token changed while observing namespace prerequisites.'}
$state.Token=$finalToken
$state
}
function Get-WelaWmiProbeStateKey {
param($State)
Assert-WelaWmiProbeNamespace $State.Namespace
$null=Get-WelaWmiProbeTokenKey $State.Token
if($State.Service -cne 'Running'){throw 'Winmgmt must already be running.'}
if($State.Host.Status -cne 'Observed' -or $State.Host.Build -notin @(22000,22621,22631,20348,26100,26200) -or $State.Host.ProductType -notin @(1,2,3) -or -not $State.Computer){throw 'Host is outside the reviewed Windows 11/Server context.'}
if($State.AuditMask -notin @(1,3) -or -not $State.Precedence.ValueExists -or $State.Precedence.Type -cne 'DWord' -or $State.Precedence.Value -ne 1){throw 'Other Object Access success auditing and typed audit precedence DWORD1 must already be configured.'}
if(-not $State.Channel.Enabled -or $State.Channel.Name -cne 'Security' -or -not $State.Channel.SecurityDescriptor){throw 'The Security channel must already be readable and enabled.'}
if($State.Descriptor.Namespace -cne $State.Namespace -or -not $State.Descriptor.DescriptorMof){throw 'Incomplete selected namespace descriptor.'}
$descriptor=$State.Descriptor.DescriptorJson|ConvertFrom-Json -ErrorAction Stop
$sids=@($State.Token.Sid)+@($State.Token.Groups|Where-Object {($_.Attributes -band 4) -ne 0 -and ($_.Attributes -band 16) -eq 0}|ForEach-Object Sid)
$matches=@($descriptor.SACL|Where-Object {$_.AceType -eq 2 -and ($_.AceFlags -band 64) -ne 0 -and ($_.AceFlags -band 8) -eq 0 -and ($_.AccessMask -band 1) -ne 0 -and (Get-WelaWmiSid $_.Trustee) -in $sids})
if(-not $matches.Count){throw 'No observed success read audit ACE matches this caller on the selected namespace; no SACL is added.'}
$State|ConvertTo-Json -Depth 16 -Compress
}
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 Start-WelaWmiProbeRead {
param($State)
$fresh=Get-WelaWmiProbeState $State.Namespace
if((Get-WelaWmiProbeStateKey $fresh) -cne (Get-WelaWmiProbeStateKey $State)){throw 'WMI prerequisites changed before the fixed read.'}
$watermark=Get-WelaWmiProbeWatermark
$worker=Join-Path $PSScriptRoot 'WmiProbeWorker.ps1'
$info=New-Object Diagnostics.ProcessStartInfo;$info.FileName=$State.Engine
$info.Arguments='-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "'+$worker+'" -Namespace "'+$State.Namespace+'"'
$info.UseShellExecute=$false;$info.CreateNoWindow=$true;$info.RedirectStandardOutput=$true;$info.RedirectStandardError=$true
$info.StandardOutputEncoding=[Text.UTF8Encoding]::new($false,$true);$info.StandardErrorEncoding=[Text.UTF8Encoding]::new($false,$true)
$process=$null
try{
$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.'}
$text=$output.Result;$diagnostic=$errors.Result
if($text.Length -gt 262144 -or $diagnostic.Length -gt 65536){throw 'Worker output exceeded its evidence bound.'}
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.'}
# 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.'}
$operation|Add-Member NoteProperty SecurityRecordIdBefore $watermark
$operation
}finally{if($process){try{if(-not $process.HasExited){$process.Kill();$null=$process.WaitForExit(1000)}}finally{$process.Dispose()}}}
}
function Read-WelaWmiProbeEvents {
param($Operation)
$query="*[System[Provider[@Name='Microsoft-Windows-Security-Auditing'] and EventID=4662 and EventRecordID>$($Operation.SecurityRecordIdBefore) and TimeCreated[@SystemTime>='$($Operation.StartedUtc)' and @SystemTime<='$($Operation.CompletedUtc)']]]"
$records=@();$xml=@()
try{
try{$records=@(Get-WinEvent -LogName Security -FilterXPath $query -MaxEvents 256 -ErrorAction Stop)}catch{if($_.FullyQualifiedErrorId -notlike 'NoMatchingEventsFound*'){throw}}
foreach($record in $records){$text=[string]$record.ToXml();if($text.Length -gt 131072){throw 'Native event exceeds the 128 KiB character bound.'};$xml+=$text}
[pscustomobject]@{Xml=$xml;Capped=($records.Count -ge 256);Query=$query;MaximumEvents=256}
}finally{foreach($record in $records){$record.Dispose()}}
}
function Test-WelaWmiProbeEvent {
param([string]$Xml,$Operation,$State)
$reader=$null
try{
if($Xml.Length -gt 131072){return $false}
$settings=New-Object Xml.XmlReaderSettings;$settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit;$settings.XmlResolver=$null;$settings.MaxCharactersInDocument=131072
$reader=[Xml.XmlReader]::Create([IO.StringReader]::new($Xml),$settings);$doc=New-Object Xml.XmlDocument;$doc.XmlResolver=$null;$doc.Load($reader)
$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('e','http://schemas.microsoft.com/win/2004/08/events/event')
if($doc.DocumentElement.LocalName -cne 'Event' -or $doc.DocumentElement.NamespaceURI -cne $ns.LookupNamespace('e') -or $doc.SelectNodes('/e:Event/e:System',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:EventData',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:UserData',$ns).Count){return $false}
$system=@{};foreach($name in @('Provider','EventID','Version','Keywords','EventRecordID','Channel','Computer','TimeCreated')){$nodes=$doc.SelectNodes("/e:Event/e:System/e:$name",$ns);if($nodes.Count -ne 1){return $false};$system[$name]=$nodes[0]}
if($system.Provider.GetAttribute('Name') -cne 'Microsoft-Windows-Security-Auditing' -or $system.Provider.GetAttribute('Guid').Trim('{}') -ine '54849625-5478-4994-a5ba-3e3b0328c30d' -or $system.EventID.InnerText -cne '4662' -or $system.Version.InnerText -cne '0' -or $system.Channel.InnerText -cne 'Security' -or $system.Keywords.InnerText -ine '0x8020000000000000' -or $system.EventRecordID.InnerText -cnotmatch '^[1-9][0-9]*$' -or [long]$system.EventRecordID.InnerText -le $Operation.SecurityRecordIdBefore){return $false}
$computers=@($State.Computer);if($State.Host.DomainJoined){$computers+=$State.Computer+'.'+$State.Host.Domain};if($system.Computer.InnerText -notin $computers){return $false}
$time=ConvertTo-WelaArrivalUtc $system.TimeCreated.GetAttribute('SystemTime')
if($time -lt (ConvertTo-WelaArrivalUtc $Operation.StartedUtc) -or $time -gt (ConvertTo-WelaArrivalUtc $Operation.CompletedUtc)){return $false}
$data=@{};foreach($node in $doc.SelectSingleNode('/e:Event/e:EventData',$ns).ChildNodes){if($node.NodeType -eq 'Whitespace'){continue};$name=$node.GetAttribute('Name');if($node.NodeType -ne 'Element' -or $node.LocalName -cne 'Data' -or $node.NamespaceURI -cne $ns.LookupNamespace('e') -or -not $name -or $data.ContainsKey($name) -or @($node.ChildNodes|Where-Object NodeType -eq Element).Count){return $false};$data[$name]=$node.InnerText}
# DS4662 and WMI4662 are different sources. No event-ID-only credit.
if($data.ObjectServer -cne 'WMI' -or $data.ObjectName -ine $State.Namespace -or $data.SubjectUserSid -cne $Operation.BeforeToken.Sid -or $data.SubjectLogonId -notmatch '^0x[0-9a-fA-F]+$' -or $data.AccessMask -notmatch '^0x[0-9a-fA-F]+$'){return $false}
if([Convert]::ToUInt64($data.SubjectLogonId.Substring(2),16) -ne [Convert]::ToUInt64($Operation.BeforeToken.AuthenticationId.Substring(2),16) -or [Convert]::ToUInt64($data.AccessMask.Substring(2),16) -ne 1){return $false}
return $true
}catch{return $false}finally{if($reader){$reader.Dispose()}}
}
function Invoke-WelaWmiProbe {
param([ValidateSet('Plan','Run')][string]$Action='Plan',[string]$Namespace,[string]$OutputPath,[ValidateRange(1,30)][int]$TimeoutSeconds=15)
Assert-WelaWmiProbeNamespace $Namespace
if(($Action -eq 'Run') -ne (-not [string]::IsNullOrWhiteSpace($OutputPath))){throw 'Run requires a new WmiProbeOutputPath; Plan creates no files.'}
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaLocalWmiReadProbe';Action=$Action;Status='Unverified';ExitCode=1;RecordedUtc=[DateTime]::UtcNow.ToString('o');Before=$null;After=$null;Operation=$null;Query=$null;Candidates=0;Matches=0;Artifacts=@();Diagnostic='';OutputPath=$null;PolicyChanges=0;NamespaceChanges=0;ReadyRuleCredit=0;RequestAttribution='Not exclusive: WMI4662 has no probe nonce or client PID; concurrent same-token namespace reads can match.';Scope='Observed local WMI namespace read access only; provider-operation success, remote access, forwarding and Sigma/backend validation are separate. Sysmon excluded.'}
if($Action -eq 'Run'){$report.OutputPath=New-WelaArrivalOutput $OutputPath $PSScriptRoot}
try{
$before=Get-WelaWmiProbeState $Namespace;$report.Before=$before;$key=Get-WelaWmiProbeStateKey $before
if($Action -eq 'Plan'){$report.After=$before;$report.Status='PrerequisitesObserved';$report.ExitCode=0;return $report}
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'before.json' ($before|ConvertTo-Json -Depth 20)
$operation=Start-WelaWmiProbeRead $before;$report.Operation=$operation
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'operation.json' ($operation|ConvertTo-Json -Depth 12)
$timer=[Diagnostics.Stopwatch]::StartNew();$matches=@()
do{
$batch=Read-WelaWmiProbeEvents $operation;$report.Query=$batch.Query;$report.Candidates=@($batch.Xml).Count
if($batch.Capped -isnot [bool] -or $batch.Capped){throw 'The 256-event query cap was reached or completeness is unknown.'}
$matches=@($batch.Xml|Where-Object {Test-WelaWmiProbeEvent $_ $operation $before})
if($matches.Count){break};Start-Sleep -Milliseconds 250
}while($timer.Elapsed.TotalSeconds -lt $TimeoutSeconds)
$report.Matches=$matches.Count
if($matches.Count -gt 16){throw 'More than 16 matching records exceed the bounded evidence set.'}
$i=0;foreach($xml in $matches){$i++;$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath ('event-'+$i+'.xml') $xml}
if(-not $matches.Count){$i=0;foreach($xml in @($batch.Xml|Select-Object -First 4)){$i++;$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath ('candidate-'+$i+'.xml') $xml};throw 'No exact WMI namespace read event was observed in the fixed operation interval.'}
if((Get-WelaWmiProbeWatermark) -lt $operation.SecurityRecordIdBefore){throw 'Security log record boundary moved backwards; evidence continuity is unknown.'}
$after=Get-WelaWmiProbeState $Namespace;$report.After=$after
if((Get-WelaWmiProbeStateKey $after) -cne $key){throw 'Host, token, namespace descriptor, policy, channel or source changed during collection.'}
$report.Status='LocalNamespaceAccessObserved';$report.ExitCode=0
}catch{$report.Diagnostic=$_.Exception.Message}
finally{
if($report.Before -and -not $report.After){try{$report.After=Get-WelaWmiProbeState $Namespace}catch{$report.Diagnostic+=' Final observation failed: '+$_.Exception.Message}}
if($report.OutputPath -and $report.After){$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'after.json' ($report.After|ConvertTo-Json -Depth 20)}
}
if($report.OutputPath){$null=Write-WelaArrivalArtifact $report.OutputPath 'manifest.json' ($report|ConvertTo-Json -Depth 24)}
$report
}
+74
View File
@@ -0,0 +1,74 @@
// Read-only process-token observations. No privilege or authorization changes.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;
namespace Wela.WmiProbe {
public sealed class Group { public string Sid; public uint Attributes; }
public sealed class Privilege { public string Luid; public uint Attributes; }
public sealed class Token {
public string Sid, Name, AuthenticationId, AuthenticationType, ImpersonationLevel, TokenSource;
public Group[] Groups; public Privilege[] Privileges;
}
public static class Native {
[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;}
[StructLayout(LayoutKind.Sequential)] struct TokenGroups {public uint Count;public SidAndAttributes First;}
[StructLayout(LayoutKind.Sequential)] struct LuidAndAttributes {public Luid Luid;public uint Attributes;}
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentThread();
[DllImport("kernel32.dll",SetLastError=true)] static extern bool CloseHandle(IntPtr h);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenProcessToken(IntPtr p,uint access,out IntPtr t);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenThreadToken(IntPtr p,uint access,bool self,out IntPtr t);
[DllImport("advapi32.dll",SetLastError=true)] static extern bool GetTokenInformation(IntPtr t,int cls,IntPtr data,int length,out int needed);
static string Hex(Luid id) {return "0x"+(((ulong)(uint)id.High<<32)|id.Low).ToString("x");}
static IntPtr Read(IntPtr token,int cls,out int length) {
GetTokenInformation(token,cls,IntPtr.Zero,0,out length);
if(Marshal.GetLastWin32Error()!=122||length<4||length>65536)throw new InvalidOperationException("Unknown or oversized token information.");
IntPtr data=Marshal.AllocHGlobal(length);
if(!GetTokenInformation(token,cls,data,length,out length)){int error=Marshal.GetLastWin32Error();Marshal.FreeHGlobal(data);throw new Win32Exception(error);}
return data;
}
static Token ReadToken(IntPtr token,string source) {
Token result=new Token();result.TokenSource=source;using(WindowsIdentity identity=new WindowsIdentity(token)){result.Sid=identity.User.Value;result.Name=identity.Name;result.AuthenticationType=identity.AuthenticationType;result.ImpersonationLevel=identity.ImpersonationLevel.ToString();}
int length;IntPtr p=Read(token,10,out length);
try {if(length<Marshal.SizeOf(typeof(Statistics)))throw new InvalidOperationException("Truncated token statistics.");result.AuthenticationId=Hex(((Statistics)Marshal.PtrToStructure(p,typeof(Statistics))).AuthenticationId);}finally{Marshal.FreeHGlobal(p);}
p=Read(token,2,out length);
try {int count=Marshal.ReadInt32(p),offset=(int)Marshal.OffsetOf(typeof(TokenGroups),"First"),size=Marshal.SizeOf(typeof(SidAndAttributes));if(count<0||count>4096||offset+(long)count*size>length)throw new InvalidOperationException("Invalid token groups.");List<Group> groups=new List<Group>();for(int i=0;i<count;i++){SidAndAttributes g=(SidAndAttributes)Marshal.PtrToStructure(IntPtr.Add(p,offset+i*size),typeof(SidAndAttributes));groups.Add(new Group{Sid=new SecurityIdentifier(g.Sid).Value,Attributes=g.Attributes});}groups.Sort((a,b)=>String.CompareOrdinal(a.Sid,b.Sid));result.Groups=groups.ToArray();}finally{Marshal.FreeHGlobal(p);}
p=Read(token,3,out length);
try {int count=Marshal.ReadInt32(p),size=Marshal.SizeOf(typeof(LuidAndAttributes));if(count<0||count>4096||4+(long)count*size>length)throw new InvalidOperationException("Invalid token privileges.");List<Privilege> privileges=new List<Privilege>();for(int i=0;i<count;i++){LuidAndAttributes v=(LuidAndAttributes)Marshal.PtrToStructure(IntPtr.Add(p,4+i*size),typeof(LuidAndAttributes));privileges.Add(new Privilege{Luid=Hex(v.Luid),Attributes=v.Attributes});}privileges.Sort((a,b)=>String.CompareOrdinal(a.Luid,b.Luid));result.Privileges=privileges.ToArray();}finally{Marshal.FreeHGlobal(p);}
return result;
}
static bool Equivalent(Token a,Token b) {
if(a.Sid!=b.Sid||a.AuthenticationId!=b.AuthenticationId||a.Groups.Length!=b.Groups.Length||a.Privileges.Length!=b.Privileges.Length)return false;
for(int i=0;i<a.Groups.Length;i++)if(a.Groups[i].Sid!=b.Groups[i].Sid||a.Groups[i].Attributes!=b.Groups[i].Attributes)return false;
for(int i=0;i<a.Privileges.Length;i++)if(a.Privileges[i].Luid!=b.Privileges[i].Luid||a.Privileges[i].Attributes!=b.Privileges[i].Attributes)return false;
return true;
}
static string Difference(Token a,Token b) {
if(a.Sid!=b.Sid)return "user SID differs";
if(a.AuthenticationId!=b.AuthenticationId)return "logon LUID differs";
if(a.Groups.Length!=b.Groups.Length)return "group count differs";
for(int i=0;i<a.Groups.Length;i++)if(a.Groups[i].Sid!=b.Groups[i].Sid||a.Groups[i].Attributes!=b.Groups[i].Attributes)return "group "+a.Groups[i].Sid+" process="+a.Groups[i].Attributes+" effective="+b.Groups[i].Attributes;
if(a.Privileges.Length!=b.Privileges.Length)return "privilege count differs";
for(int i=0;i<a.Privileges.Length;i++)if(a.Privileges[i].Luid!=b.Privileges[i].Luid||a.Privileges[i].Attributes!=b.Privileges[i].Attributes)return "privilege "+a.Privileges[i].Luid+" process="+a.Privileges[i].Attributes+" effective="+b.Privileges[i].Attributes;
return "unknown difference";
}
[DllImport("advapi32.dll")] static extern bool IsTokenRestricted(IntPtr token);
public static Token Snapshot() {
IntPtr thread=IntPtr.Zero,process=IntPtr.Zero;
if(!OpenThreadToken(GetCurrentThread(),8,true,out thread)){int error=Marshal.GetLastWin32Error();if(error!=1008)throw new Win32Exception(error);}
try {
if(!OpenProcessToken(GetCurrentProcess(),8,out process))throw new Win32Exception(Marshal.GetLastWin32Error());
if(IsTokenRestricted(process)||(thread!=IntPtr.Zero&&IsTokenRestricted(thread)))throw new InvalidOperationException("Restricted tokens are unsupported.");
Token primary=ReadToken(process,"Process");
if(thread==IntPtr.Zero)return primary;
Token effective=ReadToken(thread,"EquivalentSelfThread");
if(!Equivalent(primary,effective))throw new InvalidOperationException("Effective thread token differs from the process token ("+Difference(primary,effective)+"); an ordinary child cannot preserve this caller context.");
return effective;
} finally {if(process!=IntPtr.Zero)CloseHandle(process);if(thread!=IntPtr.Zero)CloseHandle(thread);}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
# Fixed local query worker, isolated so an unresponsive provider can be terminated.
param([Parameter(Mandatory)][string]$Namespace)
$ErrorActionPreference='Stop'
[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)
. (Join-Path $PSScriptRoot 'WmiProbe.ps1')
Assert-WelaWmiProbeNamespace $Namespace
Initialize-WelaWmiProbeNative
if((Get-Service -Name Winmgmt -ErrorAction Stop).Status -ne 'Running'){throw 'Winmgmt is not running; no WMI connection was attempted.'}
Add-Type -AssemblyName System.Management -ErrorAction Stop
$before=[Wela.WmiProbe.Native]::Snapshot()
$nonce='WelaReadProbe_'+[guid]::NewGuid().ToString('N')
$query="SELECT Name FROM __Namespace WHERE Name='$nonce'"
$options=New-Object System.Management.ConnectionOptions
$options.EnablePrivileges=$false
$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
try {
$scope.Connect()
$enumeration=New-Object System.Management.EnumerationOptions
$enumeration.Timeout=[TimeSpan]::FromSeconds(10);$enumeration.ReturnImmediately=$true;$enumeration.Rewindable=$false
$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
$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
}finally{if($rows){$rows.Dispose()};if($searcher){$searcher.Dispose()}}
+15
View File
@@ -0,0 +1,15 @@
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent;$engine=(Get-Process -Id $PID).Path;$count=0
$cases=@(
@{Args=@('wmi-probe','-Help');Code=0;Pattern='Fixed local read'},
@{Args=@('configure','-WmiProbeAction','Run','-Auto');Code=1;Pattern='require wmi-probe'},
@{Args=@('wmi-auditing','-WmiProbeNamespace','root\default');Code=1;Pattern='require wmi-probe'},
@{Args=@('wmi-probe','-Help','-WmiAction','Configure');Code=1;Pattern='only dedicated'},
@{Args=@('wmi-probe','-Help','-Auto');Code=1;Pattern='only dedicated'},
@{Args=@('wmi-probe','-Help','-DryRun');Code=1;Pattern='only dedicated'},
@{Args=@('wmi-probe','-Help','-ResultsPath','unused');Code=1;Pattern='only dedicated'},
@{Args=@('wmi-probe','-Help','-Profile','wela-2.2.0');Code=1;Pattern='only dedicated'},
@{Args=@('wmi-probe','-WmiProbeNamespace','\\remote\root\default');Code=1;Pattern='exact local'},
@{Args=@('wmi-probe','-WmiProbeNamespace','root\default','-WmiProbeAction','Run');Code=1;Pattern='new WmiProbeOutputPath'})
foreach($case in $cases){$ErrorActionPreference='Continue';$output=& $engine -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') @($case.Args) 2>&1|Out-String;$code=$LASTEXITCODE;$ErrorActionPreference='Stop';if($code -ne $case.Code -or $output -notmatch $case.Pattern){throw "CLI failed: $($case.Args -join ' ') [$code] $output"};$count++}
Write-Host "PASS: $count WMI probe public CLI checks."
$global:LASTEXITCODE=0
+83
View File
@@ -0,0 +1,83 @@
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
. (Join-Path $repo 'scripts/WefArrival.ps1')
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
. (Join-Path $repo 'scripts/WmiProbe.ps1')
Add-Type -Path (Join-Path $repo 'scripts/WmiProbeNative.cs') -ErrorAction Stop
$script:count=0
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
function Reject($Code,$Pattern){$message='';try{&$Code|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, got $message"}
function Clone($Value){ConvertFrom-WelaArrivalJson ($Value|ConvertTo-Json -Depth 24 -Compress)}
# Exercise the actual native token-equivalence gate with synthetic field values.
$equivalent=[Wela.WmiProbe.Native].GetMethod('Equivalent',[Reflection.BindingFlags]'NonPublic,Static')
function NativeToken {
$t=[Wela.WmiProbe.Token]::new();$t.Sid='S-1-5-21-1-2-3-1001';$t.AuthenticationId='0x123'
$g=[Wela.WmiProbe.Group]::new();$g.Sid='S-1-1-0';$g.Attributes=7;$t.Groups=@($g)
$p=[Wela.WmiProbe.Privilege]::new();$p.Luid='0x8';$p.Attributes=0;$t.Privileges=@($p);$t
}
$a=NativeToken;$b=NativeToken;$a.TokenSource='Process';$b.TokenSource='EquivalentSelfThread'
Assert ($equivalent.Invoke($null,@($a,$b))) 'Equivalent runtime self token is accepted without replacement.'
foreach($change in @('Sid','AuthenticationId','GroupSid','GroupAttributes','PrivilegeLuid','PrivilegeAttributes','GroupCount','PrivilegeCount')){
$b=NativeToken
switch($change){
Sid {$b.Sid='S-1-5-18'}
AuthenticationId {$b.AuthenticationId='0x124'}
GroupSid {$b.Groups[0].Sid='S-1-5-11'}
GroupAttributes {$b.Groups[0].Attributes=16}
PrivilegeLuid {$b.Privileges[0].Luid='0x9'}
PrivilegeAttributes {$b.Privileges[0].Attributes=2}
GroupCount {$b.Groups=@()}
PrivilegeCount {$b.Privileges=@()}
}
Assert (-not $equivalent.Invoke($null,@($a,$b))) ('Different effective token is refused: '+$change)
}
$token=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1001';Name='LAB\Reader';AuthenticationId='0x123';AuthenticationType='NTLM';ImpersonationLevel='None';Groups=@([pscustomobject]@{Sid='S-1-1-0';Attributes=7});Privileges=@([pscustomobject]@{Luid='0x8';Attributes=0})}
$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}
$xml='<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"/><EventID>4662</EventID><Version>0</Version><Keywords>0x8020000000000000</Keywords><EventRecordID>101</EventRecordID><Channel>Security</Channel><Computer>LAB</Computer><TimeCreated SystemTime="2025-01-02T03:04:05.5000000Z"/></System><EventData><Data Name="SubjectUserSid">S-1-5-21-1-2-3-1001</Data><Data Name="SubjectLogonId">0x123</Data><Data Name="ObjectServer">WMI</Data><Data Name="ObjectName">root\default</Data><Data Name="AccessMask">0x1</Data></EventData></Event>'
Assert (Test-WelaWmiProbeEvent $xml $operation $state) 'Exact synthetic WMI namespace event matches.'
$mutations=@(
@('4662','4663'),@('>0</Version>','>1</Version>'),@('>WMI<','>DS<'),@('root\default','root\cimv2'),@('0x1</Data>','0x2</Data>'),@('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])}
Assert (-not(Test-WelaWmiProbeEvent ('<!DOCTYPE Event [<!ENTITY x "WMI">]>'+$xml.Replace('>WMI<','>&x;<')) $operation $state)) 'DTD input is refused.'
Assert (-not(Test-WelaWmiProbeEvent $xml.Replace('</EventData>','<Data Name="ObjectName">root\default</Data></EventData>') $operation $state)) 'Duplicate event data are refused.'
Assert (-not(Test-WelaWmiProbeEvent $xml.Replace('</System>','<EventID>4662</EventID></System>') $operation $state)) 'Duplicate System fields are refused.'
Assert (-not(Test-WelaWmiProbeEvent ('x'*131073) $operation $state)) 'Oversized raw evidence is refused.'
foreach($name in @('root\default','root\WelaProbe_a123','root\cimv2\security')){Assert-WelaWmiProbeNamespace $name;Assert $true 'Exact local namespace accepted.'}
foreach($name in @('root','ROOT\default','\\remote\root\default','root\default:__SystemSecurity=@','root\*','root\..\default','root/default','root\default;Write-Host x')){Reject {Assert-WelaWmiProbeNamespace $name} 'exact local'}
$null=Get-WelaWmiProbeStateKey $state
foreach($mask in @(0,2,4)){$bad=Clone $state;$bad.AuditMask=$mask;Reject {Get-WelaWmiProbeStateKey $bad} 'success auditing'}
$bad=Clone $state;$bad.Precedence.Type='String';Reject {Get-WelaWmiProbeStateKey $bad} 'typed audit'
$bad=Clone $state;$bad.Channel.Enabled=$false;Reject {Get-WelaWmiProbeStateKey $bad} 'Security channel'
$bad=Clone $state;$bad.Token.Groups[0].Attributes=16;Reject {Get-WelaWmiProbeStateKey $bad} 'No observed success'
foreach($field in @('AceType','AceFlags','AccessMask')){$d=Clone $descriptor;$d.SACL[0].$field=0;$bad=Clone $state;$bad.Descriptor.DescriptorJson=$d|ConvertTo-Json -Depth 10 -Compress;Reject {Get-WelaWmiProbeStateKey $bad} 'No observed success'}
$bad=Clone $state;$bad.Host.Build=99999;Reject {Get-WelaWmiProbeStateKey $bad} 'outside'
$bad=Clone $state;$bad.Service='Stopped';Reject {Get-WelaWmiProbeStateKey $bad} 'already be running'
Reject {Invoke-WelaWmiProbe -Namespace 'root\default' -Action Run} 'new WmiProbeOutputPath'
Reject {Invoke-WelaWmiProbe -Namespace 'root\default' -OutputPath ignored} 'Plan creates no files'
# Public production report path; only native boundaries are mocked here.
$script:mode='Success';$script:reads=0;$script:workerCalls=0
function Get-WelaWmiProbeState {param($Namespace);$script:reads++;$copy=Clone $state;if($script:mode -eq 'Drift' -and $script:reads -gt 1){$copy.Sources='changed'};if($script:mode -eq 'Blocked'){$copy.AuditMask=0};$copy}
function Start-WelaWmiProbeRead {param($State);$script:workerCalls++;$operation}
function Read-WelaWmiProbeEvents {param($Operation);if($script:mode -eq 'ReadError'){throw 'native read denied'};[pscustomobject]@{Xml=@($xml);Capped=($script:mode -eq 'Cap');Query='fixture bounded query'}}
function Get-WelaWmiProbeWatermark {if($script:mode -eq 'Clear'){99}else{101}}
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-fixtures-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $temp
try{
$plan=Invoke-WelaWmiProbe -Namespace 'root\default'
Assert ($plan.Status -eq 'PrerequisitesObserved' -and $script:workerCalls -eq 0) 'Default Plan never invokes the fixed worker.'
foreach($mode in @('Success','Blocked','Cap','ReadError','Drift','Clear')){
$script:mode=$mode;$script:reads=0;$script:workerCalls=0;$dir=Join-Path $temp $mode
$result=Invoke-WelaWmiProbe -Action Run -Namespace 'root\default' -OutputPath $dir -TimeoutSeconds 1
$manifest=ConvertFrom-WelaArrivalJson ([IO.File]::ReadAllText((Join-Path $dir 'manifest.json')))
Assert ($manifest.ReadyRuleCredit -eq 0 -and $manifest.PolicyChanges -eq 0 -and $manifest.NamespaceChanges -eq 0) 'Every report retains the no-change/no-readiness boundary.'
Assert ($null -ne $manifest.After) 'After observation survives success/failure.'
foreach($artifact in $manifest.Artifacts){Assert ($artifact.Sha256 -ceq (Get-FileHash -LiteralPath (Join-Path $dir $artifact.Name)).Hash.ToLowerInvariant()) 'Manifest artifact hashes match exact written bytes.'}
if($mode -eq 'Success'){Assert ($result.Status -eq 'LocalNamespaceAccessObserved' -and $result.Matches -eq 1 -and $result.ExitCode -eq 0) 'Public report verifies the bounded event.';Assert ([IO.File]::ReadAllText((Join-Path $dir 'event-1.xml')) -ceq $xml) 'Raw event XML is preserved.'}
else{Assert ($result.Status -eq 'Unverified' -and $result.ExitCode -eq 1 -and $result.Diagnostic) "Failure $mode never becomes observed."}
if($mode -eq 'Blocked'){Assert ($script:workerCalls -eq 0) 'Missing prerequisites block worker invocation.'}
}
Reject {Invoke-WelaWmiProbe -Action Run -Namespace 'root\default' -OutputPath (Join-Path $temp 'Success')} 'new directory'
}finally{Remove-Item -LiteralPath $temp -Recurse -Force}
Write-Host "PASS: $script:count WMI probe fixtures; native boundaries were mocked."
$global:LASTEXITCODE=0
+72
View File
@@ -0,0 +1,72 @@
# Real native APIs and public CLI. Never dot-source mocked fixture functions.
param([switch]$AllowDisposableNamespaceWrite)
$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
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/ControlApplicability.ps1')
. (Join-Path $repo 'scripts/WefArrival.ps1')
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
. (Join-Path $repo 'scripts/WmiProbe.ps1')
$context=Get-WelaDefaultContext
if(-not(Test-WelaDefaultContextComplete $context) -or $context.Build -notin @(20348,26100) -or $context.ProductType -ne 3 -or $context.DomainRole -ne 2 -or $context.DomainJoined){throw 'Only an observed disposable workgroup Server2022/2025 is permitted.'}
$script:count=0
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
function PolicyKey($Map){(@($Map.Keys|Sort-Object|ForEach-Object{"$_=$($Map[$_])"}) -join ';')}
$engine=(Get-Process -Id $PID).Path
$guid='0CCE9227-69AE-11D9-BED3-505054503030'
$path='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa';$name='SCENoApplyLegacyAuditPolicy'
$originalPolicies=Get-WelaEffectiveAuditPolicy;$originalPrecedence=Get-WelaRegistryState $path $name
Initialize-WelaWmiProbeNative
$originalToken=[Wela.WmiProbe.Native]::Snapshot()
$namespaceName='WelaReadTest_'+[guid]::NewGuid().ToString('N');$namespace='root\'+$namespaceName
$private=New-WelaArrivalOutput (Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-native-'+[guid]::NewGuid().ToString('N'))) $PSScriptRoot
try{$null=[Wela.WmiProbe.Native]::Snapshot();Write-Host 'Token remains equivalent immediately after private output creation.'}catch{Write-Host ('Native token diagnostic after private output creation: '+$_.Exception.Message)}
$created=$false;$instance=$null;$factory=$null;$failure=$null;$cleanupErrors=@()
try{
Initialize-WelaWmiInterop
$factory=New-Object System.Management.ManagementClass -ArgumentList '\\.\root:__Namespace'
$instance=$factory.CreateInstance();$instance.Name=$namespaceName
$options=New-Object System.Management.PutOptions;$options.Type=[System.Management.PutType]::CreateOnly
$createdPath=$instance.Put($options);$created=$true
Assert ($createdPath.RelativePath -ieq ('__NAMESPACE.Name="'+$namespaceName+'"')) 'CreateOnly returned the exact owned namespace.'
$before=Get-WelaWmiNamespaceSnapshot $namespace
Assert ((Get-WelaWmiProbeTokenKey ([Wela.WmiProbe.Native]::Snapshot())) -ceq (Get-WelaWmiProbeTokenKey $originalToken)) 'Real namespace descriptor read does not leave broader thread privileges.'
$defs=@(Get-WelaWmiAuditDefinitions -Namespace 'root\default')
$defs[0].Namespace=$namespace
$config=New-WelaConfigurationContext -Auto -BackupPath (Join-Path $private 'sacl-before')
Set-WelaWmiAuditControls -Context $config -Plan @([pscustomobject]@{Namespace=$namespace;Definitions=$defs})
$configured=Complete-WelaConfiguration -Context $config -Scope 'wmi-namespace-sacl-only'
Assert ($configured.ExitCode -eq 0 -and $configured.Results[0].Status -eq 'Applied') 'Real production writer configured only the owned namespace.'
$after=Get-WelaWmiNamespaceSnapshot $namespace
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.'
}catch{$failure=$_}
finally{
try{Set-WelaEffectiveAuditPolicy -Guid $guid -Mask $originalPolicies[$guid] -Mode exact}catch{$cleanupErrors+='Audit restoration: '+$_.Exception.Message}
try{if($originalPrecedence.ValueExists){Set-ItemProperty -LiteralPath $path -Name $name -Type $originalPrecedence.Type -Value $originalPrecedence.Value}else{Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop}}catch{$cleanupErrors+='Precedence restoration: '+$_.Exception.Message}
try{Assert ((PolicyKey (Get-WelaEffectiveAuditPolicy)) -ceq (PolicyKey $originalPolicies)) 'All59 original native audit masks restored.';Assert (((Get-WelaRegistryState $path $name)|ConvertTo-Json -Compress) -ceq ($originalPrecedence|ConvertTo-Json -Compress)) 'Typed original precedence restored.'}catch{$cleanupErrors+='Policy verification: '+$_.Exception.Message}
try{if($created){$instance.Delete();$left=@(Get-CimInstance -Namespace root -ClassName __Namespace -Filter ("Name='$namespaceName'") -ErrorAction Stop);Assert ($left.Count -eq 0) 'Only the owned temporary namespace was removed.'}}catch{$cleanupErrors+='Namespace cleanup: '+$_.Exception.Message}
if($instance){$instance.Dispose()};if($factory){$factory.Dispose()}
[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."
$global:LASTEXITCODE=0
+1
View File
@@ -7,6 +7,7 @@
**改善:**
- 固定のローカル名前空間読み取りを行う任意実行の `wmi-probe` を追加しました。実トークン・監査ポリシー・完全な SACL を観測し、WMI Security4662 を厳密に照合して、容量制限付きの非公開 XML とコードの指紋を記録します。本番の名前空間やポリシーは変更せず、Sigma の評価には加算しません。WMI 接続は明示的に管理するセキュリティ特権だけを使用し、意図しないスレッド特権の有効化を防ぎます。両 PowerShell エンジンの使い捨て Server 2022/2025 テストで実際のローカル 4662 と監査設定・名前空間の復元を確認しました。リモートアクセス、プロバイダー処理の成否、個々のクエリへの排他的な帰属は未検証です。 (#428) (@Shirofune-Security)
- 正規バックアップと現在の WELA 監査コンポーネントを照合し、新規・無効・未リンクの GPO のみを作成する `gpo-create` の Review / Plan / Create を追加しました。実ファイルとネイティブレポートの厳密な検証、明示的なドメイン/書き込み可能 DC、変更しない保護付きバックアップコピー、永続 GUID 記録、内容・無効状態・権限・リンク・バージョンの直前/最終確認で既存ポリシーを保護します。Windows テストは Microsoft の固定バックアップの読み取りと対象外ポリシー/ワークグループの拒否を確認し、実 AD/SYSVOL への正常インポートとクライアント/イベントの受け入れ検証は別途必要です。適用や Sigma の有効性は主張しません。(#427) (@Shirofune-Security)
- 既に無効なネイティブ購読のクエリと説明だけを変更する `wec-update` を追加。定義・実ホスト・コードの指紋、レビュー済み計画のハッシュ、永続レシート、直前確認と変更後の読み戻しにより、再作成や有効化をせずに変更を検証します。使い捨て Windows テストは実更新・復元と古い計画の拒否を確認します。稼働中ソースのブックマーク・配送・Sigma 準備状態は未検証です。 (#426) (@Shirofune-Security)
- 任意実行の`dns-analytical`を追加し、DNS Serverの分析ログを監査・計画・明示選択で設定できるようにしました。トレース再設定への個別同意、永続的な変更前記録、容量を制限したネイティブETLの退避とハッシュ検証に対応し、ACL・パス・既存の大きいバッファを保持します。退避失敗で停止した状態を失敗として報告します。使い捨てDNS環境のループバックイベント257と設定復元のテストを追加し、転送・Sigmaの利用可能性は未検証のままです。 (#425) (@Shirofune-Security)
+1
View File
@@ -7,6 +7,7 @@
**Improvements:**
- Added opt-in `wmi-probe` for a fixed local namespace read with observed token, audit-policy and full SACL context, exact WMI Security4662 correlation, bounded private raw XML and source fingerprints. Production makes no namespace/policy changes and grants no Sigma credit. WMI connections now use only the explicitly scoped security privilege, avoiding unintended thread privilege expansion. Disposable Server 2022/2025 tests under both PowerShell engines verify real local 4662 events and exact policy/namespace cleanup. Remote access, provider-operation success and exclusive query attribution remain unverified. (#428) (@Shirofune-Security)
- Added opt-in `gpo-create` review, plan and new disabled/unlinked GPO creation from an exact genuine backup matched to current WELA audit components. Strict payload/native-report validation, explicit domain/writable-DC identity, protected unchanged backup copies, durable GUID receipts and fresh/final content, flags, permissions, link and version checks preserve existing policies. Native Windows tests read a pinned Microsoft backup and exercise broad-payload/workgroup refusal; positive AD/SYSVOL import and client/event acceptance remain pending, with no deployment or Sigma credit. (#427) (@Shirofune-Security)
- Added `wec-update` to review and apply query/description changes to one already disabled native subscription through existing-only WEC handles. Complete definition/context/code fingerprints, a separately reviewed plan hash, durable receipts, fresh checks and preserved-property readback reject drift without recreation or activation. Disposable Windows tests cover actual updates/restoration and stale plans; active-source bookmarks, delivery and Sigma readiness remain unverified. (#426) (@Shirofune-Security)
- Added opt-in `dns-analytical` auditing, planning and selective DNS Server channel configuration with explicit trace-reset consent, durable state records and bounded native ETL archives verified before resets. Preserve ACLs, paths and larger buffers; report stopped partial failures honestly. Added disposable standalone-DNS tests for loopback event 257 and exact configuration restoration; forwarding and Sigma readiness remain unverified. (#425) (@Shirofune-Security)