# Bounded local callback-delivery measurement. Product code never generates events or changes logging.
function Get-WelaMeasurementCatalog {
$path=Join-Path (Split-Path $PSScriptRoot -Parent) 'config/event_measurement.json'
$data=Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
$expected=@('Security','System','Application','Microsoft-Windows-DNS-Client/Operational','Microsoft-Windows-CAPI2/Operational','Microsoft-Windows-WinRM/Operational','Microsoft-Windows-PowerShell/Operational')
if ($data.schemaVersion -ne 1 -or $data.kind -cne 'WelaLocalDeliveryMeasurement' -or ($data.channels -join '|') -cne ($expected -join '|')) {throw 'Unsupported measurement catalog; arbitrary channels cannot be enabled through this command.'}
[pscustomobject]@{Channels=$expected;Sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant();Source=$data.source;Scope=$data.scope}
}
function Resolve-WelaMeasurementPath {
param([Parameter(Mandatory)][string]$Path)
# Validate lexical aliases before Windows/provider canonicalization can trim them.
foreach ($part in $Path.Split([char[]]@('\','/'))) {
if ($part -notin @('.','..') -and ($part -match '[. ]$' -or $part -match '^(?i:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)')) {throw 'Measurement evidence rejects ambiguous path aliases and reserved names.'}
}
$full=Resolve-WelaEvtxPath $Path
foreach ($part in $full.Substring([IO.Path]::GetPathRoot($full).Length).Split([char[]]@('\','/'))) {
if ($part -match '[. ]$' -or $part -match '^(?i:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)' -or $part -match '[<>"|]') {throw 'Measurement evidence rejects ambiguous path aliases and reserved names.'}
}
# Unlike a missing future output component, denied ancestor metadata is not absence.
$ancestor=$full
while ($ancestor) {
try {
$item=Get-Item -LiteralPath $ancestor -Force -ErrorAction Stop
if ([int]$item.Attributes -band [int][IO.FileAttributes]::ReparsePoint) {throw 'Measurement evidence cannot traverse reparse points.'}
} catch [System.Management.Automation.ItemNotFoundException] { }
$parent=[IO.Directory]::GetParent($ancestor);if (-not $parent) {break};$ancestor=$parent.FullName
}
$full
}
function New-WelaMeasurementOutput {
param([string]$Path)
$full=Resolve-WelaMeasurementPath $Path
# No source input is consumed; use the existing new-directory ACL adapter with an unrelated sentinel.
$root=New-WelaEvtxOutput -Path $full -SourcePath (Join-Path ([IO.Path]::GetPathRoot($full)) ('wela-unused-'+[guid]::NewGuid().ToString('N')))
if ($env:OS -eq 'Windows_NT') {
$acl=Get-Acl -LiteralPath $root -ErrorAction Stop
if (-not $acl.AreAccessRulesProtected) {throw 'Private evidence directory ACL protection was not applied.'}
$allowed=@([Security.Principal.WindowsIdentity]::GetCurrent().User.Value,'S-1-5-18','S-1-5-32-544')
foreach ($rule in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])) {
if ($rule.IsInherited -or $rule.IdentityReference.Value -notin $allowed -or $rule.AccessControlType -ne 'Allow') {throw 'Unexpected private evidence directory access rule.'}
}
}
$root
}
function Write-WelaMeasurementArtifact {
param([string]$Root,[string]$Name,[string]$Text)
$null=Resolve-WelaMeasurementPath $Root
if ($Name -cnotmatch '^(?:[a-z][a-z0-9-]*\.json|event-[0-9]{4}\.xml|bookmark-[0-9]{4}\.xml)$') {throw 'Unexpected measurement artifact name.'}
$bytes=[Text.UTF8Encoding]::new($false).GetBytes($Text)
$stream=[IO.File]::Open((Join-Path $Root $Name),[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()}
$hash=Get-WelaEvtxHash $bytes
if ((Get-FileHash -LiteralPath (Join-Path $Root $Name) -Algorithm SHA256).Hash.ToLowerInvariant() -cne $hash) {throw 'Measurement artifact readback differs.'}
[pscustomobject]@{Name=$Name;Sha256=$hash;Bytes=$bytes.Length}
}
function Get-WelaMeasurementState {
param([string]$Channel)
$reader=Get-WelaEvtxReader
# Local IP-helper metadata performs no DNS query. A shared short-name prefix is not identity.
$network=[Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
$names=@($reader.Computer,$network.HostName)
if (-not [string]::IsNullOrWhiteSpace($network.DomainName)) {$names+=($network.HostName+'.'+$network.DomainName)}
if (@($names|Where-Object {$_ -notmatch '^[\p{L}\p{N}][\p{L}\p{N}_.-]{0,254}$'}).Count) {throw 'Exact native local computer names are unavailable.'}
$reader|Add-Member NoteProperty SourceComputerNames @($names|Sort-Object -Unique)
$configuration=New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration($Channel)
$session=New-Object System.Diagnostics.Eventing.Reader.EventLogSession
try {
$information=$session.GetLogInformation($Channel,[System.Diagnostics.Eventing.Reader.PathType]::LogName)
if ($configuration.LogName -cne $Channel -or [string]$configuration.LogType -notin @('Administrative','Operational') -or -not $configuration.IsEnabled) {throw 'The exact channel must be registered, enabled and Administrative/Operational.'}
if ([string]::IsNullOrWhiteSpace($configuration.SecurityDescriptor) -or [string]::IsNullOrWhiteSpace($configuration.LogFilePath) -or $configuration.MaximumSizeInBytes -le 0 -or $null -eq $information.CreationTime -or $null -eq $information.RecordCount -or $null -eq $information.OldestRecordNumber) {throw 'Required channel metadata or log identity is unavailable.'}
[pscustomobject]@{
CapturedUtc=[datetime]::UtcNow.ToString('o');Reader=$reader
Configuration=[pscustomobject]@{Name=$configuration.LogName;Type=[string]$configuration.LogType;Enabled=[bool]$configuration.IsEnabled;Mode=[string]$configuration.LogMode;MaximumBytes=[long]$configuration.MaximumSizeInBytes;RegisteredPath=$configuration.LogFilePath;SecurityDescriptor=$configuration.SecurityDescriptor;Providers=@($configuration.ProviderNames|Sort-Object)}
Log=[pscustomobject]@{CreatedUtc=$information.CreationTime.ToUniversalTime().ToString('o');OldestRecord=[long]$information.OldestRecordNumber;RecordCount=[long]$information.RecordCount;FileBytes=$information.FileSize;Full=$information.IsLogFull}
}
} finally {$configuration.Dispose();$session.Dispose()}
}
function Assert-WelaMeasurementState {
param($Before,$After)
foreach ($name in @('Reader','Configuration')) {
if ((ConvertTo-Json -InputObject $Before.$name -Depth 20 -Compress) -cne (ConvertTo-Json -InputObject $After.$name -Depth 20 -Compress)) {throw "Measurement $name changed during collection/export."}
}
if ($Before.Log.CreatedUtc -cne $After.Log.CreatedUtc -or $After.Log.OldestRecord -lt $Before.Log.OldestRecord -or ($Before.Log.RecordCount -gt 0 -and $After.Log.RecordCount -eq 0)) {throw 'Log clear/reset or inconsistent identity was observed.'}
}
function Read-WelaMeasurementXmlDocument {
param([string]$Text,[int]$Maximum=1048576)
if ([Text.Encoding]::UTF8.GetByteCount($Text) -gt $Maximum) {throw 'XML exceeds its evidence limit.'}
$settings=New-Object Xml.XmlReaderSettings
$settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit;$settings.XmlResolver=$null;$settings.MaxCharactersInDocument=$Maximum
$reader=[Xml.XmlReader]::Create([IO.StringReader]::new($Text),$settings)
try {$document=New-Object Xml.XmlDocument;$document.XmlResolver=$null;$document.PreserveWhitespace=$true;$document.Load($reader)} finally {$reader.Dispose()}
return ,$document
}
function Get-WelaMeasurementXmlKey {
param($Node,[int]$Depth=0)
if ($Depth -ge 64) {throw 'Event XML semantic comparison exceeds the 64-element nesting cap.'}
# Keep text in its original position relative to element children. UserData can
# contain mixed content; collecting all text separately would erase payload order.
$attributes=@($Node.Attributes | Where-Object {$_.NamespaceURI -ne 'http://www.w3.org/2000/xmlns/'} | Sort-Object NamespaceURI,LocalName -CaseSensitive | ForEach-Object {ConvertTo-Json -InputObject @($_.NamespaceURI,$_.LocalName,$_.Value) -Compress})
$content=New-Object 'System.Collections.Generic.List[string]'
$text=New-Object Text.StringBuilder
$hasElements=@($Node.ChildNodes | Where-Object NodeType -eq Element).Count -gt 0
$mixed=@($Node.ChildNodes | Where-Object {$_.NodeType -in @('Text','CDATA','SignificantWhitespace')}).Count -gt 0
foreach ($child in $Node.ChildNodes) {
if ($child.NodeType -eq 'Element') {
if ($text.Length) {$content.Add((ConvertTo-Json -InputObject @('Text',$text.ToString()) -Compress));$null=$text.Clear()}
$content.Add((ConvertTo-Json -InputObject @('Element',(Get-WelaMeasurementXmlKey -Node $child -Depth ($Depth+1))) -Compress))
} elseif ($child.NodeType -in @('Text','CDATA','SignificantWhitespace')) {$null=$text.Append($child.Value)}
elseif ($child.NodeType -eq 'Whitespace') {
# Ignore indentation only for element-only content; mixed/leaf text is data.
if (-not $hasElements -or $mixed) {$null=$text.Append($child.Value)}
} else {throw 'Unsupported event XML node.'}
}
if ($text.Length) {$content.Add((ConvertTo-Json -InputObject @('Text',$text.ToString()) -Compress))}
# Child digests keep memory proportional to the bounded XML, rather than
# repeatedly JSON-escaping each descendant's serialized representation.
$key=ConvertTo-Json -InputObject @($Node.NamespaceURI,$Node.LocalName,$attributes,@($content.ToArray())) -Depth 30 -Compress
Get-WelaEvtxHash ([Text.Encoding]::UTF8.GetBytes($key))
}
function Read-WelaMeasurementEvent {
param([string]$Xml,[string]$Channel,[string[]]$Computer)
$doc=Read-WelaMeasurementXmlDocument $Xml
$ns='http://schemas.microsoft.com/win/2004/08/events/event';$root=$doc.DocumentElement
if ($root.LocalName -cne 'Event' -or $root.NamespaceURI -cne $ns -or @($root.Attributes|Where-Object NamespaceURI -ne 'http://www.w3.org/2000/xmlns/').Count) {throw 'Unexpected event XML root.'}
$parts=@{}
foreach ($node in $root.ChildNodes) {
if ($node.NodeType -eq 'Whitespace') {continue}
if ($node.NodeType -ne 'Element' -or $node.NamespaceURI -cne $ns -or $node.LocalName -cnotin @('System','EventData','UserData','BinaryEventData','RenderingInfo') -or $parts.ContainsKey($node.LocalName)) {throw 'Unknown or duplicate event XML section.'}
$parts[$node.LocalName]=$node
}
if (-not $parts.System -or @('EventData','UserData','BinaryEventData'|Where-Object {$parts.ContainsKey($_)}).Count -gt 1) {throw 'Ambiguous event payload.'}
$system=@{}
foreach ($node in $parts.System.ChildNodes) {
if ($node.NodeType -eq 'Whitespace') {continue}
if ($node.NodeType -ne 'Element' -or $node.NamespaceURI -cne $ns -or $system.ContainsKey($node.LocalName)) {throw 'Ambiguous System identity.'}
$system[$node.LocalName]=$node
}
foreach ($name in @('Provider','EventID','Version','TimeCreated','EventRecordID','Channel','Computer')) {if (-not $system.ContainsKey($name)) {throw "Missing native event identity: $name"}}
[uint64]$record=0;[uint32]$eventId=0;[byte]$version=0
if (-not [uint64]::TryParse($system.EventRecordID.InnerText,[ref]$record) -or $record -eq 0 -or -not [uint32]::TryParse($system.EventID.InnerText,[ref]$eventId) -or -not [byte]::TryParse($system.Version.InnerText,[ref]$version)) {throw 'Invalid native numeric event identity.'}
$source=$system.Computer.InnerText
if ($system.Channel.InnerText -cne $Channel -or $source -notmatch '^[\p{L}\p{N}][\p{L}\p{N}_.-]{0,254}$' -or $source -notin $Computer) {throw 'Event source/channel does not match the actual local reader.'}
$provider=$system.Provider.GetAttribute('Name');if ([string]::IsNullOrWhiteSpace($provider)) {throw 'Provider name is unavailable.'}
$keys=@((Get-WelaMeasurementXmlKey $parts.System))
foreach ($name in @('EventData','UserData','BinaryEventData')) {if ($parts.ContainsKey($name)) {$keys+=$name+'='+(Get-WelaMeasurementXmlKey $parts[$name])}}
[pscustomobject]@{RecordId=$record.ToString([Globalization.CultureInfo]::InvariantCulture);Channel=$Channel;Computer=$source;Provider=$provider;ProviderGuid=$system.Provider.GetAttribute('Guid');EventId=$eventId;Version=$version;EventUtc=(ConvertTo-WelaEvtxUtc $system.TimeCreated.GetAttribute('SystemTime')).ToString('o');Key=($keys -join '|')}
}
function Assert-WelaMeasurementBookmark {
param([string]$Xml,$Event)
$doc=Read-WelaMeasurementXmlDocument -Text $Xml -Maximum 65536
if ($doc.DocumentElement.LocalName -cne 'BookmarkList') {throw 'Unexpected native bookmark root.'}
$entries=@($doc.DocumentElement.ChildNodes|Where-Object NodeType -eq Element)
if ($entries.Count -ne 1 -or $entries[0].LocalName -cne 'Bookmark' -or $entries[0].GetAttribute('Channel') -cne $Event.Channel -or $entries[0].GetAttribute('RecordId') -cne $Event.RecordId) {throw 'Native bookmark does not identify the delivered event.'}
}
function New-WelaMeasurementObserver {
param([string]$Channel,[int]$Seconds,[int]$MaximumEvents)
if (-not ('Wela.EventMeasurementV1.Observer' -as [type])) {Add-Type -Path (Join-Path $PSScriptRoot 'EventMeasurementNative.cs') -ErrorAction Stop}
[Wela.EventMeasurementV1.Observer]::new($Channel,$Seconds,$MaximumEvents)
}
function Get-WelaMeasurementQuery {
param([string]$Channel,[array]$Events)
if ($Events.Count -lt 1 -or $Events.Count -gt 1024) {throw 'An EVTX sample requires 1 through 1024 exact record IDs.'}
$ids=@{};$selects=@()
foreach ($event in $Events) {if ($event.RecordId -cnotmatch '^[1-9][0-9]{0,19}$' -or $ids.ContainsKey($event.RecordId)) {throw 'Invalid or duplicate sampled record ID.'};$ids[$event.RecordId]=$true}
$escaped=[Security.SecurityElement]::Escape($Channel)
for ($offset=0;$offset -lt $Events.Count;$offset+=20) {
$last=[Math]::Min($offset+19,$Events.Count-1)
$predicates=@($Events[$offset..$last]|ForEach-Object {'EventRecordID='+$_.RecordId}) -join ' or '
$selects+=''
}
''+($selects -join '')+''
}
function Export-WelaMeasurementEvtx {
param([string]$Channel,[string]$Query,[string]$Path)
$null=Resolve-WelaMeasurementPath $Path
if (Test-Path -LiteralPath $Path) {throw 'EVTX sample path already exists.'}
$session=New-Object System.Diagnostics.Eventing.Reader.EventLogSession
try {$session.ExportLog($Channel,[System.Diagnostics.Eventing.Reader.PathType]::LogName,$Query,$Path,$false)} finally {$session.Dispose()}
}
function Read-WelaMeasurementEvtx {
param([string]$Path,[int]$MaximumEvents)
$query=New-Object System.Diagnostics.Eventing.Reader.EventLogQuery($Path,[System.Diagnostics.Eventing.Reader.PathType]::FilePath,'*')
$query.TolerateQueryErrors=$false
$reader=New-Object System.Diagnostics.Eventing.Reader.EventLogReader($query)
$result=New-Object 'System.Collections.Generic.List[string]'
try {
for ($i=0;$i -le $MaximumEvents;$i++) {
$event=$reader.ReadEvent([timespan]::FromSeconds(5));if ($null -eq $event) {break}
try {$xml=$event.ToXml();if ([Text.Encoding]::UTF8.GetByteCount($xml) -gt 1048576) {throw 'Exported event exceeds the XML cap.'};$result.Add($xml)} finally {$event.Dispose()}
}
} finally {$reader.Dispose()}
return ,$result.ToArray()
}
function Confirm-WelaMeasurementEvtx {
param([string]$Path,[array]$Events,[string]$Channel,[string[]]$Computer)
$null=Resolve-WelaMeasurementPath $Path
# Hold a read handle denying writes/deletion throughout native reopen verification.
$file=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
try {
if ($file.Length -lt 1 -or $file.Length -gt 67108864) {throw 'EVTX export is empty or exceeds the 64 MiB artifact cap.'}
$bytes=$file.Length;$sha=[Security.Cryptography.SHA256]::Create()
try {$hash=([BitConverter]::ToString($sha.ComputeHash($file))).Replace('-','').ToLowerInvariant()} finally {$sha.Dispose()}
$actual=Read-WelaMeasurementEvtx -Path $Path -MaximumEvents $Events.Count
if (@($actual).Count -ne $Events.Count) {throw 'EVTX reopen contains missing or extra records.'}
$expected=@{};foreach ($event in $Events) {$expected[$event.RecordId]=$event.Key}
$seen=@{}
foreach ($xml in $actual) {
$event=Read-WelaMeasurementEvent -Xml $xml -Channel $Channel -Computer $Computer
if ($seen.ContainsKey($event.RecordId) -or -not $expected.ContainsKey($event.RecordId) -or $expected[$event.RecordId] -cne $event.Key) {throw 'EVTX reopen differs from the original delivered event identity or payload.'}
$seen[$event.RecordId]=$true
}
if ($file.Length -ne $bytes) {throw 'EVTX sample changed during verification.'}
[pscustomobject]@{Status='ExactSampleReopened';Name='sample.evtx';Bytes=$bytes;Sha256=$hash;Records=$seen.Count;ByteMeaning='Logical bytes of this specific native EVTX export, including format overhead; not channel growth, allocation, backend storage or retention capacity.'}
} finally {$file.Dispose()}
}
function Invoke-WelaEventMeasurement {
[CmdletBinding()]
param([ValidateSet('Plan','Run')][string]$Action='Plan',[Parameter(Mandatory)][string]$Channel,[ValidateRange(1,60)][int]$Seconds=10,[ValidateRange(1,1024)][int]$MaximumEvents=256,[string]$OutputPath,[switch]$ExportEvtx)
$catalog=Get-WelaMeasurementCatalog
if ($Channel -cnotin $catalog.Channels) {throw 'Select one exact reviewed channel; remote, forwarded, wildcard, Analytic and Debug channels are unsupported.'}
if ($Action -eq 'Plan' -and ($OutputPath -or $ExportEvtx)) {throw 'OutputPath and ExportEvtx require Run; Plan is read-only.'}
if ($Action -eq 'Run' -and [string]::IsNullOrWhiteSpace($OutputPath)) {throw 'Run requires a new private output directory.'}
if ($OutputPath) {$OutputPath=Resolve-WelaMeasurementPath $OutputPath;if(Test-Path -LiteralPath $OutputPath){throw 'Measurement output already exists.'}}
$result=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaLocalDeliveryMeasurement';Action=$Action;Status='Planned';ExitCode=0;GeneratedUtc=[datetime]::UtcNow.ToString('o');Channel=$Channel;RequestedSeconds=$Seconds;MaximumEvents=$MaximumEvents;MaximumXmlBytes=16777216;MaximumEventXmlBytes=1048576;MaximumEvtxBytes=67108864;CatalogSha256=$catalog.Sha256;OutputPath=$OutputPath;Before=$null;After=$null;Window=$null;ObservedDeliveries=0;ObservedDeliveriesPerSecond=$null;LossAssessment='Unknown: Windows subscription diagnostics and consistency checks cannot prove that all upstream events were generated or delivered.';Evtx=[pscustomobject]@{Status='NotRequested';Bytes=$null};Events=@();Artifacts=@();Diagnostic='';PolicyChanges=0;ReadyRuleCredit=0;Scope='Local callback deliveries during one bounded monotonic window. No producer throughput, backend ingestion, retention-duration, storage-growth or Sigma readiness claim.'}
$root=$null;$observer=$null
try {
$result.Before=Get-WelaMeasurementState $Channel
if ($Action -eq 'Plan') {return $result}
$root=New-WelaMeasurementOutput $OutputPath;$result.OutputPath=$root
$result.Artifacts+=Write-WelaMeasurementArtifact $root 'before-state.json' (ConvertTo-Json -InputObject $result.Before -Depth 20)
$observer=New-WelaMeasurementObserver -Channel $Channel -Seconds $Seconds -MaximumEvents $MaximumEvents
$result.Artifacts+=Write-WelaMeasurementArtifact $root 'window-open.json' (ConvertTo-Json -InputObject ([pscustomobject]@{StartedUtc=$observer.StartedUtc;RegistrationSeconds=$observer.RegistrationSeconds;Channel=$Channel;RequestedSeconds=$Seconds;Origin='Future events only; callbacks before the measurement window are excluded.'}))
$capture=$observer.Complete();$observer.Dispose();$observer=$null
$result.Window=[pscustomobject]@{StartedUtc=$capture.StartedUtc;CompletedUtc=$capture.CompletedUtc;RegistrationSeconds=$capture.RegistrationSeconds;ElapsedSeconds=$capture.ElapsedSeconds;NativeStatus=$capture.Status;NativeError=$capture.NativeError;BeforeWindowCallbacks=$capture.BeforeWindowCallbacks;OutsideWindowCallbacks=$capture.OutsideWindowCallbacks;XmlUtf8Bytes=$capture.XmlUtf8Bytes;Clock='Stopwatch monotonic; serialized callback processing time, not event TimeCreated';LastBookmark=$null}
$result.ObservedDeliveries=@($capture.Events).Count
[uint64]$previous=0;$index=0
foreach ($delivery in $capture.Events) {
$index++;$eventName='event-{0:d4}.xml' -f $index;$bookmarkName='bookmark-{0:d4}.xml' -f $index
$result.Artifacts+=Write-WelaMeasurementArtifact $root $eventName $delivery.Xml
$result.Artifacts+=Write-WelaMeasurementArtifact $root $bookmarkName $delivery.BookmarkXml
$event=Read-WelaMeasurementEvent -Xml $delivery.Xml -Channel $Channel -Computer $result.Before.Reader.SourceComputerNames
Assert-WelaMeasurementBookmark $delivery.BookmarkXml $event
if ($previous -ne 0 -and [uint64]$event.RecordId -ne $previous+1) {throw 'Delivered record IDs are duplicated, reordered or discontinuous; completeness is unverified.'}
if ($delivery.ElapsedSeconds -lt 0 -or $delivery.ElapsedSeconds -ge $Seconds) {throw 'Delivery timestamp is outside the monotonic observation window.'}
$previous=[uint64]$event.RecordId
$event|Add-Member NoteProperty ObservedElapsedSeconds $delivery.ElapsedSeconds
$event|Add-Member NoteProperty XmlArtifact $eventName
$event|Add-Member NoteProperty BookmarkArtifact $bookmarkName
$result.Events+= $event;$result.Window.LastBookmark=$bookmarkName
}
$result.After=Get-WelaMeasurementState $Channel
$result.Artifacts+=Write-WelaMeasurementArtifact $root 'after-state.json' (ConvertTo-Json -InputObject $result.After -Depth 20)
Assert-WelaMeasurementState $result.Before $result.After
if ((Get-WelaMeasurementCatalog).Sha256 -cne $catalog.Sha256) {throw 'Measurement catalog changed during collection.'}
if ($capture.Status -cne 'WindowComplete' -or $capture.NativeError -ne 0) {throw ($capture.Status+': '+$capture.Diagnostic)}
if ($capture.ElapsedSeconds -ne $Seconds) {throw 'Native observation window did not complete.'}
if ($result.Events.Count -eq 0) {
$result.Status='NoDeliveriesObserved';$result.Evtx.Status=if($ExportEvtx){'NotCreatedNoEvents'}else{'NotRequested'}
$result.Diagnostic='No deliveries were observed in this window. This does not establish zero producer traffic, capacity or absence of loss.'
} else {
if ($ExportEvtx) {
$result.Evtx.Status='Unverified'
$query=Get-WelaMeasurementQuery $Channel $result.Events
$result.Artifacts+=Write-WelaMeasurementArtifact $root 'sample-query.json' (ConvertTo-Json -InputObject ([pscustomobject]@{Channel=$Channel;Query=$query;RecordIds=@($result.Events.RecordId)}))
Export-WelaMeasurementEvtx -Channel $Channel -Query $query -Path (Join-Path $root 'sample.evtx')
$verified=Confirm-WelaMeasurementEvtx -Path (Join-Path $root 'sample.evtx') -Events $result.Events -Channel $Channel -Computer $result.Before.Reader.SourceComputerNames
$final=Get-WelaMeasurementState $Channel;Assert-WelaMeasurementState $result.Before $final
$result.Artifacts+=Write-WelaMeasurementArtifact $root 'export-after-state.json' (ConvertTo-Json -InputObject $final -Depth 20)
$result.Evtx=$verified
$result.Artifacts+=[pscustomobject]@{Name=$verified.Name;Sha256=$verified.Sha256;Bytes=$verified.Bytes}
}
$result.Status='DeliveryWindowObserved';$result.ObservedDeliveriesPerSecond=$result.Events.Count/[double]$capture.ElapsedSeconds
}
} catch {$result.Status='Unverified';$result.ExitCode=1;$result.ObservedDeliveriesPerSecond=$null;$result.Diagnostic=$_.Exception.Message}
finally {if ($observer) {$observer.Dispose()}}
if ($root) {
# Keys are private in-memory comparators, not evidence content; original XML carries all fields.
foreach ($event in $result.Events) {$event.PSObject.Properties.Remove('Key')}
try {
foreach ($artifact in $result.Artifacts) {
$path=Resolve-WelaMeasurementPath (Join-Path $root $artifact.Name)
if ((Get-Item -LiteralPath $path -Force -ErrorAction Stop).Length -ne $artifact.Bytes -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -cne $artifact.Sha256) {throw 'Saved event evidence changed before the manifest was written.'}
}
if ((Get-WelaMeasurementCatalog).Sha256 -cne $catalog.Sha256) {throw 'Measurement catalog changed before the manifest was written.'}
} catch {
$result.Status='Unverified';$result.ExitCode=1;$result.ObservedDeliveriesPerSecond=$null;$result.Diagnostic+=' Final evidence check failed: '+$_.Exception.Message
if ($result.Evtx.Status -eq 'ExactSampleReopened') {$result.Evtx.Status='Unverified';$result.Evtx.Bytes=$null}
}
try {$null=Write-WelaMeasurementArtifact $root 'manifest.json' (ConvertTo-Json -InputObject $result -Depth 30)}
catch {$result.Status='Unverified';$result.ExitCode=1;$result.ObservedDeliveriesPerSecond=$null;$result.Diagnostic+=' Manifest write failed: '+$_.Exception.Message}
}
return $result
}