Add typed native WEC runtime observations (#424)

* Observe typed native WEC subscription runtime status

* Link typed WEC runtime changelog to PR 424

* Pass a native null source for subscription runtime queries

* Ignore unused count storage for native null WEC variants

* Keep unavailable WEC source inventories unknown and diagnose native XML reads

* Read native WEC subscription XML with bounded explicit Unicode pipes

* Use bounded Unicode subscription reads in runtime observations and cleanup

* Decode native WEC XML BOMs without unsupported Unicode switch

* Describe strict native WEC XML byte decoding

* Reference System.Xml explicitly when compiling under Windows PowerShell

* Regenerate website changelog snapshots with their proper headers
This commit is contained in:
田中ザック Isaac Mathis authored and GitHub committed 2026-09-20 22:48:32 +09:00
1 parent 5ba53fbcfb
commit 913b1dfaee
19 files changed
+650 -5

No files matched your search

+2
View File
@@ -24,5 +24,7 @@
# Fixed public pending-request fixture is pinned by its exact byte hash.
/tests/fixtures/adcs-pending-probe.csr text eol=lf
# Native WEC XML reader source identity remains identical across checkouts.
/modules/WecSubscriptionXml.cs text eol=lf
scripts/AppLockerProbe.ps1 text eol=lf
tests/AppLockerProbe*.ps1 text eol=lf
+45
View File
@@ -0,0 +1,45 @@
name: Typed native WEC runtime observations
on:
push:
branches: ['**']
paths:
- 'WELA.ps1'
- 'scripts/WecRuntime*'
- 'scripts/WefDeployment.ps1'
- 'scripts/RetentionHealth.ps1'
- 'modules/WefSubscriptions.psm1'
- 'modules/WecSubscriptionXml.cs'
- 'tests/WecSubscriptionXml.Tests.ps1'
- 'tests/WecRuntime*'
- '.github/workflows/wec-runtime.yml'
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
wec-runtime:
strategy:
fail-fast: false
matrix:
os: [windows-2022, windows-2025]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Bounded Unicode XML fixtures in Windows PowerShell5.1
shell: powershell
run: ./tests/WecSubscriptionXml.Tests.ps1
- name: ABI and observation fixtures in Windows PowerShell5.1
shell: powershell
run: ./tests/WecRuntime.Tests.ps1
- name: Native disabled subscription with exact cleanup in Windows PowerShell5.1
shell: powershell
run: ./tests/WecRuntime.Windows.Tests.ps1 -AllowDisposableSubscription
- name: Bounded Unicode XML fixtures in PowerShell7
shell: pwsh
run: ./tests/WecSubscriptionXml.Tests.ps1
- name: ABI and observation fixtures in PowerShell7
shell: pwsh
run: ./tests/WecRuntime.Tests.ps1
- name: Native disabled subscription with exact cleanup in PowerShell7
shell: pwsh
run: ./tests/WecRuntime.Windows.Tests.ps1 -AllowDisposableSubscription
+2
View File
@@ -4,6 +4,8 @@
**改善:**
- 読み取り専用の`wec-runtime`を追加し、WEC標準APIの稼働状態、数値エラー、UTC時刻と件数を制限した送信元別の観測結果を取得します。実行者・ホスト・定義の変更、不明な値と上限到達を明示し、既存のWEF・保存状態レポートでは元の文字列も保持します。過去の送信元一覧を現在の接続数とは扱わず、Activeからイベント到着やSigma利用可能性を推定しません。無効な使い捨てサブスクリプションで検証し、サービス状態とテスト対象を復元します。WEF・EVTXの合成テスト資料で時刻の末尾ゼロが失われる問題も修正しました。 (#424) (@Shirofune-Security)
- 既存の監査専用ポリシーに対する固定ネイティブ EXE の実行と AppLocker イベントの厳密な照合、保護されたハッシュ付き証拠、変更検出を行うオプトインの `applocker-probe` を追加しました。ポリシー・サービス・チャネルの変更や Sigma の評価加算は行いません。使い捨て Windows CI は一時的な監査専用テストポリシーで実際の 8003 を収集し、管理状態の観測を保持して GP ポリシーとチャネル設定を復元します。 (#423) (@Shirofune-Security)
- 既存のローカルファイル・レジストリを明示的に選択して監査・計画・設定する`targeted-sacl`を追加しました。出典ごとの監査ACE、実効ポリシーの前提条件、継承の個別同意を確認し、対象ハンドルを使ってSACLだけを更新します。既存のセキュリティ記述子を保持し、変更前と検証済みの記録、最終状態の確認、特権の復元に対応します。使い捨てオブジェクトのネイティブテストを追加し、子孫全体・転送・Sigmaの利用可能性は別途検証が必要です。 (#422) (@Shirofune-Security)
- 既存CA向けにネイティブの`adcs-auditing`監査・計画・出典付き設定を追加しました。CAと証明書の識別、監査の前提条件、型付き復旧記録、変更直前と読戻しの検証を共有し、従来のCA設定も同じ処理を使用します。停止中のCAは起動せず、専用コマンドでのフィルター変更には再起動の明示指定を求めます。設定一致・再起動の観測・イベント証拠を区別し、Sigma利用可能数には加算しません。使い捨てのスタンドアロンCAテストで保留要求の4886/4889 XMLを関連付け、元の監査設定と作成した資源を復元・削除します。エンタープライズCA・DC・収集基盤の検証は別途必要です。 (#421) (@Shirofune-Security)
+2
View File
@@ -4,6 +4,8 @@
**Improvements:**
- Added read-only `wec-runtime` with typed native WEC activity, numeric errors, UTC timestamps and bounded per-source observations. Actual reader/context and definition checks keep partial reads, caps and drift explicit; existing WEF/retention inventories retain raw text alongside typed fields. Historical source lists are not connection counts and Active grants no arrival or Sigma credit. Disposable disabled-subscription tests restore service state and remove only their owned fixture. Also fixed synthetic WEF/EVTX fixture timestamp roundtrips without weakening bundle validation. (#424) (@Shirofune-Security)
- Added opt-in `applocker-probe` planning and fixed native EXE collection against existing audit-only policy, with exact AppLocker event correlation, private hashed evidence and drift checks. No policy/service/channel changes or Sigma credit; disposable Windows CI prepares one temporary audit-only fixture for real 8003 collection, preserving management observations and restoring GP policy/channel settings. (#423) (@Shirofune-Security)
- Added opt-in `targeted-sacl` auditing, reviewed plans and selective configuration for existing local file/registry targets. Source-specific audit ACEs, policy prerequisites, inheritance consent, handle-bound SACL-only writes, preserved security descriptors, pending/confirmed receipts and final checks keep target scope explicit. Privileges are restored; native disposable-object tests cover file/registry events without claiming descendant, forwarding or Sigma readiness. (#422) (@Shirofune-Security)
- Added dedicated native `adcs-auditing` audit, plan and source-profile configuration, with pinned CA/certificate identity, verified audit prerequisites, typed journals and fresh/readback guards. Legacy CA configuration uses the same engine; stopped CAs are preserved and dedicated filter changes require explicit restart consent. Policy matches, observed restarts and request events remain separate, with no Sigma credit. Disposable standalone-CA tests collect correlated pending-request 4886/4889 XML and restore policy/created resources; enterprise/DC/backend acceptance remains separate. (#421) (@Shirofune-Security)
+16
View File
@@ -97,6 +97,8 @@
[string]$RecoveryOutputPath,
[string]$ArrivalProbePath,
[string]$ArrivalOutputPath,
[string[]]$WecRuntimeId,
[ValidateRange(1,512)][int]$WecRuntimeMaximumSources=128,
[ValidateSet('Plan','Run')][string]$AppLockerProbeAction = 'Plan',
[string]$AppLockerProbeOutputPath,
[ValidateRange(1,30)][int]$AppLockerProbeTimeoutSeconds = 15,
@@ -122,6 +124,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
. (Join-Path $ScriptRoot "scripts/ControlApplicability.ps1")
. (Join-Path $ScriptRoot "scripts/NativeValidation.ps1")
. (Join-Path $ScriptRoot "scripts/WefArrival.ps1")
. (Join-Path $ScriptRoot "scripts/WecRuntime.ps1")
. (Join-Path $ScriptRoot "scripts/AuditNotifications.ps1")
. (Join-Path $ScriptRoot "scripts/AdObjectSacl.ps1")
. (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1")
@@ -1841,6 +1844,7 @@ function Get-WelaUserProfiles {
$usage = @"
Usage:
./WELA.ps1 wec-runtime -WecRuntimeId subscription-id -ResultsPath new-runtime.json
./WELA.ps1 targeted-sacl -Help # Selected existing local SACL targets; read-only by default
./WELA.ps1 gpo-package -GpoAction Plan -GpoProfile wela-2.2.0 -Role Client -Build 26100
./WELA.ps1 gpo-package -GpoAction Export -GpoProfile wela-2.2.0 -Role Client -Build 26100 -GpoOutputPath .\audit-components
@@ -1967,6 +1971,12 @@ if ($PSBoundParameters.ContainsKey('ProfileFile')) {
if ($Cmd -eq 'profiles' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','ProfileFile','Help') }).Count) { throw 'profiles -ProfileFile lists the selected file and accepts no assessment/configuration options.' }
}
if ($Cmd -ne 'wec-runtime' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WecRuntime*'}).Count) {
throw 'WecRuntime options require wec-runtime. No command was run.'
}
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 '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) {
@@ -2074,6 +2084,12 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi
}
switch ($Cmd.ToLower()) {
'wec-runtime' {
if ($Help) {Write-Host 'Usage: ./WELA.ps1 wec-runtime -WecRuntimeId id1,id2 [-WecRuntimeMaximumSources 1..512] [-ResultsPath new.json]. Read-only local typed WEC activity/errors/times; historical source inventory is not a connection count. No arrival or Sigma claim. See docs/wec-runtime.md.';return}
$report=Invoke-WelaWecRuntime -Ids $WecRuntimeId -MaximumSources $WecRuntimeMaximumSources -ResultsPath $ResultsPath
$report
if ($report.ExitCode) {exit $report.ExitCode}
}
'targeted-sacl' {
if ($Help) { Write-Host 'Usage: ./WELA.ps1 targeted-sacl -TargetSaclProfile profile-id [-TargetSaclId id,...] [-TargetSaclAction Audit|Plan] [-IncludeOptional] [-TargetSaclIncludeChildren] [-ResultsPath new-plan.json]. Configure requires -TargetSaclAction Configure -TargetSaclPlanPath reviewed.json -TargetSaclId same-ids [-TargetSaclIncludeChildren] [-IncludeOptional] [-DryRun] [-Auto] [-BackupPath new-directory] [-ResultsPath new-results.json]. Existing local targets only; see docs/selected-sacl-configuration.md.'; return }
$report=Invoke-WelaSelectedSacl -Action $TargetSaclAction -Profile $TargetSaclProfile -Ids $TargetSaclId -PlanPath $TargetSaclPlanPath -IncludeOptional:$IncludeOptional -IncludeChildren:$TargetSaclIncludeChildren -DryRun:$DryRun -Auto:$Auto -BackupPath $BackupPath -ResultsPath $ResultsPath
+2
View File
@@ -75,3 +75,5 @@ Safe fixtures cover the public JSON/HTML path, cap arithmetic, exact XML-byte pr
Before closing issue #382, use an isolated multi-host lab to compare source/collector timestamps and actual event arrival, demonstrate the intended reader tokens can read/recover archived events, test rollover and recovery with known event sequences, measure real ingestion/storage growth under representative load and verify the external archive enforces its retention policy. Record missing-event, denied-read, time-skew, disabled-subscription, restart and recovery cases. This PR provides evidence collection; it does not supply those deployment acceptance results or increase Sigma coverage.
Primary references: [Microsoft WEF operational behavior and delivery formats](https://learn.microsoft.com/en-us/windows/security/operating-system-security/device-management/use-windows-event-forwarding-to-assist-in-intrusion-detection), [native source-initiated subscription validation](https://learn.microsoft.com/en-us/windows/win32/wec/setting-up-a-source-initiated-subscription), [Windows Time query tools](https://learn.microsoft.com/en-us/windows-server/networking/windows-time-service/windows-time-service-tools-and-settings), [Get-WinEvent ordering/query controls](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.diagnostics/get-winevent), [Security log clear 1102](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-1102), [Security log full 1104](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-1104).
Selected subscriptions also expose [typed local WEC runtime observations](wec-runtime.md) as `TypedRuntime`, alongside unchanged raw native evidence. Partial or unknown runtime reads remain explicit; Active and historical source inventory do not establish event delivery, backlog or retention compliance.
+43
View File
@@ -0,0 +1,43 @@
# Typed local WEC runtime observations
`wec-runtime` reads explicitly selected local Windows Event Collector subscriptions through `EcGetSubscriptionRunTimeStatus` in `Wecapi.dll`. It reports numeric activity and errors, UTC error/retry/heartbeat timestamps, and bounded per-source observations without parsing localized `wecutil gr` labels. It never creates, changes, retries or deletes subscriptions, starts services, configures listeners/firewalls, or generates events. Sysmon queries are excluded.
```powershell
# Native 64-bit Windows PowerShell 5.1 or PowerShell 7; use an authorized collector reader.
./WELA.ps1 wec-runtime -WecRuntimeId 'Security-Baseline' -ResultsPath .\runtime.json
./WELA.ps1 wec-runtime -WecRuntimeId 'Security-Baseline','Security-Suspect' -WecRuntimeMaximumSources 64
```
Supply 1–32 unique exact subscription IDs. The source limit is 1–512 per subscription (default 128); reaching a known larger inventory records the actual reported count, queries only the selected limit and makes the observation Partial. The native adapter also bounds buffers to one MiB, string lengths, source inventory to 4096 entries and buffer-resize retries. It imports only the runtime read API. IDs and report options are rejected on unrelated commands before profile dispatch. This read-only command does not accept Auto, DryRun or configuration options.
The selected definition is read before and after collection using native XML through bounded raw output pipes with strict XML UTF-8/UTF-16 decoding, independent of console encoding. The reader bounds output, diagnostics and elapsed time and rejects invalid Unicode or native exit errors. Its exact subscription identity, explicit enabled/type fields and native QueryList must be readable. DTDs, malformed queries and Sysmon/EMET are rejected through the existing shared parser. Full normalized XML comparison detects definition changes during the observation. A changed source list, definition, actual host context or reader identity prevents a complete result. This is a sequence of observations, not an atomic snapshot or a guarantee against an intervening delete/recreate with an identical definition.
Each report records actual Windows computer/build/role context and the reader's SID, name, authentication type, impersonation level and group SIDs before and after collection, plus collection start/end UTC. Error text remains localized evidence; numeric native values determine the structured fields. The local APIs' authorization decides whether a read succeeds; administrator membership alone is not reported as proof of effective access.
## Read the fields correctly
| Field | Meaning |
| --- | --- |
| `Status=Observed` | All required reads and consistency checks completed. It is not a healthy-delivery verdict. |
| `Status=Partial` | Some runtime data exists, but a property failed, a cap was reached or evidence changed. Retained fields remain individual observations. |
| `Status=Unknown` | A verified local context/definition or runtime observation could not be established. |
| `Subscription.Activity` | Native enum 1 Disabled, 2 Active, 3 Inactive or 4 Trying. Unknown future values retain their number and uncertainty. |
| `Fields.LastError.Value` | The subscription/source's reported UInt32 error. `Fields.*.ErrorCode` separately records failure of the API read itself. |
| Optional timestamps/messages | Native null or zero FILETIME is NotAvailable. Invalid timestamp ranges are Unknown. Valid FILETIMEs become UTC strings, with the original integer retained. |
| `SourceInventory` | For source-initiated subscriptions, sources heard from within the past 30 days; the list persists across collector reboot. For collector-initiated subscriptions, configured sources. Neither is a current connection count. A native null inventory stays Unknown with an unknown count; only a typed empty array establishes zero sources. |
Disabled, Inactive, Trying and a nonzero reported LastError can all be successfully **observed**. Exit 0 means observation completeness only; unknown/partial subscriptions cause exit 1. There is no aggregate healthy or connected-source count. An Active subscription or heartbeat does not establish event arrival, successful XPath selection, backlog size, transmission latency, synchronized clocks or Sigma readiness. `ReadyRuleCredit` stays 0. Use the separate [exact WEF arrival verifier](wef-arrival.md) for a source probe's presence on the collector.
Existing `wec-collector` and `retention-health` JSON inventories gain a separate `TypedRuntime` object while retaining their original `Runtime.Raw` localized evidence and unverified delivery fields. Source-only WEF inventory does not query a remote collector. Their existing configuration/retention exit semantics remain unchanged; examine each `TypedRuntime.Status` for observation completeness. The dedicated command uses the exit behavior above.
Output is optional and must be a new file under an existing local fixed-drive directory, without UNC/device/stream or reparse paths. Relative paths follow the PowerShell location. UTF-8 JSON is created exclusively so existing reports cannot be overwritten. Choose an operator-controlled directory; the report may contain source names, account/group identifiers and subscription XML. The command does not alter the output parent's ACL.
## Validation and remaining acceptance
Fixtures exercise the actual EC_VARIANT buffer decoder, unsigned errors, Unicode, invalid pointers/types/counts, nulls, UTC fractional timestamps, caps, failures, drift, source-history semantics, raw-evidence preservation, JSON output and early CLI guards. No live subscription is touched by those fixtures.
The Windows workflow separately requires explicit `-AllowDisposableSubscription` and GitHub-hosted disposable Server 2022/2025 context. It temporarily starts Wecsvc when needed, creates one uniquely named **disabled** subscription with no real source identity, tests actual typed reads, exact non-ASCII description preservation and missing-ID failure, and removes only the owned subscription after checking its unique description. It independently restores Wecsvc state/startup and checks the original subscription inventory. It creates no listener, firewall rule, domain membership or source endpoint. Native CI must pass before claiming the real adapter validated.
Connected source states, nonempty source history and real heartbeats/errors from Windows 11, member servers, domain controllers and ADCS remain deployment-lab acceptance. Event delivery and storage/retention guarantees are separate tests.
Primary references: Microsoft's [runtime API](https://learn.microsoft.com/en-us/windows/win32/api/evcoll/nf-evcoll-ecgetsubscriptionruntimestatus), [runtime property meanings and 30-day source history](https://learn.microsoft.com/en-us/windows/win32/api/evcoll/ne-evcoll-ec_subscription_runtime_status_info_id), [activity enum](https://learn.microsoft.com/en-us/windows/win32/api/evcoll/ne-evcoll-ec_subscription_runtime_status_active_status), [runtime sample and nullable fields](https://learn.microsoft.com/en-us/windows/win32/wec/displaying-the-status-of-an-event-collector-subscription), and [SDK ABI/constants](https://github.com/microsoft/win32metadata/blob/main/generation/WinSDK/RecompiledIdlHeaders/um/EvColl.h).
+1 -1
View File
@@ -54,7 +54,7 @@ An empty `AllowedSourceDomainComputers` input is filled from the explicit `Sourc
Readback equality covers ID, enabled state, selected delivery preset, ReadExistingEvents, content format, locale, description, destination, explicit source authorization and normalized QueryList structure/text. It is not full byte equality. Native-generated Delivery/EventSources and default transport/credential fields may be returned by `gs`; delivery/runtime expansion is not counted as an operator configuration difference when a supported preset is selected. Unknown observed fields or unrecognized authorization representations fail closed rather than receiving a matching claim. Requested and observed definitions/enabled flags are separate: an observed disabled subscription remains `ObservedEnabled: false`, even if the operator input requests enablement. Source-only runs leave the collector's observed state unknown.
JSON retains exact filters, disabled flags, local channel enablement/mode/ACL, local configuration results, native `wecutil gr` output and its errors, and unverified prerequisites. On collectors, local channel metadata is explicitly labeled **collector only**; it does not describe remote source states. Localized runtime text is preserved as evidence without inferring connected-source counts or arrival success. `LocalConfigurationStatus: RequestedSettingsMatch` describes the selected local settings only. A non-dry-run with unmet prerequisites, failed writes or mismatched final settings exits nonzero and is incomplete.
JSON retains exact filters, disabled flags, local channel enablement/mode/ACL, local configuration results, native `wecutil gr` output and its errors, and unverified prerequisites. A separate [`TypedRuntime`](wec-runtime.md) object adds native activity/error/time fields and bounded per-source observations; its Unknown/Partial status stays independent of local configuration success. On collectors, local channel metadata is explicitly labeled **collector only**; it does not describe remote source states. Localized runtime text is preserved as evidence without inferring connected-source counts or arrival success. `LocalConfigurationStatus: RequestedSettingsMatch` describes the selected local settings only. A non-dry-run with unmet prerequisites, failed writes or mismatched final settings exits nonzero and is incomplete.
## Recovery and lab acceptance
+58
View File
@@ -0,0 +1,58 @@
// Bounded native XML bytes, decoded independently of the PowerShell console code page.
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
namespace Wela.WecXml {
public static class Reader {
static async Task<byte[]> ReadBounded(Stream stream,int maximum) {
using(MemoryStream output=new MemoryStream()) {
byte[] buffer=new byte[8192];int count;
while((count=await stream.ReadAsync(buffer,0,buffer.Length).ConfigureAwait(false))!=0) {
if(output.Length+count>maximum)throw new IOException("Native WEC output exceeds its byte limit.");
output.Write(buffer,0,count);
}
return output.ToArray();
}
}
public static string DecodeXml(byte[] bytes) {
if(bytes==null||bytes.Length==0||bytes.Length>10485760)throw new InvalidDataException("Expected bounded native WEC XML bytes.");
int skip=0;Encoding encoding=new UTF8Encoding(false,true);
if(bytes.Length>=3&&bytes[0]==239&&bytes[1]==187&&bytes[2]==191){skip=3;}
else if(bytes.Length>=2&&bytes[0]==255&&bytes[1]==254){skip=2;encoding=new UnicodeEncoding(false,false,true);}
else if(bytes.Length>=2&&bytes[0]==254&&bytes[1]==255){skip=2;encoding=new UnicodeEncoding(true,false,true);}
else if(bytes.Length>=4&&bytes[0]==60&&bytes[1]==0){encoding=new UnicodeEncoding(false,false,true);}
else if(bytes.Length>=4&&bytes[0]==0&&bytes[1]==60){encoding=new UnicodeEncoding(true,false,true);}
string text=encoding.GetString(bytes,skip,bytes.Length-skip);
XmlReaderSettings settings=new XmlReaderSettings();settings.DtdProcessing=DtdProcessing.Prohibit;settings.XmlResolver=null;settings.MaxCharactersInDocument=10485760;
using(XmlReader reader=XmlReader.Create(new StringReader(text),settings)){while(reader.Read()){};}
return text;
}
public static string ReadXml(string id) {
if(id==null||!Regex.IsMatch(id,@"\A[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}\z"))throw new ArgumentException("Select one exact supported subscription ID.");
ProcessStartInfo start=new ProcessStartInfo();start.FileName=Path.Combine(Environment.SystemDirectory,"wecutil.exe");
start.Arguments="gs \""+id+"\" /f:xml";start.UseShellExecute=false;start.CreateNoWindow=true;start.RedirectStandardOutput=true;start.RedirectStandardError=true;
using(Process process=new Process()) {
process.StartInfo=start;bool started=false;
try {
started=process.Start();if(!started)throw new IOException("Native WEC process did not start.");
Stopwatch timer=Stopwatch.StartNew();Task<byte[]> output=ReadBounded(process.StandardOutput.BaseStream,10485760);Task<byte[]> error=ReadBounded(process.StandardError.BaseStream,65536);
while(!process.WaitForExit(100)) {
if(output.IsFaulted||error.IsFaulted)throw new IOException("Native WEC output could not be read within its bounds.");
if(timer.ElapsedMilliseconds>=30000)throw new TimeoutException("Native WEC definition read exceeded thirty seconds.");
}
int remaining=Math.Max(1,30000-(int)timer.ElapsedMilliseconds);
if(!Task.WaitAll(new Task[]{output,error},remaining))throw new TimeoutException("Native WEC output did not complete within thirty seconds.");
if(process.ExitCode!=0)throw new IOException("Native WEC definition read failed with exit code "+process.ExitCode+"; no definition was accepted.");
if(error.Result.Length!=0)throw new IOException("Native WEC returned unexpected diagnostic bytes; no definition was accepted.");
return DecodeXml(output.Result);
} finally {
if(started){try{if(!process.HasExited){process.Kill();process.WaitForExit(1000);}}catch(InvalidOperationException){}}
}
}
}
}
}
+14 -1
View File
@@ -159,4 +159,17 @@ function Import-WelaWefConfig {
[pscustomobject]@{ Config=$config; Path=$full; Subscriptions=$subscriptions }
}
Export-ModuleMember -Function Read-WelaWefXml, Get-WelaWefXmlKey, ConvertFrom-WelaWefQuery, Get-WelaWefAuthorization, ConvertFrom-WelaWefSubscription, Import-WelaWefConfig
function Read-WelaWecSubscriptionXml {
param([Parameter(Mandatory)][string]$Id)
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'Native WEC XML reads require 64-bit Windows.'}
$path=Join-Path $PSScriptRoot 'WecSubscriptionXml.cs';$hash=(Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash
if(-not ('Wela.WecXml.Reader' -as [type])){
$compile=@{Path=$path;ErrorAction='Stop'}
if($PSVersionTable.PSEdition -eq 'Desktop'){$compile.ReferencedAssemblies=@('System.dll','System.Core.dll','System.Xml.dll')}
Add-Type @compile;$script:WelaWecXmlSourceHash=$hash
}
if($script:WelaWecXmlSourceHash -cne $hash){throw 'Loaded native WEC XML reader differs from its source; start a fresh session.'}
[Wela.WecXml.Reader]::ReadXml($Id)
}
Export-ModuleMember -Function Read-WelaWecSubscriptionXml, Read-WelaWefXml, Get-WelaWefXmlKey, ConvertFrom-WelaWefQuery, Get-WelaWefAuthorization, ConvertFrom-WelaWefSubscription, Import-WelaWefConfig
+3 -1
View File
@@ -148,6 +148,8 @@ function Get-WelaRetentionSubscriptions {
foreach ($id in $Ids) {
$definition=Get-WelaRetentionNativeEvidence 'wecutil.exe' @('gs',$id,'/f:xml')
$runtime=Get-WelaRetentionNativeEvidence 'wecutil.exe' @('gr',$id)
try {$typedRuntime=Get-WelaWecRuntime -Id $id}
catch {$typedRuntime=[pscustomobject]@{Status='Unknown';Diagnostic=$_.Exception.Message;ReadyRuleCredit=0}}
$enabled=$null; $query=$null; $scope='Unknown'; $diagnostic=''
if ($definition.Status -eq 'CommandSucceeded') {
try {
@@ -158,7 +160,7 @@ function Get-WelaRetentionSubscriptions {
$query=ConvertFrom-WelaWefQuery $queryNodes[0].InnerText; $enabled=$enabledNodes[0].InnerText -eq 'true'; $scope='NativeQueryObserved'
} catch { $diagnostic=$_.ToString() }
}
[pscustomobject]@{ Id=$id; Enabled=$enabled; QueryScope=$scope; Query=$query; Definition=$definition; Runtime=$runtime; Diagnostic=$diagnostic; DeliveryHealth='Unknown'; Backlog='Unknown'; ActualArrival='Not tested' }
[pscustomobject]@{ Id=$id; Enabled=$enabled; QueryScope=$scope; Query=$query; Definition=$definition; Runtime=$runtime; TypedRuntime=$typedRuntime; Diagnostic=$diagnostic; DeliveryHealth='Unknown'; Backlog='Unknown'; ActualArrival='Not tested' }
}
}
+130
View File
@@ -0,0 +1,130 @@
# Local, explicitly selected WEC runtime reads; no service/subscription changes.
function Initialize-WelaWecRuntimeNative {
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess) {throw 'Typed WEC runtime requires native 64-bit Windows.'}
if (-not ('Wela.WecRuntime.Native' -as [type])) {Add-Type -Path (Join-Path $PSScriptRoot 'WecRuntimeNative.cs') -ErrorAction Stop}
}
function Read-WelaWecRuntimeValue {
param([string]$Id,[AllowNull()][string]$Source,[int]$Property)
Initialize-WelaWecRuntimeNative
# PowerShell converts $null to an empty .NET string; the native API requires
# a genuine null pointer to select subscription-level status.
[Wela.WecRuntime.Native]::Read($Id,$(if ($Source) {$Source} else {[NullString]::Value}),$Property)
}
function ConvertTo-WelaWecRuntimeField {
param($Value,[int]$Property)
$row=[pscustomobject]@{Status=$Value.State;NativeType=$Value.NativeType;ErrorCode=$Value.ErrorCode;Value=$null;RawFileTime=$null;Diagnostic=$Value.Diagnostic}
if ($Value.State -notin @('Observed','NotAvailable','Unknown')) {throw 'Invalid native observation status.'}
if ($Value.State -ne 'Observed') {
if ($Value.State -eq 'NotAvailable' -and $Property -in @(0,1,5)) {$row.Status='Unknown';$row.Diagnostic='Required native activity/error/source-inventory value is unavailable.'}
return $row
}
switch ($Property) {
{$_ -in @(0,1)} {
if ($Value.NativeType -ne 2 -or $Value.Data -isnot [uint32]) {throw 'Runtime status/error requires native UInt32.'}
$row.Value=$Value.Data
if ($Property -eq 0 -and $Value.Data -notin @(1,2,3,4)) {$row.Status='Unknown';$row.Diagnostic='Unrecognized native activity enum; numeric value retained.'}
}
2 {if ($Value.NativeType -ne 4 -or $Value.Data -isnot [string]) {throw 'Runtime message requires native String.'};$row.Value=$Value.Data}
{$_ -in @(3,4,6)} {
if ($Value.NativeType -ne 3 -or $Value.Data -isnot [uint64]) {throw 'Runtime time requires native FILETIME.'}
$row.RawFileTime=$Value.Data.ToString([Globalization.CultureInfo]::InvariantCulture)
if ($Value.Data -eq 0) {$row.Status='NotAvailable';$row.Diagnostic='Zero FILETIME; no observed timestamp.'}
else {try {$row.Value=[DateTime]::FromFileTimeUtc([long]$Value.Data).ToString('o')} catch {$row.Status='Unknown';$row.Diagnostic='Native FILETIME is outside the supported timestamp range.'}}
}
5 {
if ($Value.NativeType -ne 132 -or $Value.Data -isnot [array] -or @($Value.Data).Count -ne $Value.Count) {throw 'Source inventory requires a native String array with matching count.'}
$row.Value=@($Value.Data)
}
}
$row
}
function Get-WelaWecRuntimeFields {
param([string]$Id,[AllowNull()][string]$Source)
$names=@{0='Activity';1='LastError';2='LastErrorMessage';3='LastErrorTimeUtc';4='NextRetryTimeUtc';6='LastHeartbeatTimeUtc'}
$fields=[ordered]@{}
foreach ($property in @(0,1,2,3,4,6)) {
try {$fields[$names[$property]]=ConvertTo-WelaWecRuntimeField (Read-WelaWecRuntimeValue $Id $Source $property) $property}
catch {$fields[$names[$property]]=[pscustomobject]@{Status='Unknown';NativeType=$null;ErrorCode=$null;Value=$null;RawFileTime=$null;Diagnostic=$_.Exception.Message}}
}
$activity='Unknown'
if ($fields.Activity.Status -eq 'Observed') {$activity=@{1='Disabled';2='Active';3='Inactive';4='Trying'}[[int]$fields.Activity.Value]}
[pscustomobject]@{Source=$Source;Activity=$activity;Fields=[pscustomobject]$fields;Complete=(@($fields.Values | Where-Object Status -eq 'Unknown').Count -eq 0)}
}
function Get-WelaWecRuntimeContext {
Initialize-WelaWecRuntimeNative
$hostContext=Get-WelaDefaultContext
if (-not (Test-WelaDefaultContextComplete $hostContext)) {throw 'Actual collector host context is incomplete.'}
$identity=[Security.Principal.WindowsIdentity]::GetCurrent()
try {
[pscustomobject][ordered]@{Computer=[Environment]::MachineName;Host=$hostContext;ReaderSid=$identity.User.Value;ReaderName=$identity.Name;AuthenticationType=$identity.AuthenticationType;ImpersonationLevel=[string]$identity.ImpersonationLevel;GroupSids=@($identity.Groups | ForEach-Object Value | Sort-Object);IsSystem=$identity.IsSystem}
} finally {$identity.Dispose()}
}
function Get-WelaWecRuntimeDefinition {
param([string]$Id)
$xml=Read-WelaWecSubscriptionXml -Id $Id
$doc=Read-WelaWefXml $xml
$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('s','http://schemas.microsoft.com/2006/03/windows/events/subscription')
$fields=@{}
foreach ($name in @('SubscriptionId','SubscriptionType','Enabled','Query')) {
$nodes=@($doc.SelectNodes('/s:Subscription/s:'+$name,$ns))
if ($nodes.Count -ne 1) {throw "Native definition requires exactly one $name."}
$fields[$name]=$nodes[0].InnerText
}
if ($fields.SubscriptionId -cne $Id -or $fields.SubscriptionType -cnotin @('SourceInitiated','CollectorInitiated') -or $fields.Enabled -cnotin @('true','false')) {throw 'Native subscription identity/type/enabled state is invalid.'}
$query=ConvertFrom-WelaWefQuery $fields.Query
[pscustomobject]@{Id=$Id;Type=$fields.SubscriptionType;Enabled=($fields.Enabled -eq 'true');Query=$query;RawXml=$xml;Key=(Get-WelaWefXmlKey $doc.DocumentElement)}
}
function Get-WelaWecRuntime {
param([string]$Id,[ValidateRange(1,512)][int]$MaximumSources=128)
$ErrorActionPreference='Stop'
if ($Id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$') {throw 'Select an exact supported subscription ID.'}
$report=[pscustomobject][ordered]@{Id=$Id;Status='Unknown';StartedUtc=[DateTime]::UtcNow.ToString('o');CompletedUtc=$null;CollectorBefore=$null;CollectorAfter=$null;DefinitionBefore=$null;DefinitionAfter=$null;Subscription=$null;SourceInventory=$null;Sources=@();MaximumSources=$MaximumSources;Capped=$false;SourceListChanged=$false;Diagnostic='';EventArrival='Not tested';TransmissionLatency='Not measured';Backlog='Unknown';ReadyRuleCredit=0}
try {
$report.CollectorBefore=Get-WelaWecRuntimeContext
$report.DefinitionBefore=Get-WelaWecRuntimeDefinition $Id
$report.Subscription=Get-WelaWecRuntimeFields $Id $null
$inventory=ConvertTo-WelaWecRuntimeField (Read-WelaWecRuntimeValue $Id $null 5) 5
$report.SourceInventory=[pscustomobject]@{Observation=$inventory;AfterObservation=$null;Meaning=$(if ($report.DefinitionBefore.Type -eq 'SourceInitiated') {'Sources the collector heard from in the past 30 days; persistent across reboot. This is not a current connection count.'} else {'Configured event sources, not a current connection count.'});ReportedCount=$null;QueriedCount=0}
$sources=@()
if ($inventory.Status -eq 'Observed') {$sources=@($inventory.Value);$report.SourceInventory.ReportedCount=$sources.Count}
$seen=@{}
foreach ($source in $sources) {if ($source -isnot [string] -or [string]::IsNullOrWhiteSpace($source) -or $source.Length -gt 32768 -or $source -match '[\x00-\x1f]' -or $seen.ContainsKey($source)) {throw 'Invalid or duplicate native source identity.'};$seen[$source]=$true}
$report.Capped=$sources.Count -gt $MaximumSources
$report.Sources=@(foreach ($source in ($sources | Select-Object -First $MaximumSources)) {Get-WelaWecRuntimeFields $Id $source})
$report.SourceInventory.QueriedCount=$report.Sources.Count
$afterInventory=ConvertTo-WelaWecRuntimeField (Read-WelaWecRuntimeValue $Id $null 5) 5
$report.SourceInventory.AfterObservation=$afterInventory
$afterSources=if ($afterInventory.Status -eq 'Observed') {@($afterInventory.Value | Sort-Object)} else {@()}
if ($afterInventory.Status -ne $inventory.Status -or $afterInventory.Status -eq 'Unknown' -or (ConvertTo-Json -InputObject @($sources | Sort-Object) -Compress) -cne (ConvertTo-Json -InputObject @($afterSources) -Compress)) {$report.SourceListChanged=$true}
$report.DefinitionAfter=Get-WelaWecRuntimeDefinition $Id
$report.CollectorAfter=Get-WelaWecRuntimeContext
if ($report.DefinitionAfter.Key -cne $report.DefinitionBefore.Key -or (ConvertTo-Json $report.CollectorBefore -Depth 16 -Compress) -cne (ConvertTo-Json $report.CollectorAfter -Depth 16 -Compress)) {throw 'Collector identity/context or subscription definition changed during observation.'}
$report.Status=if ($report.Capped -or $report.SourceListChanged -or $inventory.Status -eq 'Unknown' -or -not $report.Subscription.Complete -or @($report.Sources | Where-Object {-not $_.Complete}).Count) {'Partial'} else {'Observed'}
} catch {$report.Status=if ($null -ne $report.Subscription) {'Partial'} else {'Unknown'};$report.Diagnostic=$_.Exception.Message}
finally {
if ($null -ne $report.CollectorBefore -and $null -eq $report.CollectorAfter) {try {$report.CollectorAfter=Get-WelaWecRuntimeContext} catch {$report.Diagnostic+=' Final collector context unavailable: '+$_.Exception.Message}}
$report.CompletedUtc=[DateTime]::UtcNow.ToString('o')
}
$report
}
function Invoke-WelaWecRuntime {
param([string[]]$Ids,[ValidateRange(1,512)][int]$MaximumSources=128,[string]$ResultsPath)
if (-not $Ids -or $Ids.Count -gt 32) {throw 'Select 1..32 explicit local subscription IDs.'}
$seen=@{};foreach ($id in $Ids) {if ($id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$' -or $seen.ContainsKey($id)) {throw 'Invalid or duplicate subscription ID.'};$seen[$id]=$true}
$output=$null
if ($ResultsPath) {
$provider=$null;$drive=$null;$output=$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ResultsPath,[ref]$provider,[ref]$drive)
if ($provider.Name -ne 'FileSystem' -or $output -match '^[\\/]{2}' -or $output.Substring([IO.Path]::GetPathRoot($output).Length).Contains(':') -or (Test-Path -LiteralPath $output)) {throw 'Results require a new ordinary local file without streams.'}
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT -and ([IO.DriveInfo]::new([IO.Path]::GetPathRoot($output))).DriveType -ne [IO.DriveType]::Fixed) {throw 'Results require a local fixed drive.'}
$parent=Get-Item -LiteralPath ([IO.Path]::GetDirectoryName($output)) -ErrorAction Stop
for ($node=$parent;$null -ne $node;$node=$node.Parent) {if ([int]$node.Attributes -band [int][IO.FileAttributes]::ReparsePoint) {throw 'Results cannot traverse reparse points.'}}
}
$rows=@(foreach ($id in $Ids) {Get-WelaWecRuntime $id $MaximumSources})
$report=[pscustomobject]@{SchemaVersion=1;Kind='WelaWecRuntime';CapturedUtc=[DateTime]::UtcNow.ToString('o');ExitCode=[int](@($rows | Where-Object Status -ne 'Observed').Count -gt 0);Subscriptions=$rows;ReadyRuleCredit=0;Scope='Local typed WEC runtime observations; API success/activity and historical source inventory do not establish successful event arrival, backlog, latency or rule readiness.'}
if ($output) {
$bytes=[Text.UTF8Encoding]::new($false).GetBytes((ConvertTo-Json -InputObject $report -Depth 30))
$stream=[IO.File]::Open($output,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()}
}
$report
}
+69
View File
@@ -0,0 +1,69 @@
// Read-only WEC runtime observations. No subscription create/save/retry API.
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
namespace Wela.WecRuntime {
public sealed class Value {
public string State="Unknown"; public uint? NativeType; public uint Count;
public int ErrorCode; public string Diagnostic=""; public object Data;
}
public static class Native {
[DllImport("wecapi.dll",CharSet=CharSet.Unicode,SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
static extern bool EcGetSubscriptionRunTimeStatus(string subscription,int property,string source,uint flags,uint size,IntPtr buffer,out uint used);
const uint MaximumBytes=1048576;
static void Range(IntPtr buffer,uint size,IntPtr pointer,long length) {
long offset=pointer.ToInt64()-buffer.ToInt64();
if(pointer==IntPtr.Zero||offset<16||length<0||offset>size||length>size-offset)throw new InvalidOperationException("Native variant points outside its returned buffer.");
}
static string Text(IntPtr buffer,uint size,IntPtr pointer) {
Range(buffer,size,pointer,2);long offset=pointer.ToInt64()-buffer.ToInt64();
StringBuilder value=new StringBuilder();
for(int i=0;i<32768&&offset+2L*i+2<=size;i++) {
char c=(char)(ushort)Marshal.ReadInt16(pointer,i*2);if(c==0)return value.ToString();value.Append(c);
}
throw new InvalidOperationException("Native string is unterminated or exceeds the character limit.");
}
// Public only to permit safe allocated-buffer ABI/type regression tests.
public static Value Decode(IntPtr buffer,uint size,int property) {
if(buffer==IntPtr.Zero||size<16||size>MaximumBytes||property<0||property>6)throw new InvalidOperationException("Invalid runtime buffer or property.");
uint count=unchecked((uint)Marshal.ReadInt32(buffer,8));uint type=unchecked((uint)Marshal.ReadInt32(buffer,12));
Value value=new Value {NativeType=type,Count=count};
// Count and union storage have no meaning for EcVarTypeNull; Windows may leave them untouched.
if(type==0) {value.Count=0;value.State="NotAvailable";return value;}
uint expected=property==0||property==1?2U:property==2?4U:property==5?132U:3U;
if(type!=expected)throw new InvalidOperationException("Unexpected EC_VARIANT type for runtime property.");
if(type==2) {value.Data=unchecked((uint)Marshal.ReadInt32(buffer));}
else if(type==3) {value.Data=unchecked((ulong)Marshal.ReadInt64(buffer));}
else if(type==4) {value.Data=Text(buffer,size,Marshal.ReadIntPtr(buffer));}
else {
if(count>4096)throw new InvalidOperationException("Native source inventory exceeds 4096 entries.");
string[] values=new string[count];IntPtr pointers=Marshal.ReadIntPtr(buffer);
if(count!=0)Range(buffer,size,pointers,(long)count*IntPtr.Size);
for(int i=0;i<count;i++)values[i]=Text(buffer,size,Marshal.ReadIntPtr(pointers,i*IntPtr.Size));
value.Data=values;
}
value.State="Observed";return value;
}
public static Value Read(string subscription,string source,int property) {
if(String.IsNullOrWhiteSpace(subscription)||subscription.Length>128||subscription.IndexOf('\0')>=0||property<0||property>6||source!=null&&(source.Length>32768||source.IndexOf('\0')>=0))throw new ArgumentException("Invalid selected subscription/source/property.");
uint size=16;
for(int attempt=0;attempt<3;attempt++) {
IntPtr buffer=Marshal.AllocHGlobal((int)size);
try {
uint used;bool success=EcGetSubscriptionRunTimeStatus(subscription,property,source,0,size,buffer,out used);
int error=Marshal.GetLastWin32Error();
if(success) {
if(used<16||used>size)return new Value {ErrorCode=13,Diagnostic="Native runtime returned an invalid used-buffer length."};
try{return Decode(buffer,used,property);}catch(Exception e){return new Value {ErrorCode=13,Diagnostic=e.Message};}
}
if(error!=122)return new Value {ErrorCode=error,Diagnostic=new Win32Exception(error).Message};
if(used<=size||used>MaximumBytes)return new Value {ErrorCode=122,Diagnostic="Runtime buffer growth is invalid or exceeds one MiB."};
size=used;
}finally {Marshal.FreeHGlobal(buffer);}
}
return new Value {ErrorCode=122,Diagnostic="Runtime buffer changed repeatedly; observation is incomplete."};
}
}
}
+4 -2
View File
@@ -253,14 +253,16 @@ function Get-WelaWefInventory {
foreach ($subscription in $InputModel.Subscriptions) {
$channels=@()
foreach ($name in $subscription.Query.Channels) { $channels += Get-WelaNativeChannel -Name $name }
$runtime=$null; $observed=$null; $observationError=''
$runtime=$null; $typedRuntime=$null; $observed=$null; $observationError=''
if ($Role -eq 'Collector') {
try { $observed=Get-WelaWefControlState Subscription @{ Id=$subscription.Id; SourceSids=$subscription.SourceSids } }
catch { $observationError=$_.ToString() }
try { $native=Invoke-WelaNative -FilePath 'wecutil.exe' -Arguments @('gr',$subscription.Id); $runtime=[pscustomobject]@{ State='CommandSucceeded'; Raw=$native.Diagnostic; Diagnostic='Localized native runtime status is retained without inferring event arrival.' } }
catch { $runtime=[pscustomobject]@{ State='Unknown'; Raw=$null; Diagnostic=$_.ToString() } }
try {$typedRuntime=Get-WelaWecRuntime -Id $subscription.Id}
catch {$typedRuntime=[pscustomobject]@{Status='Unknown';Diagnostic=$_.Exception.Message;ReadyRuleCredit=0}}
}
[pscustomobject]@{ Id=$subscription.Id; RequestedEnabled=$subscription.Definition.Enabled; RequestedDefinition=$subscription.Definition; ObservedEnabled=$(if ($observed.Exists) { $observed.Definition.Enabled } else { $null }); ObservedSubscription=$observed; ObservationError=$observationError; Filters=$subscription.Query.Filters; SourceChannels=$channels; ChannelObservationLocation=$(if ($Role -eq 'Collector') { 'Collector only; remote source states are not observed' } else { 'Local source' }); Runtime=$runtime; EffectiveSourceReadAccess='Not tested'; EventArrival='Not tested'; ForwardedSigmaCoverage='Not assessed' }
[pscustomobject]@{ Id=$subscription.Id; RequestedEnabled=$subscription.Definition.Enabled; RequestedDefinition=$subscription.Definition; ObservedEnabled=$(if ($observed.Exists) { $observed.Definition.Enabled } else { $null }); ObservedSubscription=$observed; ObservationError=$observationError; Filters=$subscription.Query.Filters; SourceChannels=$channels; ChannelObservationLocation=$(if ($Role -eq 'Collector') { 'Collector only; remote source states are not observed' } else { 'Local source' }); Runtime=$runtime; TypedRuntime=$typedRuntime; EffectiveSourceReadAccess='Not tested'; EventArrival='Not tested'; ForwardedSigmaCoverage='Not assessed' }
}
}
+150
View File
@@ -0,0 +1,150 @@
$ErrorActionPreference='Stop'
$repo=Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $repo 'modules/WefSubscriptions.psm1') -Force
. (Join-Path $repo 'scripts/WecRuntime.ps1')
Add-Type -Path (Join-Path $repo 'scripts/WecRuntimeNative.cs')
$script:count=0
function Assert($Value,$Message) {if (-not $Value) {throw $Message};$script:count++}
function Throws($Action,$Pattern) {$message='';try {& $Action | Out-Null} catch {$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern; received $message"}
function NativeValue([uint32]$Type,$Data,[uint32]$Count=0) {[pscustomobject]@{State='Observed';NativeType=$Type;Data=$Data;Count=$Count;ErrorCode=0;Diagnostic=''}}
# Decode synthetic native-memory buffers, not a mock of the ABI decoder.
$buffer=[Runtime.InteropServices.Marshal]::AllocHGlobal(256)
try {
$zero=New-Object byte[] 256;[Runtime.InteropServices.Marshal]::Copy($zero,0,$buffer,256)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,2)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,0,-1)
$decoded=[Wela.WecRuntime.Native]::Decode($buffer,16,1)
Assert ($decoded.Data -is [uint32] -and $decoded.Data -eq [uint32]::MaxValue) 'UInt32 error codes keep their unsigned width.'
Throws {[Wela.WecRuntime.Native]::Decode($buffer,16,2)} 'Unexpected EC_VARIANT'
Throws {[Wela.WecRuntime.Native]::Decode($buffer,15,1)} 'Invalid runtime buffer'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,3)
[Runtime.InteropServices.Marshal]::WriteInt64($buffer,0,[DateTime]::UtcNow.ToFileTimeUtc())
Assert (([Wela.WecRuntime.Native]::Decode($buffer,16,6)).Data -is [uint64]) 'FILETIME retains unsigned64 representation.'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,4)
$text=[Text.Encoding]::Unicode.GetBytes("日本語 Fehler`0");$textPointer=[IntPtr]::Add($buffer,64)
[Runtime.InteropServices.Marshal]::Copy($text,0,$textPointer,$text.Length)
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,0,$textPointer)
Assert (([Wela.WecRuntime.Native]::Decode($buffer,256,2)).Data -ceq '日本語 Fehler') 'Native strings are Unicode without English parsing.'
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,0,[IntPtr]::Add($buffer,256))
Throws {[Wela.WecRuntime.Native]::Decode($buffer,256,2)} 'outside'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,132)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,1)
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,0,[IntPtr]::Add($buffer,24))
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,24,$textPointer)
Assert (([Wela.WecRuntime.Native]::Decode($buffer,256,5)).Data[0] -ceq '日本語 Fehler') 'Source string-array pointer layout is decoded.'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,4097)
Throws {[Wela.WecRuntime.Native]::Decode($buffer,256,5)} '4096'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,0)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,0)
Assert (([Wela.WecRuntime.Native]::Decode($buffer,16,2)).State -eq 'NotAvailable') 'Null is distinct from a successful nonempty property.'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,12345)
$nativeNull=[Wela.WecRuntime.Native]::Decode($buffer,16,4)
Assert ($nativeNull.State -eq 'NotAvailable' -and $nativeNull.Count -eq 0) 'Native null ignores unused count/union storage, which Windows need not initialize.'
} finally {[Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)}
foreach ($property in @(0,1,5)) {
if ($property -in @(0,1)) {Throws {ConvertTo-WelaWecRuntimeField (NativeValue 2 '2') $property} 'UInt32'}
$nullValue=[pscustomobject]@{State='NotAvailable';NativeType=0;ErrorCode=0;Diagnostic=''}
Assert ((ConvertTo-WelaWecRuntimeField $nullValue $property).Status -eq 'Unknown') 'Missing mandatory runtime values cannot make a complete observation.'
}
Assert ((ConvertTo-WelaWecRuntimeField (NativeValue 2 ([uint32]99)) 0).Status -eq 'Unknown') 'Unknown future native enum keeps uncertainty.'
Assert ((ConvertTo-WelaWecRuntimeField (NativeValue 3 ([uint64]0)) 6).Status -eq 'NotAvailable') 'Zero heartbeat is not a1601 observed timestamp.'
Assert ((ConvertTo-WelaWecRuntimeField (NativeValue 3 ([uint64]::MaxValue)) 6).Status -eq 'Unknown') 'Out-of-range FILETIME is not silently rounded.'
$utc=[DateTime]::SpecifyKind([DateTime]'2025-01-02T03:04:05',[DateTimeKind]::Utc)
$time=ConvertTo-WelaWecRuntimeField (NativeValue 3 ([uint64]$utc.ToFileTimeUtc())) 6
Assert ($time.Value -ceq '2025-01-02T03:04:05.0000000Z') 'FILETIME renders UTC without labeling local time Z.'
$script:scenario='';$script:contextReads=0;$script:definitionReads=0;$script:inventoryReads=0;$script:nativeReads=0
$definitionReader=(Get-Command Get-WelaWecRuntimeDefinition).ScriptBlock
function Reset-Fixture {$script:scenario='';$script:contextReads=0;$script:definitionReads=0;$script:inventoryReads=0;$script:nativeReads=0}
function Get-WelaWecRuntimeContext {
$script:contextReads++
if ($script:scenario -eq 'context-denied') {throw 'Reader context unavailable'}
[pscustomobject]@{Computer='FixtureCollector';ReaderSid=$(if ($script:scenario -eq 'context-drift' -and $script:contextReads -gt 1) {'OTHER'} else {'S-1-5-18'});Host=[pscustomobject]@{Build=26100;DomainRole=3}}
}
function Get-WelaWecRuntimeDefinition {
param($Id)
$script:definitionReads++
if ($script:scenario -eq 'definition-denied') {throw 'Definition access denied'}
[pscustomobject]@{Id=$Id;Type='SourceInitiated';Enabled=$false;Key=$(if ($script:scenario -eq 'definition-drift' -and $script:definitionReads -gt 1) {'changed'} else {'same'});RawXml='<Subscription/>'}
}
function Read-WelaWecRuntimeValue {
param($Id,$Source,$Property)
$script:nativeReads++
if (($script:scenario -eq 'field-denied' -and $Property -eq 1) -or ($script:scenario -eq 'one-source-denied' -and $Source -eq 'source2' -and $Property -eq 0)) {return [pscustomobject]@{State='Unknown';NativeType=$null;Count=0;ErrorCode=5;Data=$null;Diagnostic='Localized access denied'}}
switch ($Property) {
0 {NativeValue 2 ([uint32]$(if ($script:scenario -eq 'active') {2} elseif ($script:scenario -eq 'trying') {4} else {1}))}
1 {NativeValue 2 ([uint32]$(if ($script:scenario -eq 'trying') {1722} else {0}))}
2 {NativeValue 4 'Lokalisierte Nachricht <script>'}
5 {
$script:inventoryReads++
if ($script:scenario -eq 'null-inventory' -or ($script:scenario -eq 'null-after-inventory' -and $script:inventoryReads -gt 1)) {return [pscustomobject]@{State='NotAvailable';NativeType=0;ErrorCode=0;Count=0;Data=$null;Diagnostic=''}}
$sources=@('source1','source2')
if ($script:scenario -eq 'empty') {$sources=@()}
if ($script:scenario -eq 'duplicate') {$sources=@('source1','SOURCE1')}
if ($script:scenario -eq 'source-drift' -and $script:inventoryReads -gt 1) {$sources=@('source1')}
NativeValue 132 $sources $sources.Count
}
default {NativeValue 3 ([uint64]0)}
}
}
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-wec-runtime-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory -Path $temp
try {
foreach ($case in @('','active','trying','empty')) {
Reset-Fixture;$script:scenario=$case;$report=Get-WelaWecRuntime 'Fixture'
Assert ($report.Status -eq 'Observed') "$case yields a complete observation without claiming health."
Assert ($report.ReadyRuleCredit -eq 0 -and $report.EventArrival -eq 'Not tested' -and $report.Backlog -eq 'Unknown') 'Native status grants no arrival/backlog/Ready proof.'
Assert ($report.SourceInventory.Meaning -match '30 days' -and $report.SourceInventory.Meaning -match 'not a current connection') 'Source inventory retains documented historical meaning.'
if ($case -eq 'trying') {Assert ($report.Subscription.Activity -eq 'Trying' -and $report.Subscription.Fields.LastError.Value -eq 1722) 'Trying and numeric native failure remain visible despite successful reads.'}
if ($case -eq 'empty') {Assert ($report.SourceInventory.ReportedCount -eq 0 -and $report.Sources.Count -eq 0) 'Empty source array is an observed empty inventory, never active sources.'}
}
foreach ($case in @('field-denied','one-source-denied','definition-drift','context-drift','source-drift','duplicate','null-inventory','null-after-inventory')) {
Reset-Fixture;$script:scenario=$case;$report=Get-WelaWecRuntime 'Fixture'
Assert ($report.Status -eq 'Partial') "$case cannot return complete observation."
Assert ($null -ne $report.CollectorAfter) 'Partial observation retains actual final collector context.'
if ($case -eq 'null-inventory') {Assert ($null -eq $report.SourceInventory.ReportedCount -and $report.SourceInventory.Observation.Status -eq 'Unknown') 'Null inventory cannot be promoted to an observed zero-source result.'}
if ($case -eq 'null-after-inventory') {Assert ($report.SourceListChanged -and $report.SourceInventory.AfterObservation.Status -eq 'Unknown') 'Inventory becoming unavailable is explicit observation drift.'}
if ($case -eq 'field-denied') {Assert ($report.Subscription.Fields.LastError.ErrorCode -eq 5 -and $report.Subscription.Fields.LastError.Status -eq 'Unknown') 'Native read error code is separate from subscription LastError value.'}
}
foreach ($case in @('definition-denied','context-denied')) {Reset-Fixture;$script:scenario=$case;$report=Get-WelaWecRuntime 'Fixture';Assert ($report.Status -eq 'Unknown' -and $script:nativeReads -eq 0) 'Unverified context/definition stops runtime queries.'}
Reset-Fixture;$report=Get-WelaWecRuntime 'Fixture' -MaximumSources 1
Assert ($report.Status -eq 'Partial' -and $report.Capped -and $report.Sources.Count -eq 1 -and $report.SourceInventory.ReportedCount -eq 2) 'Source cap keeps actual denominator and explicit incompleteness.'
Assert ($script:nativeReads -eq 14) 'Source cap bounds actual per-source native calls.'
Reset-Fixture
Throws {Invoke-WelaWecRuntime @('Fixture','fixture')} 'duplicate'
Throws {Invoke-WelaWecRuntime @()} '1..32'
Throws {Invoke-WelaWecRuntime @('../other')} 'Invalid'
Push-Location $temp
try {$report=Invoke-WelaWecRuntime @('Fixture') -ResultsPath './runtime.json';Assert (Test-Path ./runtime.json) 'Relative output uses PowerShell location.';Throws {Invoke-WelaWecRuntime @('Fixture') -ResultsPath './runtime.json'} 'new ordinary'} finally {Pop-Location}
$json=Get-Content -LiteralPath (Join-Path $temp 'runtime.json') -Raw | ConvertFrom-Json
Assert ($json.Subscriptions[0].Subscription.Fields.LastErrorMessage.Value -ceq 'Lokalisierte Nachricht <script>') 'JSON preserves localized text and exact nested observations.'
# Exercise actual definition parser with only native XML acquisition replaced.
function Read-WelaWecSubscriptionXml {param($Id) $script:xml}
function Invoke-WelaNative {param($FilePath,$Arguments) [pscustomobject]@{ExitCode=0;Diagnostic=$script:xml}}
$script:xml='<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription"><SubscriptionId>Fixture</SubscriptionId><SubscriptionType>SourceInitiated</SubscriptionType><Enabled>false</Enabled><Query>&lt;QueryList&gt;&lt;Query Id="0" Path="Security"&gt;&lt;Select&gt;*&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Query></Subscription>'
Assert ((& $definitionReader 'Fixture').Enabled -eq $false) 'Native definition parser retains disabled state.'
Throws {& $definitionReader 'Other'} 'identity'
$script:xml=$script:xml.Replace('Path="Security"','Path="Microsoft-Windows-Sysmon/Operational"')
Throws {& $definitionReader 'Fixture'} 'Sysmon'
# Both older inventories preserve raw evidence and add typed observations.
. (Join-Path $repo 'scripts/WefDeployment.ps1')
. (Join-Path $repo 'scripts/RetentionHealth.ps1')
function Get-WelaNativeChannel {param($Name) [pscustomobject]@{Name=$Name}}
function Get-WelaWefControlState {[pscustomobject]@{Exists=$true;Definition=[pscustomobject]@{Enabled=$false}}}
$script:xml='<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription"><Enabled>false</Enabled><Query>&lt;QueryList&gt;&lt;Query Id="0" Path="Security"&gt;&lt;Select&gt;*&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Query></Subscription>'
Reset-Fixture
$input=[pscustomobject]@{Subscriptions=@([pscustomobject]@{Id='Fixture';SourceSids=@();Definition=[pscustomobject]@{Enabled=$false};Query=[pscustomobject]@{Channels=@('Security');Filters=@()}})}
$wef=Get-WelaWefInventory $input Collector
Assert ($wef.Runtime.Raw -ceq $script:xml -and $wef.TypedRuntime.Status -eq 'Observed') 'WEF inventory retains raw runtime evidence alongside typed result.'
$source=Get-WelaWefInventory $input Source
Assert ($null -eq $source.TypedRuntime -and $null -eq $source.Runtime) 'Source inventory does not claim collector runtime observations.'
$retention=Get-WelaRetentionSubscriptions @('Fixture')
Assert ($retention.Runtime.Raw -ceq $script:xml -and $retention.TypedRuntime.Status -eq 'Observed' -and $retention.DeliveryHealth -eq 'Unknown') 'Retention adds typed observations without promoting delivery health.'
$exe=(Get-Process -Id $PID).Path
foreach ($arguments in @(@('configure','-WecRuntimeId','Fixture'),@('wec-runtime','-Auto'),@('wec-runtime','-Profile','wela-2.2.0'))) {
$old=$ErrorActionPreference;$ErrorActionPreference='Continue'
try {$output=& $exe -NoProfile -File (Join-Path $repo 'WELA.ps1') @arguments 2>&1;$code=$LASTEXITCODE} finally {$ErrorActionPreference=$old}
Assert ($code -ne 0 -and ($output -join ' ') -match 'No command was run') 'Public early guard prevents unrelated options from dispatching.'
}
} finally {Remove-Item -LiteralPath $temp -Recurse -Force}
$global:LASTEXITCODE=0
Write-Host "WEC runtime: $script:count assertions passed. Synthetic native buffers and observations; no subscriptions or services changed."
+83
View File
@@ -0,0 +1,83 @@
param([switch]$AllowDisposableSubscription)
$ErrorActionPreference='Stop'
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not $AllowDisposableSubscription -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted') {throw 'Native fixture requires explicit opt-in on a disposable GitHub-hosted Windows runner.'}
$repo=Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $repo 'modules/WefSubscriptions.psm1') -Force
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/ControlApplicability.ps1')
. (Join-Path $repo 'scripts/WecRuntime.ps1')
$script:checks=0
function Assert($Condition,$Message) {if (-not $Condition) {throw $Message};$script:checks++}
function ServiceState {Get-CimInstance Win32_Service -Filter "Name='Wecsvc'" | Select-Object Name,State,StartMode}
function Key($Value) {ConvertTo-Json -InputObject $Value -Depth 16 -Compress}
function Subscriptions {@((Invoke-WelaNative 'wecutil.exe' @('es')).Output | ForEach-Object {$_.ToString().Trim()} | Where-Object {$_})}
$beforeService=ServiceState
if ($beforeService.State -notin @('Running','Stopped') -or $beforeService.StartMode -notin @('Auto','Manual','Disabled')) {throw 'Fixture requires stable existing service state.'}
$serviceKey='HKLM:\SYSTEM\CurrentControlSet\Services\Wecsvc'
$beforeDelayed=Get-WelaRegistryState $serviceKey DelayedAutoStart
$nonce=[guid]::NewGuid().ToString('N');$id='WELA-Runtime-Test-'+$nonce;$description='Owned disposable runtime fixture '+$nonce+' '+([string][char]0x65e5)+([string][char]0x672c)+([string][char]0x8a9e)
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-wec-runtime-'+$nonce)
$null=New-Item -ItemType Directory -Path $temp
$created=$false;$beforeIds=$null
try {
if ($beforeService.StartMode -eq 'Disabled') {Set-Service Wecsvc -StartupType Manual}
if ($beforeService.State -eq 'Stopped') {Start-Service Wecsvc}
$beforeIds=@(Subscriptions)
if ($beforeIds -contains $id) {throw 'Unique fixture ID unexpectedly already exists.'}
$xml=@"
<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription">
<SubscriptionId>$id</SubscriptionId><SubscriptionType>SourceInitiated</SubscriptionType>
<Description>$description</Description><Enabled>false</Enabled>
<Uri>http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog</Uri><ConfigurationMode>Normal</ConfigurationMode>
<Query><![CDATA[<QueryList><Query Id="0" Path="Application"><Select>*[System[(EventID=1)]]</Select></Query></QueryList>]]></Query>
<ReadExistingEvents>false</ReadExistingEvents><TransportName>HTTP</TransportName><ContentFormat>Events</ContentFormat>
<Locale Language="en-US"/><LogFile>ForwardedEvents</LogFile>
<AllowedSourceDomainComputers>O:NSG:NSD:(A;;GA;;;S-1-5-21-111111111-222222222-333333333-1234)</AllowedSourceDomainComputers>
</Subscription>
"@
$xmlPath=Join-Path $temp 'owned-disabled.xml';[IO.File]::WriteAllText($xmlPath,$xml,[Text.UTF8Encoding]::new($false))
# Disabled, uniquely named and no real source: no listener/firewall/source deployment.
$created=$true;$null=Invoke-WelaNative 'wecutil.exe' @('cs',$xmlPath)
$definitionBefore=Get-WelaWecRuntimeDefinition $id;$serviceDuring=ServiceState
Assert ($definitionBefore.RawXml.Contains($description)) 'Explicit native Unicode XML preserves the exact non-ASCII description under both PowerShell engines.'
$result=Invoke-WelaWecRuntime @($id) -ResultsPath (Join-Path $temp 'runtime.json')
if ($result.ExitCode -ne 0) {throw (ConvertTo-Json $result -Depth 30)}
$row=$result.Subscriptions[0]
Assert ($row.Status -eq 'Observed' -and $row.Subscription.Activity -eq 'Disabled' -and -not $row.DefinitionBefore.Enabled) 'Actual disabled subscription is observed through the native runtime API.'
Assert ($row.Subscription.Fields.Activity.NativeType -eq 2 -and $row.Subscription.Fields.Activity.Value -eq 1) 'Native activity uses documented UInt32 enum1.'
Assert ($row.Subscription.Fields.LastError.NativeType -eq 2 -and $row.Subscription.Fields.LastError.Status -eq 'Observed') 'Native LastError is a typed observation, separate from API failure.'
Assert ($row.SourceInventory.ReportedCount -eq 0 -and $row.Sources.Count -eq 0) 'Owned disabled subscription has no observed source history.'
Assert ($row.CollectorBefore.ReaderSid -eq [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -and $row.CollectorBefore.Computer -eq [Environment]::MachineName) 'Actual reader and collector identities are captured.'
Assert ($row.ReadyRuleCredit -eq 0 -and $row.EventArrival -eq 'Not tested') 'Disabled observation makes no forwarding/Ready claim.'
Assert ((Get-WelaWecRuntimeDefinition $id).Key -ceq $definitionBefore.Key -and (Key (ServiceState)) -ceq (Key $serviceDuring)) 'Production runtime command changes no subscription or service state.'
$missing=Read-WelaWecRuntimeValue ($id+'-Missing') $null 0
Assert ($missing.State -eq 'Unknown' -and $missing.ErrorCode -ne 0) 'Actual native missing-subscription error is retained numerically.'
$missingReport=Get-WelaWecRuntime ($id+'-Missing')
Assert ($missingReport.Status -eq 'Unknown' -and $null -eq $missingReport.Subscription) 'Public observation cannot promote an absent subscription.'
Write-Host "PASS: $script:checks actual read-only runtime assertions against an owned disabled subscription. No event source, connection or arrival is claimed."
} catch {Write-Host ('Primary native fixture failure: '+$_.Exception.Message);throw} finally {
$errors=@()
try {
if ($created -and @(Subscriptions) -contains $id) {
$doc=Read-WelaWefXml (Read-WelaWecSubscriptionXml -Id $id)
$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('s','http://schemas.microsoft.com/2006/03/windows/events/subscription')
$descriptions=@($doc.SelectNodes('/s:Subscription/s:Description',$ns))
if ($descriptions.Count -ne 1 -or $descriptions[0].InnerText -cne $description) {throw 'Fixture ownership changed; refusing deletion.'}
$null=Invoke-WelaNative 'wecutil.exe' @('ds',$id)
}
if ($null -ne $beforeIds -and (Key @($beforeIds | Sort-Object)) -cne (Key @(Subscriptions | Sort-Object))) {throw 'Subscription inventory was not restored.'}
} catch {$errors+=$_.Exception.Message}
try {
if ($beforeService.State -eq 'Stopped' -and (Get-Service Wecsvc).Status -ne 'Stopped') {Stop-Service Wecsvc}
if ($beforeService.StartMode -eq 'Disabled') {Set-Service Wecsvc -StartupType Disabled}
if ((Key (Get-WelaRegistryState $serviceKey DelayedAutoStart)) -cne (Key $beforeDelayed)) {
if ($beforeDelayed.ValueExists) {$null=New-ItemProperty -LiteralPath $serviceKey -Name DelayedAutoStart -Value $beforeDelayed.Value -PropertyType $beforeDelayed.Type -Force}
else {Remove-ItemProperty -LiteralPath $serviceKey -Name DelayedAutoStart -ErrorAction Stop}
}
if ((Key (ServiceState)) -cne (Key $beforeService) -or (Key (Get-WelaRegistryState $serviceKey DelayedAutoStart)) -cne (Key $beforeDelayed)) {throw 'Wecsvc state/startup restoration differs.'}
} catch {$errors+=$_.Exception.Message}
if ($errors.Count) {throw "Fixture cleanup failed; preserve $temp : $($errors -join '; ')"}
Remove-Item -LiteralPath $temp -Recurse -Force
Write-Host 'PASS: original subscription inventory and Wecsvc state/startup restored; only the owned fixture was removed.'
}
$global:LASTEXITCODE=0
+22
View File
@@ -0,0 +1,22 @@
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent
Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
if(-not ('Wela.WecXml.Reader' -as [type])){
$compile=@{Path="$repo/modules/WecSubscriptionXml.cs";ErrorAction='Stop'}
if($PSVersionTable.PSEdition -eq 'Desktop'){$compile.ReferencedAssemblies=@('System.dll','System.Core.dll','System.Xml.dll')}
Add-Type @compile
}
$count=0
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
function Reject([scriptblock]$Action){$failed=$false;try{&$Action|Out-Null}catch{$failed=$true};Assert $failed 'Invalid native XML bytes/identity must fail'}
$text='<Subscription><Description>'+([string][char]0x65e5)+([string][char]0x672c)+([string][char]0x8a9e)+'</Description></Subscription>'
$bytes=[Text.Encoding]::Unicode.GetBytes($text)
foreach($value in @($bytes,([byte[]](@(255,254)+$bytes)),([Text.Encoding]::UTF8.GetBytes($text)),([byte[]](@(239,187,191)+[Text.Encoding]::UTF8.GetBytes($text))),([byte[]](@(254,255)+[Text.Encoding]::BigEndianUnicode.GetBytes($text))))){$decoded=[Wela.WecXml.Reader]::DecodeXml($value);Assert ($decoded -ceq $text) 'Strict UTF8/UTF16 with/without BOM preserves non-ASCII';Assert ((Read-WelaWefXml $decoded).DocumentElement.InnerText -ceq (([string][char]0x65e5)+([string][char]0x672c)+([string][char]0x8a9e))) 'Native XML is readable without console transcoding'}
Reject {[Wela.WecXml.Reader]::DecodeXml([byte[]]@())}
Reject {[Wela.WecXml.Reader]::DecodeXml([byte[]]@(60))}
Reject {[Wela.WecXml.Reader]::DecodeXml([byte[]]@(0,216))}
Reject {[Wela.WecXml.Reader]::DecodeXml([byte[]]@(60,120,62,195,40,60,47,120,62))}
Reject {[Wela.WecXml.Reader]::DecodeXml([Text.Encoding]::UTF8.GetBytes('<!DOCTYPE x [<!ENTITY a SYSTEM "file:///etc/passwd">]><x>&a;</x>'))}
Reject {[Wela.WecXml.Reader]::DecodeXml([Text.Encoding]::Unicode.GetBytes('native error'))}
Reject {[Wela.WecXml.Reader]::DecodeXml([byte[]]::new(10485762))}
foreach($id in @('bad"argument','bad\path',"bad`n")){Reject {[Wela.WecXml.Reader]::ReadXml($id)}}
Write-Host "WEC Unicode XML: $count assertions passed."
+2
View File
@@ -7,6 +7,8 @@
**改善:**
- 読み取り専用の`wec-runtime`を追加し、WEC標準APIの稼働状態、数値エラー、UTC時刻と件数を制限した送信元別の観測結果を取得します。実行者・ホスト・定義の変更、不明な値と上限到達を明示し、既存のWEF・保存状態レポートでは元の文字列も保持します。過去の送信元一覧を現在の接続数とは扱わず、Activeからイベント到着やSigma利用可能性を推定しません。無効な使い捨てサブスクリプションで検証し、サービス状態とテスト対象を復元します。WEF・EVTXの合成テスト資料で時刻の末尾ゼロが失われる問題も修正しました。 (#424) (@Shirofune-Security)
- 既存の監査専用ポリシーに対する固定ネイティブ EXE の実行と AppLocker イベントの厳密な照合、保護されたハッシュ付き証拠、変更検出を行うオプトインの `applocker-probe` を追加しました。ポリシー・サービス・チャネルの変更や Sigma の評価加算は行いません。使い捨て Windows CI は一時的な監査専用テストポリシーで実際の 8003 を収集し、管理状態の観測を保持して GP ポリシーとチャネル設定を復元します。 (#423) (@Shirofune-Security)
- 既存のローカルファイル・レジストリを明示的に選択して監査・計画・設定する`targeted-sacl`を追加しました。出典ごとの監査ACE、実効ポリシーの前提条件、継承の個別同意を確認し、対象ハンドルを使ってSACLだけを更新します。既存のセキュリティ記述子を保持し、変更前と検証済みの記録、最終状態の確認、特権の復元に対応します。使い捨てオブジェクトのネイティブテストを追加し、子孫全体・転送・Sigmaの利用可能性は別途検証が必要です。 (#422) (@Shirofune-Security)
- 既存CA向けにネイティブの`adcs-auditing`監査・計画・出典付き設定を追加しました。CAと証明書の識別、監査の前提条件、型付き復旧記録、変更直前と読戻しの検証を共有し、従来のCA設定も同じ処理を使用します。停止中のCAは起動せず、専用コマンドでのフィルター変更には再起動の明示指定を求めます。設定一致・再起動の観測・イベント証拠を区別し、Sigma利用可能数には加算しません。使い捨てのスタンドアロンCAテストで保留要求の4886/4889 XMLを関連付け、元の監査設定と作成した資源を復元・削除します。エンタープライズCA・DC・収集基盤の検証は別途必要です。 (#421) (@Shirofune-Security)
+2
View File
@@ -7,6 +7,8 @@
**Improvements:**
- Added read-only `wec-runtime` with typed native WEC activity, numeric errors, UTC timestamps and bounded per-source observations. Actual reader/context and definition checks keep partial reads, caps and drift explicit; existing WEF/retention inventories retain raw text alongside typed fields. Historical source lists are not connection counts and Active grants no arrival or Sigma credit. Disposable disabled-subscription tests restore service state and remove only their owned fixture. Also fixed synthetic WEF/EVTX fixture timestamp roundtrips without weakening bundle validation. (#424) (@Shirofune-Security)
- Added opt-in `applocker-probe` planning and fixed native EXE collection against existing audit-only policy, with exact AppLocker event correlation, private hashed evidence and drift checks. No policy/service/channel changes or Sigma credit; disposable Windows CI prepares one temporary audit-only fixture for real 8003 collection, preserving management observations and restoring GP policy/channel settings. (#423) (@Shirofune-Security)
- Added opt-in `targeted-sacl` auditing, reviewed plans and selective configuration for existing local file/registry targets. Source-specific audit ACEs, policy prerequisites, inheritance consent, handle-bound SACL-only writes, preserved security descriptors, pending/confirmed receipts and final checks keep target scope explicit. Privileges are restored; native disposable-object tests cover file/registry events without claiming descendant, forwarding or Sigma readiness. (#422) (@Shirofune-Security)
- Added dedicated native `adcs-auditing` audit, plan and source-profile configuration, with pinned CA/certificate identity, verified audit prerequisites, typed journals and fresh/readback guards. Legacy CA configuration uses the same engine; stopped CAs are preserved and dedicated filter changes require explicit restart consent. Policy matches, observed restarts and request events remain separate, with no Sigma credit. Disposable standalone-CA tests collect correlated pending-request 4886/4889 XML and restore policy/created resources; enterprise/DC/backend acceptance remains separate. (#421) (@Shirofune-Security)