mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-01 12:04:46 +02:00
108 lines
11 KiB
PowerShell
108 lines
11 KiB
PowerShell
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent
|
|
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
|
|
. (Join-Path $repo 'scripts/WefArrival.ps1')
|
|
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
|
|
. (Join-Path $repo 'scripts/WmiProbe.ps1')
|
|
Add-Type -Path (Join-Path $repo 'scripts/WmiProbeNative.cs') -ErrorAction Stop
|
|
$script:count=0
|
|
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
|
function Reject($Code,$Pattern){$message='';try{&$Code|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, got $message"}
|
|
function Clone($Value){ConvertFrom-WelaArrivalJson ($Value|ConvertTo-Json -Depth 24 -Compress)}
|
|
# Exercise the actual native token-equivalence gate with synthetic field values.
|
|
$equivalent=[Wela.WmiProbe.Native].GetMethod('Equivalent',[Reflection.BindingFlags]'NonPublic,Static')
|
|
function NativeToken {
|
|
$t=[Wela.WmiProbe.Token]::new();$t.Sid='S-1-5-21-1-2-3-1001';$t.AuthenticationId='0x123'
|
|
$g=[Wela.WmiProbe.Group]::new();$g.Sid='S-1-1-0';$g.Attributes=7;$t.Groups=@($g)
|
|
$p=[Wela.WmiProbe.Privilege]::new();$p.Luid='0x8';$p.Attributes=0;$t.Privileges=@($p);$t
|
|
}
|
|
$a=NativeToken;$b=NativeToken;$a.TokenSource='Process';$b.TokenSource='EquivalentSelfThread'
|
|
Assert ($equivalent.Invoke($null,@($a,$b))) 'Equivalent runtime self token is accepted without replacement.'
|
|
foreach($change in @('Sid','AuthenticationId','GroupSid','GroupAttributes','PrivilegeLuid','PrivilegeAttributes','GroupCount','PrivilegeCount')){
|
|
$b=NativeToken
|
|
switch($change){
|
|
Sid {$b.Sid='S-1-5-18'}
|
|
AuthenticationId {$b.AuthenticationId='0x124'}
|
|
GroupSid {$b.Groups[0].Sid='S-1-5-11'}
|
|
GroupAttributes {$b.Groups[0].Attributes=16}
|
|
PrivilegeLuid {$b.Privileges[0].Luid='0x9'}
|
|
PrivilegeAttributes {$b.Privileges[0].Attributes=2}
|
|
GroupCount {$b.Groups=@()}
|
|
PrivilegeCount {$b.Privileges=@()}
|
|
}
|
|
Assert (-not $equivalent.Invoke($null,@($a,$b))) ('Different effective token is refused: '+$change)
|
|
}
|
|
$token=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1001';Name='LAB\Reader';AuthenticationId='0x123';AuthenticationType='NTLM';ImpersonationLevel='None';Groups=@([pscustomobject]@{Sid='S-1-1-0';Attributes=7});Privileges=@([pscustomobject]@{Luid='0x8';Attributes=0})}
|
|
$descriptor=[pscustomobject]@{ControlFlags=32788;Owner=$null;Group=$null;DACL=@();SACL=@([pscustomobject]@{AceType=2;AceFlags=64;AccessMask=1;Trustee=[pscustomobject]@{SIDString='S-1-1-0'}})}
|
|
$state=[pscustomobject][ordered]@{Namespace='root\default';Computer='LAB';Service='Running';Host=[pscustomobject]@{Status='Observed';Build=26100;ProductType=3;DomainJoined=$false};Token=$token;Descriptor=[pscustomobject]@{Namespace='root\default';DescriptorJson=($descriptor|ConvertTo-Json -Depth 10 -Compress);DescriptorMof='fixture descriptor'};AuditMask=1;Precedence=[pscustomobject]@{ValueExists=$true;Type='DWord';Value=1};Channel=[pscustomobject]@{Name='Security';Enabled=$true;SecurityDescriptor='O:SYG:SYD:(A;;0x1;;;SY)'};Engine='/fixture';EngineHash=('a'*64);Sources='fixture-sources'}
|
|
$operation=[pscustomobject]@{Namespace='root\default';StartedUtc='2025-01-02T03:04:05.1234500Z';CompletedUtc='2025-01-02T03:04:06.1234500Z';ExpectedAccessMask=1;SecurityRecordIdBefore=100;BeforeToken=$token;AfterToken=$token}
|
|
# Clock evidence and exact bounds: coarse or padded intervals cannot authorize events.
|
|
$timed=Clone $operation;$timed|Add-Member NoteProperty Clock 'GetSystemTimePreciseAsFileTime'
|
|
$launch=[DateTimeOffset]'2025-01-02T03:04:05Z';$observed=[DateTimeOffset]'2025-01-02T03:04:07Z'
|
|
$interval=Assert-WelaWmiProbeInterval $timed $launch $observed
|
|
Assert ($interval.Start.UtcDateTime.Ticks -eq ([DateTimeOffset]'2025-01-02T03:04:05.1234500Z').UtcDateTime.Ticks) 'Precise fractional timestamp survives normalization.'
|
|
foreach($case in @('MissingClock','CoarseClock','Reversed','BeforeLaunch','Future','Overlong','ParentReversed')){
|
|
$bad=Clone $timed;$l=$launch;$o=$observed
|
|
switch($case){
|
|
MissingClock {$bad.PSObject.Properties.Remove('Clock')}
|
|
CoarseClock {$bad.Clock='DateTime.UtcNow'}
|
|
Reversed {$bad.CompletedUtc='2025-01-02T03:04:05Z'}
|
|
BeforeLaunch {$bad.StartedUtc='2025-01-02T03:04:04.9999999Z'}
|
|
Future {$bad.CompletedUtc='2025-01-02T03:04:07.0000001Z'}
|
|
Overlong {$bad.CompletedUtc='2025-01-02T03:04:25.1234501Z';$o=[DateTimeOffset]'2025-01-02T03:05:00Z'}
|
|
ParentReversed {$l=$observed;$o=$launch}
|
|
}
|
|
Reject {Assert-WelaWmiProbeInterval $bad $l $o} 'precise fixed worker time interval'
|
|
}
|
|
$clockImport=[Wela.WmiProbe.Native].GetMethod('GetSystemTimePreciseAsFileTime',[Reflection.BindingFlags]'NonPublic,Static').GetCustomAttributes([Runtime.InteropServices.DllImportAttribute],$false)[0]
|
|
Assert ($clockImport.ExactSpelling -and $clockImport.EntryPoint -ceq 'GetSystemTimePreciseAsFileTime') 'The native UTC clock is bound exactly.'
|
|
$xml='<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"/><EventID>4662</EventID><Version>0</Version><Keywords>0x8020000000000000</Keywords><EventRecordID>101</EventRecordID><Channel>Security</Channel><Computer>LAB</Computer><TimeCreated SystemTime="2025-01-02T03:04:05.5000000Z"/></System><EventData><Data Name="SubjectUserSid">S-1-5-21-1-2-3-1001</Data><Data Name="SubjectLogonId">0x123</Data><Data Name="ObjectServer">WMI</Data><Data Name="ObjectName">root\default</Data><Data Name="AccessMask">0x1</Data></EventData></Event>'
|
|
Assert (Test-WelaWmiProbeEvent $xml $operation $state) 'Exact synthetic WMI namespace event matches.'
|
|
foreach($edge in @(@('03:04:05.1234500Z',$true),@('03:04:06.1234500Z',$true),@('03:04:05.1234499Z',$false),@('03:04:06.1234501Z',$false))){
|
|
Assert ((Test-WelaWmiProbeEvent $xml.Replace('03:04:05.5000000Z',$edge[0]) $operation $state) -eq $edge[1]) ('Exact 100ns boundary without positive time padding: '+$edge[0])
|
|
}
|
|
|
|
$mutations=@(
|
|
@('4662','4663'),@('>0</Version>','>1</Version>'),@('>WMI<','>DS<'),@('root\default','root\cimv2'),@('0x1</Data>','0x2</Data>'),@('0x123','0x124'),@('S-1-5-21-1-2-3-1001','S-1-5-21-1-2-3-1002'),@('>LAB<','>OTHER<'),@('>Security<','>Application<'),@('0x8020000000000000','0x8010000000000000'),@('>101<','>100<'),@('03:04:05.5000000Z','03:04:05.1000000Z'),@('03:04:05.5000000Z','03:04:06.5000000Z'),@('54849625-5478-4994-a5ba-3e3b0328c30d','54849625-5478-4994-a5ba-3e3b0328c30e'),@('Name="AccessMask"','Name="SubjectLogonId"'))
|
|
foreach($pair in $mutations){$changed=$xml.Replace($pair[0],$pair[1]);Assert ($changed -cne $xml) 'Mutation changed the fixture.';Assert (-not(Test-WelaWmiProbeEvent $changed $operation $state)) ('Reject mismatched '+$pair[0])}
|
|
Assert (-not(Test-WelaWmiProbeEvent ('<!DOCTYPE Event [<!ENTITY x "WMI">]>'+$xml.Replace('>WMI<','>&x;<')) $operation $state)) 'DTD input is refused.'
|
|
Assert (-not(Test-WelaWmiProbeEvent $xml.Replace('</EventData>','<Data Name="ObjectName">root\default</Data></EventData>') $operation $state)) 'Duplicate event data are refused.'
|
|
Assert (-not(Test-WelaWmiProbeEvent $xml.Replace('</System>','<EventID>4662</EventID></System>') $operation $state)) 'Duplicate System fields are refused.'
|
|
Assert (-not(Test-WelaWmiProbeEvent ('x'*131073) $operation $state)) 'Oversized raw evidence is refused.'
|
|
foreach($name in @('root\default','root\WelaProbe_a123','root\cimv2\security')){Assert-WelaWmiProbeNamespace $name;Assert $true 'Exact local namespace accepted.'}
|
|
foreach($name in @('root','ROOT\default','\\remote\root\default','root\default:__SystemSecurity=@','root\*','root\..\default','root/default','root\default;Write-Host x')){Reject {Assert-WelaWmiProbeNamespace $name} 'exact local'}
|
|
$null=Get-WelaWmiProbeStateKey $state
|
|
foreach($mask in @(0,2,4)){$bad=Clone $state;$bad.AuditMask=$mask;Reject {Get-WelaWmiProbeStateKey $bad} 'success auditing'}
|
|
$bad=Clone $state;$bad.Precedence.Type='String';Reject {Get-WelaWmiProbeStateKey $bad} 'typed audit'
|
|
$bad=Clone $state;$bad.Channel.Enabled=$false;Reject {Get-WelaWmiProbeStateKey $bad} 'Security channel'
|
|
$bad=Clone $state;$bad.Token.Groups[0].Attributes=16;Reject {Get-WelaWmiProbeStateKey $bad} 'No observed success'
|
|
foreach($field in @('AceType','AceFlags','AccessMask')){$d=Clone $descriptor;$d.SACL[0].$field=0;$bad=Clone $state;$bad.Descriptor.DescriptorJson=$d|ConvertTo-Json -Depth 10 -Compress;Reject {Get-WelaWmiProbeStateKey $bad} 'No observed success'}
|
|
$bad=Clone $state;$bad.Host.Build=99999;Reject {Get-WelaWmiProbeStateKey $bad} 'outside'
|
|
$bad=Clone $state;$bad.Service='Stopped';Reject {Get-WelaWmiProbeStateKey $bad} 'already be running'
|
|
Reject {Invoke-WelaWmiProbe -Namespace 'root\default' -Action Run} 'new WmiProbeOutputPath'
|
|
Reject {Invoke-WelaWmiProbe -Namespace 'root\default' -OutputPath ignored} 'Plan creates no files'
|
|
# Public production report path; only native boundaries are mocked here.
|
|
$script:mode='Success';$script:reads=0;$script:workerCalls=0
|
|
function Get-WelaWmiProbeState {param($Namespace);$script:reads++;$copy=Clone $state;if($script:mode -eq 'Drift' -and $script:reads -gt 1){$copy.Sources='changed'};if($script:mode -eq 'Blocked'){$copy.AuditMask=0};$copy}
|
|
function Start-WelaWmiProbeRead {param($State);$script:workerCalls++;$operation}
|
|
function Read-WelaWmiProbeEvents {param($Operation);if($script:mode -eq 'ReadError'){throw 'native read denied'};[pscustomobject]@{Xml=@($xml);Capped=($script:mode -eq 'Cap');Query='fixture bounded query'}}
|
|
function Get-WelaWmiProbeWatermark {if($script:mode -eq 'Clear'){99}else{101}}
|
|
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-fixtures-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $temp
|
|
try{
|
|
$plan=Invoke-WelaWmiProbe -Namespace 'root\default'
|
|
Assert ($plan.Status -eq 'PrerequisitesObserved' -and $script:workerCalls -eq 0) 'Default Plan never invokes the fixed worker.'
|
|
foreach($mode in @('Success','Blocked','Cap','ReadError','Drift','Clear')){
|
|
$script:mode=$mode;$script:reads=0;$script:workerCalls=0;$dir=Join-Path $temp $mode
|
|
$result=Invoke-WelaWmiProbe -Action Run -Namespace 'root\default' -OutputPath $dir -TimeoutSeconds 1
|
|
$manifest=ConvertFrom-WelaArrivalJson ([IO.File]::ReadAllText((Join-Path $dir 'manifest.json')))
|
|
Assert ($manifest.ReadyRuleCredit -eq 0 -and $manifest.PolicyChanges -eq 0 -and $manifest.NamespaceChanges -eq 0) 'Every report retains the no-change/no-readiness boundary.'
|
|
Assert ($null -ne $manifest.After) 'After observation survives success/failure.'
|
|
foreach($artifact in $manifest.Artifacts){Assert ($artifact.Sha256 -ceq (Get-FileHash -LiteralPath (Join-Path $dir $artifact.Name)).Hash.ToLowerInvariant()) 'Manifest artifact hashes match exact written bytes.'}
|
|
if($mode -eq 'Success'){Assert ($result.Status -eq 'LocalNamespaceAccessObserved' -and $result.Matches -eq 1 -and $result.ExitCode -eq 0) 'Public report verifies the bounded event.';Assert ([IO.File]::ReadAllText((Join-Path $dir 'event-1.xml')) -ceq $xml) 'Raw event XML is preserved.'}
|
|
else{Assert ($result.Status -eq 'Unverified' -and $result.ExitCode -eq 1 -and $result.Diagnostic) "Failure $mode never becomes observed."}
|
|
if($mode -eq 'Blocked'){Assert ($script:workerCalls -eq 0) 'Missing prerequisites block worker invocation.'}
|
|
}
|
|
Reject {Invoke-WelaWmiProbe -Action Run -Namespace 'root\default' -OutputPath (Join-Path $temp 'Success')} 'new directory'
|
|
}finally{Remove-Item -LiteralPath $temp -Recurse -Force}
|
|
Write-Host "PASS: $script:count WMI probe fixtures; native boundaries were mocked."
|
|
$global:LASTEXITCODE=0
|