diff --git a/.gitattributes b/.gitattributes index 7f4524b7..ac5a1db3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -96,3 +96,7 @@ tests/NativeProviderConfigure.Windows.Tests.ps1 text eol=lf /modules/WecSubscriptionInventory.cs text eol=lf /tests/WecCollectorObservation* text eol=lf /tests/WecSubscriptionInventory* text eol=lf + +# Native query receipts bind the same source bytes on every supported engine. +/scripts/WefQuery* text eol=lf +/tests/WefQuery* text eol=lf diff --git a/scripts/WefQuery.ps1 b/scripts/WefQuery.ps1 index 66ab9d61..9e071498 100644 --- a/scripts/WefQuery.ps1 +++ b/scripts/WefQuery.ps1 @@ -22,7 +22,15 @@ function Get-WelaWefQuerySources { } function Get-WelaWefQueryToken { Initialize-WelaWefQueryNative - [Wela.WefQueryToken.Native]::Snapshot() + ConvertTo-WelaWefQueryTokenObservation ([Wela.WefQueryToken.Native]::Snapshot()) +} +function ConvertTo-WelaWefQueryTokenObservation { + param($Token) + if($Token -isnot [Wela.WefQueryToken.Token]){throw 'Expected the native query token observation.'} + # Normalize native DTOs at the boundary, using the same strict shape as worker receipts. + $observed=ConvertFrom-WelaArrivalJson (Get-WelaWefQueryKey $Token) + $null=Get-WelaWefQueryTokenKey $observed + $observed } function Get-WelaWefQueryTokenKey { param($Token) diff --git a/tests/WefQuery.Tests.ps1 b/tests/WefQuery.Tests.ps1 index 54f1912c..79a9bbbf 100644 --- a/tests/WefQuery.Tests.ps1 +++ b/tests/WefQuery.Tests.ps1 @@ -7,6 +7,15 @@ function Assert($value,[string]$message){if(-not $value){throw $message};$script function Reject([scriptblock]$code,[string]$message){$caught=$false;try{& $code|Out-Null}catch{$caught=$true};Assert $caught $message} function Clone($value){ConvertFrom-WelaArrivalJson (Get-WelaWefQueryKey $value)} Initialize-WelaWefQueryNative +$nativeToken=[Wela.WefQueryToken.Token]::new();$nativeToken.Sid='S-1-5-21-1-2-3-1000';$nativeToken.Name='Host\reader';$nativeToken.AuthenticationId='0x123';$nativeToken.AuthenticationType='NTLM';$nativeToken.ImpersonationLevel='None';$nativeToken.TokenSource='Process' +$nativeGroup=[Wela.WefQueryToken.Group]::new();$nativeGroup.Sid='S-1-5-32-545';$nativeGroup.Attributes=[uint32]::MaxValue;$nativeToken.Groups=@($nativeGroup);$nativeToken.Privileges=@() +$observedToken=ConvertTo-WelaWefQueryTokenObservation $nativeToken +Assert ($observedToken -is [pscustomobject] -and $observedToken.Groups -is [array] -and $observedToken.Groups.Count -eq 1 -and $observedToken.Privileges -is [array] -and $observedToken.Privileges.Count -eq 0) 'Actual native DTO normalizes singleton groups and empty privileges for strict receipt validation.' +Assert ($observedToken.Groups[0].Attributes -eq [uint32]::MaxValue -and (Get-WelaWefQueryTokenKey $observedToken) -ceq (Get-WelaWefQueryTokenKey (Clone $nativeToken))) 'Native token normalization preserves every unsigned attribute and token comparison.' +$nativePrivilege=[Wela.WefQueryToken.Privilege]::new();$nativePrivilege.Luid='0x14';$nativePrivilege.Attributes=2;$nativeToken.Privileges=@($nativePrivilege) +Assert ((ConvertTo-WelaWefQueryTokenObservation $nativeToken).Privileges[0].Attributes -eq 2) 'Native privilege DTO normalizes without losing enabled attributes.' +$nativeToken.Sid='invalid';Reject {ConvertTo-WelaWefQueryTokenObservation $nativeToken} 'Invalid native token remains unverified.' +Reject {ConvertTo-WelaWefQueryTokenObservation $observedToken} 'Native boundary rejects a substituted arbitrary object.' $buffer=[Runtime.InteropServices.Marshal]::AllocHGlobal(128) try{ function Reset-Buffer([int]$type,[int]$count){for($i=0;$i -lt 128;$i++){[Runtime.InteropServices.Marshal]::WriteByte($buffer,$i,0)};[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,$type);[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,$count);[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,[IntPtr]::Add($buffer,16))} diff --git a/tests/WefQuery.Windows.Tests.ps1 b/tests/WefQuery.Windows.Tests.ps1 index bdc2c099..9d109e0c 100644 --- a/tests/WefQuery.Windows.Tests.ps1 +++ b/tests/WefQuery.Windows.Tests.ps1 @@ -102,10 +102,15 @@ finally{ if($aclChanged){try{& wevtutil.exe sl $channel ('/ca:'+$channelBefore.SecurityDescriptor);if($LASTEXITCODE -ne 0){throw 'Original descriptor restore failed.'};$global:LASTEXITCODE=0}catch{$cleanupErrors+=$_.Exception.Message}} if($ownedSid){try{$current=Get-LocalUser -Name $userName -ErrorAction Stop;if($current.SID.Value -cne $ownedSid){throw 'Owned account changed identity.'};Remove-LocalUser -SID $ownedSid -ErrorAction Stop;if(Get-LocalUser -SID $ownedSid -ErrorAction SilentlyContinue){throw 'Owned account remains.'}}catch{$cleanupErrors+=$_.Exception.Message}} $restored=$null;try{$restored=[pscustomobject]@{Host=(Get-WelaWefQueryHost);Profiles=@(Profiles);Hives=@(Hives);Services=(Services);Channels=(NativeChannels);Masks=(Get-WelaEffectiveAuditPolicy);Precedence=(Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy);Token=(Get-WelaWefQueryToken)};Save 'restored.json' $restored}catch{$cleanupErrors+=$_.Exception.Message} - $channelsRestored=$restored -and (Key $restored.Channels) -ceq (Key $before.Channels);$servicesRestored=$restored -and (Key $restored.Services) -ceq (Key $before.Services);$policyRestored=$restored -and (Key $restored.Masks) -ceq (Key $before.Masks) -and (Key $restored.Precedence) -ceq (Key $before.Precedence);$tokenRestored=$restored -and (Get-WelaWefQueryTokenKey $restored.Token) -ceq (Get-WelaWefQueryTokenKey $before.Token) - $profilesRestored=$restored -and (Key $restored.Profiles) -ceq (Key $before.Profiles) -and (Key $restored.Hives) -ceq (Key $before.Hives) - if(-not $profilesRestored -or -not $channelsRestored -or -not $servicesRestored -or -not $policyRestored -or -not $tokenRestored){$cleanupErrors+='Original profile/hive/channel/services/policy/token differ.'} - Save 'cleanup.json' ([pscustomobject]@{Passed=$passed;Assertions=$script:assertions;ChannelsRestored=[bool]$channelsRestored;ServicesRestored=[bool]$servicesRestored;PolicyRestored=[bool]$policyRestored;TokenRestored=[bool]$tokenRestored;ProfilesAndHivesRestored=[bool]$profilesRestored;AccountRemoved=[bool](-not $ownedSid -or -not(Get-LocalUser -SID $ownedSid -ErrorAction SilentlyContinue));Errors=$cleanupErrors;EventGeneration='Not tested';Forwarding='Not tested';Sources=(Get-WelaWefQuerySources)}) + $channelsRestored=$false;$servicesRestored=$false;$policyRestored=$false;$tokenRestored=$false;$profilesRestored=$false;$accountRemoved=$false;$cleanupSources=$null + try{ + $channelsRestored=$restored -and (Key $restored.Channels) -ceq (Key $before.Channels);$servicesRestored=$restored -and (Key $restored.Services) -ceq (Key $before.Services);$policyRestored=$restored -and (Key $restored.Masks) -ceq (Key $before.Masks) -and (Key $restored.Precedence) -ceq (Key $before.Precedence);$tokenRestored=$restored -and (Get-WelaWefQueryTokenKey $restored.Token) -ceq (Get-WelaWefQueryTokenKey $before.Token) + $profilesRestored=$restored -and (Key $restored.Profiles) -ceq (Key $before.Profiles) -and (Key $restored.Hives) -ceq (Key $before.Hives) + }catch{$cleanupErrors+='Cleanup comparison: '+$_.Exception.Message} + try{$accountRemoved=-not $ownedSid -or -not(Get-LocalUser -SID $ownedSid -ErrorAction SilentlyContinue)}catch{$cleanupErrors+='Account observation: '+$_.Exception.Message} + try{$cleanupSources=Get-WelaWefQuerySources}catch{$cleanupErrors+='Source observation: '+$_.Exception.Message} + if(-not $profilesRestored -or -not $channelsRestored -or -not $servicesRestored -or -not $policyRestored -or -not $tokenRestored -or -not $accountRemoved){$cleanupErrors+='Original profile/hive/channel/services/policy/token or owned account differ.'} + Save 'cleanup.json' ([pscustomobject]@{Passed=$passed;Assertions=$script:assertions;ChannelsRestored=[bool]$channelsRestored;ServicesRestored=[bool]$servicesRestored;PolicyRestored=[bool]$policyRestored;TokenRestored=[bool]$tokenRestored;ProfilesAndHivesRestored=[bool]$profilesRestored;AccountRemoved=[bool]$accountRemoved;Errors=$cleanupErrors;EventGeneration='Not tested';Forwarding='Not tested';Sources=$cleanupSources}) if($cleanupErrors.Count){throw ('Fixture cleanup incomplete: '+($cleanupErrors -join '; '))} } Write-Host "WefQuery.Windows.Tests: $script:assertions actual native assertions passed; complete owned fixture cleanup."