mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-11-09 22:49:58 +01:00
Validate native AppLocker EXE event generation with an opt-in probe (#423)
* Add native AppLocker EXE event validation probe * Reference PR 423 in changelogs * Isolate AppLocker native fixture and preserve prerequisite diagnostics * Report an integer zero for an empty AppLocker policy * Prepare disposable AppLocker probe policy without bypassing production importer guards * Retain bounded native AppLocker channel diagnostics on probe failure * Require native policy application before the disposable AppLocker probe * Preserve exact timestamp strings in native evidence fixtures * Record actual runner session and AppLocker publication diagnostics * Activate and restore the native policy converter on disposable AppLocker hosts * Compare native task freshness without guessing its timestamp timezone * Verify effective policy and borrowed converter inactivity during fixture cleanup * Track the actual native policy-converter task instance instead of cached timestamps
This commit is contained in:
1 parent
ff0e5c1890
commit
5ba53fbcfb
17 files changed
+351
-7
No files matched your search
@@ -0,0 +1,36 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
. "$repo/scripts/AppLockerReadiness.ps1"
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/AppLockerProbe.ps1"
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Reject([scriptblock]$Action,[string]$Pattern){$message='';try{&$Action|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern; got $message"}
|
||||
$policy='<AppLockerPolicy Version="1"><RuleCollection Type="Exe" EnforcementMode="AuditOnly"><FilePathRule Id="12345678-1234-1234-1234-123456789abc" Name="Windows" UserOrGroupSid="S-1-1-0" Action="Allow"><Conditions><FilePathCondition Path="%WINDIR%\*" /></Conditions></FilePathRule></RuleCollection></AppLockerPolicy>'
|
||||
$state=[pscustomobject]@{Host=[pscustomobject]@{Status='Candidate';Is64BitProcess=$true;PartOfDomain=$false};Computer='TEST';Domain='WORKGROUP';Reader=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1000'};EffectivePolicy=[pscustomobject]@{Status='Observed';Policy=(ConvertFrom-WelaAppLockerXml $policy)};Service=[pscustomobject]@{Status='Observed';State='Running';StartMode='Manual'};Channel=[pscustomobject]@{Name='Microsoft-Windows-AppLocker/EXE and DLL';Enabled=$true;SecurityDescriptor='O:SYG:SYD:(A;;0x1;;;SY)'};Source='C:\Windows\System32\cmd.exe';SourceHash=('a'*64)}
|
||||
$null=Get-WelaAppLockerProbeKey $state;Assert $true 'Valid audit-only prereqs'
|
||||
foreach($mode in @('Enabled','NotConfigured')){$state.EffectivePolicy.Policy=ConvertFrom-WelaAppLockerXml ($policy.Replace('AuditOnly',$mode));Reject {Get-WelaAppLockerProbeKey $state} 'AuditOnly'}
|
||||
$state.EffectivePolicy.Policy=ConvertFrom-WelaAppLockerXml $policy
|
||||
$state.Service.State='Stopped';Reject {Get-WelaAppLockerProbeKey $state} 'already be running';$state.Service.State='Running'
|
||||
$state.Channel.Enabled=$false;Reject {Get-WelaAppLockerProbeKey $state} 'enabled';$state.Channel.Enabled=$true
|
||||
$process=[pscustomobject]@{Executable='C:\Temp\wela-owned.exe';ProcessId=1234;UserSid=$state.Reader.Sid;StartedUtc='2026-09-01T00:00:00.0000000Z'}
|
||||
$event='<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><System><Provider Name="Microsoft-Windows-AppLocker" Guid="{cbda4dbf-8d5d-4f69-9578-be14aa540d22}"/><EventID>8003</EventID><Version>0</Version><EventRecordID>42</EventRecordID><TimeCreated SystemTime="2026-09-01T00:00:01.0000000Z"/><Channel>Microsoft-Windows-AppLocker/EXE and DLL</Channel><Computer>TEST</Computer></System><UserData><RuleAndFileData xmlns="http://schemas.microsoft.com/schemas/event/Microsoft.Windows/1.0.0.0"><PolicyName>EXE</PolicyName><TargetUser>S-1-5-21-1-2-3-1000</TargetUser><TargetProcessId>1234</TargetProcessId><FilePath>C:\Temp\wela-owned.exe</FilePath></RuleAndFileData></UserData></Event>'
|
||||
$end=([DateTimeOffset]::Parse('2026-09-01T00:00:02Z')).UtcDateTime
|
||||
Assert (Test-WelaAppLockerProbeEvent $event $process $state $end) 'Exact fixture must match'
|
||||
Assert (Test-WelaAppLockerProbeEvent ($event.Replace('8003','8002')) $process $state $end) 'Allowed event matches but has distinct EventId'
|
||||
$mutations=@(@('8003','8004'),@('1234','1235'),@('S-1-5-21-1-2-3-1000','S-1-5-21-1-2-3-1001'),@('C:\Temp\wela-owned.exe','C:\Temp\other.exe'),@('<PolicyName>EXE','<PolicyName>DLL'),@('<Computer>TEST','<Computer>OTHER'),@('cbda4dbf','abda4dbf'),@('<Version>0','<Version>1'),@('00:00:01.0000000Z','00:00:03.0000000Z'),@('</System>','<EventID>8003</EventID></System>'),@('</RuleAndFileData>','<TargetUser>S-1-1-0</TargetUser></RuleAndFileData>'))
|
||||
foreach($pair in $mutations){$bad=$event.Replace($pair[0],$pair[1]);Assert ($bad -cne $event) 'Mutation changed fixture';Assert (-not(Test-WelaAppLockerProbeEvent $bad $process $state $end)) ('Reject '+$pair[0])}
|
||||
Assert (-not(Test-WelaAppLockerProbeEvent ('<!DOCTYPE Event [<!ENTITY x SYSTEM "file:///etc/passwd">]>'+$event) $process $state $end)) 'DTD rejected'
|
||||
Reject {Invoke-WelaAppLockerProbe -Action Run} 'requires'
|
||||
Reject {Invoke-WelaAppLockerProbe -Action Plan -OutputPath ignored} 'requires'
|
||||
# Exercise actual orchestration with mocked native boundaries, including duplicate/drift/cap failures.
|
||||
$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-applocker-test-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
|
||||
$script:state=$state;$script:event=$event;$script:process=$process;$script:scenario='ok';$script:reads=0
|
||||
function Get-WelaAppLockerProbeState {$script:reads++;if($script:scenario -eq 'drift' -and $script:reads -gt 1){$script:state.Service.StartMode='Auto'};$script:state}
|
||||
function Start-WelaAppLockerProbeProcess {param($Root,$State);$path=Join-Path $Root 'fixed.exe';[IO.File]::WriteAllText($path,'fixed');$script:process|Add-Member NoteProperty ExecutableHash (Get-FileHash $path).Hash.ToLowerInvariant() -Force;$script:process.Executable=$path;$script:process}
|
||||
function Read-WelaAppLockerProbeEvents {param($StartUtc,$EndUtc);[pscustomobject]@{Xml=if($script:scenario -eq 'duplicate'){@($script:event,$script:event)}else{@($script:event)};Capped=($script:scenario -eq 'cap')}}
|
||||
function Test-WelaAppLockerProbeEvent {$true}
|
||||
try {
|
||||
foreach($scenario in @('ok','duplicate','drift','cap')){$script:scenario=$scenario;$script:reads=0;$state.Service.StartMode='Manual';$result=Invoke-WelaAppLockerProbe Run (Join-Path $root $scenario) 1;Assert ($result.ReadyRuleCredit -eq 0 -and $result.PolicyChanges -eq 0) 'No readiness or changes';Assert (($result.ExitCode -eq 0) -eq ($scenario -eq 'ok')) "Expected outcome $scenario : $($result.Diagnostic)";Assert (Test-Path (Join-Path $result.OutputPath 'manifest.json')) 'Failure/success manifest retained'}
|
||||
} finally {Remove-Item -LiteralPath $root -Recurse -Force}
|
||||
Write-Host "AppLocker probe tests passed: $count assertions."
|
||||
@@ -0,0 +1,118 @@
|
||||
param([switch]$AllowDisposablePolicyWrite)
|
||||
$ErrorActionPreference='Stop'
|
||||
if($env:OS -ne 'Windows_NT'){Write-Host 'Skipped: Windows required.';exit 0}
|
||||
if(-not $AllowDisposablePolicyWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit disposable GitHub-hosted policy-write opt-in required.'}
|
||||
function Refresh-DisposableComputerPolicy {
|
||||
$info=New-Object Diagnostics.ProcessStartInfo
|
||||
$info.FileName=Join-Path ([Environment]::SystemDirectory) 'gpupdate.exe';$info.Arguments='/target:computer /force /wait:30';$info.UseShellExecute=$false
|
||||
$process=[Diagnostics.Process]::Start($info)
|
||||
try {if(-not $process.WaitForExit(60000)){$process.Kill();throw 'Disposable computer policy refresh exceeded 60 seconds.'};if($process.ExitCode -ne 0){throw ('Disposable computer policy refresh failed: '+$process.ExitCode)}} finally {$process.Dispose()}
|
||||
}
|
||||
function Stop-DisposablePolicyConverter {
|
||||
$task=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop
|
||||
if($task.State -in @('Running','Queued')) {Stop-ScheduledTask -InputObject $task -ErrorAction Stop}
|
||||
$deadline=[DateTime]::UtcNow.AddSeconds(15)
|
||||
do {$task=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop;if($task.State -in @('Ready','Disabled')){return};Start-Sleep -Milliseconds 200}while([DateTime]::UtcNow -lt $deadline)
|
||||
throw 'The verified borrowed PolicyConverter task did not become idle.'
|
||||
}
|
||||
function Run-DisposablePolicyConverter {
|
||||
# The Task Scheduler CIM provider can retain stale LastRunTime on Server2022.
|
||||
# Follow the actual COM Run instance and native completion state instead.
|
||||
$scheduler=$null;$folder=$null;$registered=$null;$instance=$null;$running=$null;$definition=$null;$settings=$null
|
||||
try {
|
||||
$scheduler=New-Object -ComObject 'Schedule.Service';$scheduler.Connect()
|
||||
$folder=$scheduler.GetFolder('\Microsoft\Windows\AppID');$registered=$folder.GetTask('PolicyConverter')
|
||||
$definition=$registered.Definition;$settings=$definition.Settings
|
||||
if(-not $settings.AllowDemandStart){throw 'The verified PolicyConverter task does not allow an on-demand invocation.'}
|
||||
$running=$registered.GetInstances(0)
|
||||
if($running.Count -ne 0 -or $registered.State -ne 3){throw 'The verified borrowed PolicyConverter task must be idle before invocation.'}
|
||||
$null=[Runtime.InteropServices.Marshal]::FinalReleaseComObject($running);$running=$null
|
||||
$instance=$registered.Run($null)
|
||||
if($null -eq $instance -or [string]::IsNullOrWhiteSpace($instance.InstanceGuid)){throw 'Native PolicyConverter did not return a task instance identity.'}
|
||||
$instanceId=[string]$instance.InstanceGuid;$deadline=[DateTime]::UtcNow.AddSeconds(30)
|
||||
do {
|
||||
$running=$registered.GetInstances(0)
|
||||
try {$idle=$running.Count -eq 0 -and $registered.State -eq 3}finally{$null=[Runtime.InteropServices.Marshal]::FinalReleaseComObject($running);$running=$null}
|
||||
if($idle){if($registered.LastTaskResult -ne 0){throw ('Native policy conversion failed: '+$registered.LastTaskResult)};Write-Host ('Native PolicyConverter instance completed: '+$instanceId);return}
|
||||
Start-Sleep -Milliseconds 200
|
||||
}while([DateTime]::UtcNow -lt $deadline)
|
||||
Stop-DisposablePolicyConverter
|
||||
throw 'The owned native PolicyConverter instance did not complete within thirty seconds.'
|
||||
}finally{foreach($item in @($running,$instance,$settings,$definition,$registered,$folder,$scheduler)){if($null -ne $item -and [Runtime.InteropServices.Marshal]::IsComObject($item)){$null=[Runtime.InteropServices.Marshal]::FinalReleaseComObject($item)}}}
|
||||
}
|
||||
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
. "$repo/scripts/Configuration.ps1"
|
||||
. "$repo/scripts/AppLockerReadiness.ps1"
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/AppLockerProbe.ps1"
|
||||
$root=Join-Path $env:RUNNER_TEMP ('wela-applocker-native-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
|
||||
Write-Host ('Native fixture process session: '+[Diagnostics.Process]::GetCurrentProcess().SessionId)
|
||||
$converter=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop
|
||||
$converterBefore=Export-ScheduledTask -InputObject $converter -ErrorAction Stop
|
||||
$converterDisabled=$converter.State -eq 'Disabled';$converterChanged=$false
|
||||
$actions=@($converter.Actions)
|
||||
if($converter.State -notin @('Disabled','Ready') -or $actions.Count -ne 1 -or [Environment]::ExpandEnvironmentVariables($actions[0].Execute).Trim('"') -ine (Join-Path ([Environment]::SystemDirectory) 'appidpolicyconverter.exe') -or $actions[0].Arguments){throw ('Only the unchanged native PolicyConverter action is permitted: '+($actions|ConvertTo-Json -Depth 8))}
|
||||
$before=Get-WelaAppLockerReadiness
|
||||
if($before.Host.PartOfDomain -or $before.Management.Status -ne 'Observed' -or $before.LocalPolicy.Status -ne 'Observed' -or $before.EffectiveGpPolicy.Status -ne 'Observed' -or $before.LocalPolicy.Policy.TotalRules -ne 0 -or $before.EffectiveGpPolicy.Policy.TotalRules -ne 0 -or $before.LocalPolicy.Policy.HasUnknownPolicyData -or $before.EffectiveGpPolicy.Policy.HasUnknownPolicyData){Write-Host ($before | ConvertTo-Json -Depth 16);throw 'Disposable test requires empty, understood local/effective policies on a non-domain disposable host.'}
|
||||
$backup=Join-Path $root 'policy-before.xml';[IO.File]::WriteAllText($backup,$before.LocalPolicy.Policy.Xml)
|
||||
[IO.File]::WriteAllText((Join-Path $root 'prerequisites-before.json'),($before | ConvertTo-Json -Depth 16))
|
||||
$fixture='<AppLockerPolicy Version="1"><RuleCollection Type="Exe" EnforcementMode="AuditOnly"><FilePathRule Id="12345678-1234-1234-1234-123456789abc" Name="Disposable Windows path only" Description="Owned native event fixture" UserOrGroupSid="S-1-1-0" Action="Allow"><Conditions><FilePathCondition Path="%WINDIR%\*" /></Conditions></FilePathRule></RuleCollection></AppLockerPolicy>'
|
||||
$policyPath=Join-Path $root 'fixture.xml';[IO.File]::WriteAllText($policyPath,$fixture)
|
||||
$log=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Microsoft-Windows-AppLocker/EXE and DLL');$enabled=$log.IsEnabled;$touched=$false;$cleanup=@();$primary=$null
|
||||
try {
|
||||
$touched=$true
|
||||
# Test-only preparation under explicit disposable-host and empty-GP gates.
|
||||
# Hosted images contain enrollment/provider keys: preserve them and CSP Unknown.
|
||||
# The production importer must continue to reject those observations.
|
||||
$preparedUtc=[DateTime]::UtcNow
|
||||
Set-AppLockerPolicy -XmlPolicy $policyPath -ErrorAction Stop
|
||||
if($before.Service.StartMode -eq 'Disabled'){throw 'Test will not change protected AppIDSvc startup mode.'}
|
||||
if($before.Service.State -ne 'Running'){Start-Service AppIDSvc -ErrorAction Stop}
|
||||
$log.IsEnabled=$true;$log.SaveChanges()
|
||||
if($converterDisabled){$converterChanged=$true;$null=Enable-ScheduledTask -InputObject $converter -ErrorAction Stop}
|
||||
Refresh-DisposableComputerPolicy
|
||||
Run-DisposablePolicyConverter
|
||||
$applied=$false;$applyDeadline=[DateTime]::UtcNow.AddSeconds(30)
|
||||
do {
|
||||
$records=@()
|
||||
try {try{$records=@(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-AppLocker/EXE and DLL';Id=8001;StartTime=$preparedUtc} -MaxEvents 10 -ErrorAction Stop)}catch{if($_.FullyQualifiedErrorId -notlike 'NoMatchingEventsFound*'){throw}};$applied=$records.Count -gt 0} finally {foreach($record in $records){$record.Dispose()}}
|
||||
if($applied){break};Start-Sleep -Milliseconds 250
|
||||
} while([DateTime]::UtcNow -lt $applyDeadline)
|
||||
if(-not $applied){throw 'No native 8001 policy-applied event after disposable GP refresh.'}
|
||||
Write-Host 'Native 8001 policy-applied evidence observed after disposable GP refresh.'
|
||||
# Wait for actual effective audit-only policy, without treating elapsed time as success.
|
||||
$deadline=[DateTime]::UtcNow.AddSeconds(30)
|
||||
do {$state=Get-WelaAppLockerProbeState;$ready=$false;try{$null=Get-WelaAppLockerProbeKey $state;$ready=$true}catch{};if($ready){break};Start-Sleep -Milliseconds 500}while([DateTime]::UtcNow -lt $deadline)
|
||||
$probe=Invoke-WelaAppLockerProbe -Action Run -OutputPath (Join-Path $root 'evidence') -TimeoutSeconds 30
|
||||
if($probe.ExitCode -or $probe.Status -ne 'NativeExeEventObserved' -or $probe.EventId -ne 8003){throw ($probe|ConvertTo-Json -Depth 24)}
|
||||
foreach($artifact in $probe.Artifacts){if((Get-FileHash (Join-Path $probe.OutputPath $artifact.Name)).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'Artifact hash mismatch'}}
|
||||
Write-Host "Native AppLocker 8003 observed under PowerShell $($PSVersionTable.PSVersion), build $($probe.Before.Host.Build). Zero Sigma credit."
|
||||
} catch {
|
||||
$primary=$_;Write-Host $_
|
||||
Get-ChildItem -LiteralPath $root -Recurse -Filter 'candidate-*.xml'|ForEach-Object {Write-Host ([IO.File]::ReadAllText($_.FullName))}
|
||||
# Read-only diagnostic independent of the production XPath filter and parser.
|
||||
try {
|
||||
$recent=@(Get-WinEvent -LogName 'Microsoft-Windows-AppLocker/EXE and DLL' -MaxEvents 12 -ErrorAction Stop)
|
||||
try {foreach($record in $recent){Write-Host ('Recent native channel XML: '+$record.ToXml())}} finally {foreach($record in $recent){$record.Dispose()}}
|
||||
} catch {Write-Host ('Recent native channel read: '+$_.Exception.Message)}
|
||||
Get-CimInstance Win32_SystemDriver -Filter "Name='AppID'" | Select-Object Name,State,StartMode | ConvertTo-Json | Write-Host
|
||||
try {Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -ErrorAction Stop | Select-Object TaskName,State | ConvertTo-Json | Write-Host}catch{Write-Host ('AppID task read: '+$_.Exception.Message)}
|
||||
try {$nativeLog=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Microsoft-Windows-AppLocker/EXE and DLL');try{$nativeLog | Select-Object IsEnabled,LogType,ProviderLevel,ProviderKeywords,LogIsolation | ConvertTo-Json | Write-Host}finally{$nativeLog.Dispose()}}catch{Write-Host ('Channel metadata read: '+$_.Exception.Message)}
|
||||
Write-Host ((Get-WelaAppLockerPolicySnapshot Effective) | ConvertTo-Json -Depth 12)
|
||||
}
|
||||
finally {
|
||||
if($touched){
|
||||
try {Stop-DisposablePolicyConverter}catch{$cleanup+=$_.Exception.Message}
|
||||
try {Set-AppLockerPolicy -XmlPolicy $backup -ErrorAction Stop;Refresh-DisposableComputerPolicy;if($converterChanged -or -not $converterDisabled){Run-DisposablePolicyConverter};$restored=Get-WelaAppLockerPolicySnapshot Local;if($restored.Status -ne 'Observed' -or (Get-WelaAppLockerXmlKey $restored.Policy.Xml) -cne (Get-WelaAppLockerXmlKey $before.LocalPolicy.Policy.Xml)){throw 'Local policy restoration differs'};$effectiveRestored=Get-WelaAppLockerPolicySnapshot Effective;if($effectiveRestored.Status -ne 'Observed' -or (Get-WelaAppLockerXmlKey $effectiveRestored.Policy.Xml) -cne (Get-WelaAppLockerXmlKey $before.EffectiveGpPolicy.Policy.Xml)){throw 'Effective GP policy restoration differs'}}catch{$cleanup+=$_.Exception.Message}
|
||||
try {Stop-DisposablePolicyConverter;if($converterChanged){$null=Disable-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop};$taskAfter=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop;if($taskAfter.State -ne $(if($converterDisabled){'Disabled'}else{'Ready'}) -or (Export-ScheduledTask -InputObject $taskAfter -ErrorAction Stop) -cne $converterBefore){throw 'Native PolicyConverter task definition was not restored'}}catch{$cleanup+=$_.Exception.Message}
|
||||
try {$log.IsEnabled=$enabled;$log.SaveChanges();$verify=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new($log.LogName);try{if($verify.IsEnabled -ne $enabled){throw 'Channel restoration differs'}}finally{$verify.Dispose()}}catch{$cleanup+=$_.Exception.Message}
|
||||
if($before.Service.State -ne 'Running') {try {Stop-Service AppIDSvc -ErrorAction Stop}catch{Write-Host 'Protected AppIDSvc could not stop; startup mode was untouched. The disposable hosted VM is discarded after this job.'}}
|
||||
$afterService=Get-WelaAppLockerService;if($afterService.StartMode -ne $before.Service.StartMode){$cleanup+='AppIDSvc startup mode changed'}
|
||||
}
|
||||
$log.Dispose()
|
||||
}
|
||||
if($cleanup.Count){throw ('Native cleanup failed: '+($cleanup -join '; '))}
|
||||
if($primary){throw $primary}
|
||||
Write-Host 'Original local policy and channel state restored; service startup mode preserved.'
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -9,6 +9,8 @@ $placeholderNodes = @('Exe','Dll','Msi','Script','Appx') | ForEach-Object { '<Ru
|
||||
$placeholders = '<AppLockerPolicy Version="1">' + ($placeholderNodes -join '') + '</AppLockerPolicy>'
|
||||
$unusedPlaceholders = '<AppLockerPolicy Version="1">' + (($placeholderNodes | Select-Object -Skip 1) -join '') + '</AppLockerPolicy>'
|
||||
$readbackPlaceholders = $xml.Replace('</AppLockerPolicy>', (($placeholderNodes | Select-Object -Skip 1) -join '') + '</AppLockerPolicy>')
|
||||
$emptyPolicy=ConvertFrom-WelaAppLockerXml -Xml '<AppLockerPolicy Version="1" />'
|
||||
Assert ($emptyPolicy.TotalRules -is [int] -and $emptyPolicy.TotalRules -eq 0) 'An understood empty policy reports zero rules, not an unknown null count.'
|
||||
$desired=ConvertFrom-WelaAppLockerXml -Xml $xml -ForImport
|
||||
Assert ($desired.TotalRules -eq 1 -and -not $desired.HasEnforcement) 'Audit-only rule must parse.'
|
||||
Assert ((Get-WelaAppLockerXmlKey $xml) -ceq (Get-WelaAppLockerXmlKey ($xml.Replace('Type="Exe" EnforcementMode="AuditOnly"', 'EnforcementMode="AuditOnly" Type="Exe"')))) 'Attribute ordering cannot change compliance.'
|
||||
|
||||
@@ -25,6 +25,10 @@ try {
|
||||
$fixture=New-WelaEvtxFixture (Join-Path $temp 'source')
|
||||
$source=Import-WelaEvtxProbe $fixture.Directory
|
||||
Assert ($source.Event.Computer -eq 'source01.lab.test' -and $source.Files.Count -eq 5) 'Completed native-shaped source bundle validates with all hashes'
|
||||
$fractional=New-WelaEvtxFixture (Join-Path $temp 'fractional-zeroes') -Timestamp ([datetime]::SpecifyKind([datetime]'2025-01-02T03:04:05.1234500',[DateTimeKind]::Utc))
|
||||
$fractionalSource=Import-WelaEvtxProbe $fractional.Directory
|
||||
Assert ($fractionalSource.Manifest.BeforeState.capturedAtUtc -is [string] -and $fractionalSource.Manifest.BeforeState.capturedAtUtc.EndsWith('.1234500Z')) 'Fixture retains fractional timestamp zeroes as strings in embedded metadata'
|
||||
Assert ($fractionalSource.Files.Count -eq 5) 'Deterministic fractional-zero fixture passes the unchanged strict bundle importer'
|
||||
foreach($case in @('hash','extra','duplicate-json','bad-status','embedded','typed','missing-mask','source-drift','time','process-command','unknown-field','bad-kind','duplicate-artifact')) {
|
||||
$dir=Join-Path $temp $case;Copy-Item $fixture.Directory $dir -Recurse
|
||||
$m=Clone $fixture.Manifest
|
||||
|
||||
@@ -25,6 +25,10 @@ try {
|
||||
$fixture=New-WelaArrivalFixture (Join-Path $temp 'source')
|
||||
$source=Import-WelaArrivalProbe $fixture.Directory
|
||||
Assert ($source.Event.Computer -eq 'source01.lab.test' -and $source.Files.Count -eq 5) 'Completed native-shaped source bundle validates with all hashes'
|
||||
$fractional=New-WelaArrivalFixture (Join-Path $temp 'fractional-zeroes') -Timestamp ([datetime]::SpecifyKind([datetime]'2025-01-02T03:04:05.1234500',[DateTimeKind]::Utc))
|
||||
$fractionalSource=Import-WelaArrivalProbe $fractional.Directory
|
||||
Assert ($fractionalSource.Manifest.BeforeState.capturedAtUtc -is [string] -and $fractionalSource.Manifest.BeforeState.capturedAtUtc.EndsWith('.1234500Z')) 'Fixture retains fractional timestamp zeroes as strings in embedded metadata'
|
||||
Assert ($fractionalSource.Files.Count -eq 5) 'Deterministic fractional-zero fixture passes the unchanged strict bundle importer'
|
||||
foreach($case in @('hash','extra','duplicate-json','bad-status','embedded','typed','missing-mask','source-drift','time','process-command','unknown-field','bad-kind','duplicate-artifact')) {
|
||||
$dir=Join-Path $temp $case;Copy-Item $fixture.Directory $dir -Recurse
|
||||
$m=Clone $fixture.Manifest
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
# Synthetic source/collector data only. No native event generation or telemetry claim.
|
||||
function New-WelaEvtxFixture {
|
||||
param([string]$Directory)
|
||||
$now=[DateTime]::UtcNow.AddSeconds(-5)
|
||||
param([string]$Directory,[datetime]$Timestamp=([DateTime]::UtcNow.AddSeconds(-5)))
|
||||
$now=$Timestamp
|
||||
$hostState=[pscustomobject][ordered]@{Status='Observed';Build=20348;UBR=4000;Edition='ServerDatacenter';ProductType=3;DomainRole=3;DomainJoined=$true;Domain='lab.test';Architecture='64-bit';ProcessorArchitecture=9;InstalledRoles=@('Web-Server');RolesStatus='Observed';Diagnostic=''}
|
||||
$policies=@{};foreach ($p in (Import-WelaAuditProfiles).catalog) {$policies[$p.guid]=0};$policies['0cce922b-69ae-11d9-bed3-505054503030']=1
|
||||
$state=[pscustomobject][ordered]@{capturedAtUtc=$now.AddSeconds(-2).ToString('o');context=[pscustomobject]@{computer='source01';role='MemberServer';build=20348;patch='20348.4000';domainJoined=$true;installedRoles=@('Web-Server')};hostObservation=$hostState;auditPolicies=$policies;auditPrecedence=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Value=1;Type='DWord'};commandLineCapture=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Value=1;Type='DWord'};securityChannelEnabled=$true}
|
||||
@@ -13,7 +13,7 @@ function New-WelaEvtxFixture {
|
||||
"@
|
||||
$null=New-Item -ItemType Directory -Path $Directory
|
||||
$artifacts=@();foreach ($entry in @(@('before-state.json',$before),@('after-state.json',$after),@('process.json',($process|ConvertTo-Json -Depth 6)),@('event.xml',$xml))) {$artifacts+=Write-WelaProbeArtifact $Directory $entry[0] $entry[1]}
|
||||
$manifest=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaNativeProbeComponents';Probe='security-4688-command-line-v1';Action='Run';Status='NativeEventObserved';ExitCode=0;GeneratedUtc=$now.AddSeconds(-3).ToString('o');PolicyChanges=0;ReadyRuleCredit=0;Scope='Synthetic test fixture';RequiredEvidence=@('Reviewed complete rule and normalization','Backend ingestion','Translated query and successful query result');BeforeState=(ConvertFrom-Json $before);AfterState=(ConvertFrom-Json $after);Process=$process;Artifacts=$artifacts;Diagnostic='';OutputPath=$Directory}
|
||||
$manifest=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaNativeProbeComponents';Probe='security-4688-command-line-v1';Action='Run';Status='NativeEventObserved';ExitCode=0;GeneratedUtc=$now.AddSeconds(-3).ToString('o');PolicyChanges=0;ReadyRuleCredit=0;Scope='Synthetic test fixture';RequiredEvidence=@('Reviewed complete rule and normalization','Backend ingestion','Translated query and successful query result');BeforeState=(ConvertFrom-WelaEvtxJson $before);AfterState=(ConvertFrom-WelaEvtxJson $after);Process=$process;Artifacts=$artifacts;Diagnostic='';OutputPath=$Directory}
|
||||
$null=Write-WelaProbeArtifact $Directory 'manifest.json' ($manifest|ConvertTo-Json -Depth 20)
|
||||
[pscustomobject]@{Directory=$Directory;Xml=$xml;Host=$hostState;Process=$process;Manifest=$manifest}
|
||||
}
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
# Synthetic source/collector data only. No native event generation or telemetry claim.
|
||||
function New-WelaArrivalFixture {
|
||||
param([string]$Directory)
|
||||
$now=[DateTime]::UtcNow.AddSeconds(-5)
|
||||
param([string]$Directory,[datetime]$Timestamp=([DateTime]::UtcNow.AddSeconds(-5)))
|
||||
$now=$Timestamp
|
||||
$hostState=[pscustomobject][ordered]@{Status='Observed';Build=20348;UBR=4000;Edition='ServerDatacenter';ProductType=3;DomainRole=3;DomainJoined=$true;Domain='lab.test';Architecture='64-bit';ProcessorArchitecture=9;InstalledRoles=@('Web-Server');RolesStatus='Observed';Diagnostic=''}
|
||||
$policies=@{};foreach ($p in (Import-WelaAuditProfiles).catalog) {$policies[$p.guid]=0};$policies['0cce922b-69ae-11d9-bed3-505054503030']=1
|
||||
$state=[pscustomobject][ordered]@{capturedAtUtc=$now.AddSeconds(-2).ToString('o');context=[pscustomobject]@{computer='source01';role='MemberServer';build=20348;patch='20348.4000';domainJoined=$true;installedRoles=@('Web-Server')};hostObservation=$hostState;auditPolicies=$policies;auditPrecedence=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Value=1;Type='DWord'};commandLineCapture=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Value=1;Type='DWord'};securityChannelEnabled=$true}
|
||||
@@ -13,7 +13,7 @@ function New-WelaArrivalFixture {
|
||||
"@
|
||||
$null=New-Item -ItemType Directory -Path $Directory
|
||||
$artifacts=@();foreach ($entry in @(@('before-state.json',$before),@('after-state.json',$after),@('process.json',($process|ConvertTo-Json -Depth 6)),@('event.xml',$xml))) {$artifacts+=Write-WelaProbeArtifact $Directory $entry[0] $entry[1]}
|
||||
$manifest=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaNativeProbeComponents';Probe='security-4688-command-line-v1';Action='Run';Status='NativeEventObserved';ExitCode=0;GeneratedUtc=$now.AddSeconds(-3).ToString('o');PolicyChanges=0;ReadyRuleCredit=0;Scope='Synthetic test fixture';RequiredEvidence=@('Reviewed complete rule and normalization','Backend ingestion','Translated query and successful query result');BeforeState=(ConvertFrom-Json $before);AfterState=(ConvertFrom-Json $after);Process=$process;Artifacts=$artifacts;Diagnostic='';OutputPath=$Directory}
|
||||
$manifest=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaNativeProbeComponents';Probe='security-4688-command-line-v1';Action='Run';Status='NativeEventObserved';ExitCode=0;GeneratedUtc=$now.AddSeconds(-3).ToString('o');PolicyChanges=0;ReadyRuleCredit=0;Scope='Synthetic test fixture';RequiredEvidence=@('Reviewed complete rule and normalization','Backend ingestion','Translated query and successful query result');BeforeState=(ConvertFrom-WelaArrivalJson $before);AfterState=(ConvertFrom-WelaArrivalJson $after);Process=$process;Artifacts=$artifacts;Diagnostic='';OutputPath=$Directory}
|
||||
$null=Write-WelaProbeArtifact $Directory 'manifest.json' ($manifest|ConvertTo-Json -Depth 20)
|
||||
[pscustomobject]@{Directory=$Directory;Xml=$xml;Host=$hostState;Process=$process;Manifest=$manifest}
|
||||
}
|
||||
Reference in new issue
Block a user