mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-03 13:04:43 +02:00
102 lines
10 KiB
PowerShell
102 lines
10 KiB
PowerShell
param([switch]$AllowDisposableWarningWrite)
|
|
$ErrorActionPreference='Stop'
|
|
if(-not $AllowDisposableWarningWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit opt-in on a disposable GitHub-hosted Windows runner is required.'}
|
|
$repo=Split-Path $PSScriptRoot -Parent
|
|
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
|
|
Import-Module (Join-Path $repo 'modules/NativeProviders.psm1') -Force
|
|
. (Join-Path $repo 'scripts/Configuration.ps1')
|
|
$engine=(Get-Process -Id $PID).Path
|
|
$root=Join-Path $env:RUNNER_TEMP ('wela-security-warning-'+[guid]::NewGuid().ToString('N'))
|
|
$null=New-Item -ItemType Directory -Path $root
|
|
$path='HKLM:\SYSTEM\CurrentControlSet\Services\Eventlog\Security';$name='WarningLevel'
|
|
$count=0;$failure=$null;$cleanupErrors=@()
|
|
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
|
function Save($Name,$Value){ConvertTo-Json -InputObject $Value -Depth 25|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8}
|
|
function Key($Value){ConvertTo-Json -InputObject $Value -Depth 25 -Compress}
|
|
function Masks($Value){@($Value.Keys|Sort-Object|ForEach-Object{"$_=$($Value[$_])"}) -join ';'}
|
|
function Warning {Get-WelaRegistryState $path $name}
|
|
function Unselected {
|
|
# Own the native registry view and read its descriptor without Get-Acl LiteralPath provider conversion.
|
|
$baseKey=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64)
|
|
$key=$null
|
|
try{
|
|
$key=$baseKey.OpenSubKey('SYSTEM\CurrentControlSet\Services\Eventlog\Security')
|
|
if(-not $key){throw 'Existing Security registry key is unavailable.'}
|
|
$values=@(foreach($n in @($key.GetValueNames()|Sort-Object)){
|
|
if($n -ine $name){[pscustomobject][ordered]@{Name=$n;Type=[string]$key.GetValueKind($n);Value=$key.GetValue($n,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}}
|
|
})
|
|
$subkeys=@($key.GetSubKeyNames()|Sort-Object)
|
|
$security=if($PSVersionTable.PSVersion.Major -ge 6){[Microsoft.Win32.RegistryAclExtensions]::GetAccessControl($key)}else{$key.GetAccessControl()}
|
|
$acl=$security.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access -bor [Security.AccessControl.AccessControlSections]::Owner -bor [Security.AccessControl.AccessControlSections]::Group)
|
|
}finally{if($key){$key.Dispose()};$baseKey.Dispose()}
|
|
[pscustomobject][ordered]@{OtherSecurityValues=$values;SecuritySubkeys=$subkeys;SecurityAcl=$acl;SecurityChannel=Get-WelaNativeChannel Security;ApplicationChannel=Get-WelaNativeChannel Application;OneSettings=Get-WelaRegistryState 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' EnableOneSettingsAuditing;CrashOnAuditFail=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' CrashOnAuditFail;EventLogService=[string](Get-Service EventLog).Status}
|
|
}
|
|
function Public([string]$Label,[string[]]$Arguments,[int]$Expected=0){
|
|
$prior=$ErrorActionPreference
|
|
try{$ErrorActionPreference='Continue';$output=& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') audit-notifications @Arguments 2>&1|Out-String;$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior}
|
|
$output|Set-Content -LiteralPath (Join-Path $root ($Label+'.txt')) -Encoding UTF8
|
|
Assert ($code -eq $Expected) "Public $Label exited $code, expected $Expected : $output"
|
|
}
|
|
$before=Warning;$unselected=Unselected;$masks=Get-WelaEffectiveAuditPolicy
|
|
Assert $before.KeyExists 'Existing Security registry key is required; fixture never creates/removes it.'
|
|
Save 'original.json' @{Warning=$before;Unselected=$unselected;Masks=$masks;Engine=$PSVersionTable.PSVersion.ToString();OS=[Environment]::OSVersion.VersionString}
|
|
$base=@('-NotificationControl','SecurityWarning')
|
|
try{
|
|
# Remove only the selected value to exercise absence rather than a fabricated default.
|
|
if((Warning).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop}
|
|
$seed=Warning
|
|
Public 'plan' ($base+@('-NotificationAction','Plan','-WarningPercent','90','-ResultsPath',(Join-Path $root 'plan.json')))
|
|
$plan=Get-Content (Join-Path $root 'plan.json') -Raw|ConvertFrom-Json
|
|
Assert ($plan.Plan.Count -eq 1 -and $plan.Plan[0].Definition.Id -ceq 'SecurityWarning' -and $plan.Plan[0].Desired -eq 90 -and -not $plan.Plan[0].Before.Policy.ValueExists) 'Public Plan retains actual absence and exactly one selected control.'
|
|
$dryBackup=Join-Path $root 'dry-backup'
|
|
Public 'dry' ($base+@('-NotificationAction','Configure','-WarningPercent','90','-Auto','-DryRun','-BackupPath',$dryBackup,'-ResultsPath',(Join-Path $root 'dry.json')))
|
|
$dry=Get-Content (Join-Path $root 'dry.json') -Raw|ConvertFrom-Json
|
|
Assert ($dry.ExitCode -eq 0 -and $dry.DryRun -and $dry.Results[0].Status -ceq 'Skipped' -and -not(Test-Path $dryBackup)) 'Public dry run reports a skipped proposal and creates no journal.'
|
|
Assert ((Key (Warning)) -ceq (Key $seed) -and (Key (Unselected)) -ceq (Key $unselected) -and (Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $masks)) 'Plan/DryRun preserve the selected absence, unrelated native state and all59 masks.'
|
|
$cases=@(
|
|
@{Id='absent';Before=$null;Maximum=90;Desired=90;Status='Applied'},
|
|
@{Id='zero';Before=0;Maximum=80;Desired=80;Status='Applied'},
|
|
@{Id='higher';Before=95;Maximum=70;Desired=70;Status='Applied'},
|
|
@{Id='earlier';Before=25;Maximum=90;Desired=25;Status='AlreadyCompliant'}
|
|
)
|
|
foreach($case in $cases){
|
|
if($null -ne $case.Before){$null=New-ItemProperty -LiteralPath $path -Name $name -Value $case.Before -PropertyType DWord -Force}
|
|
$prior=Warning;$backup=Join-Path $root ($case.Id+'-backup');$results=Join-Path $root ($case.Id+'.json')
|
|
Public $case.Id ($base+@('-NotificationAction','Configure','-WarningPercent',[string]$case.Maximum,'-Auto','-BackupPath',$backup,'-ResultsPath',$results))
|
|
$report=Get-Content $results -Raw|ConvertFrom-Json;$after=Warning
|
|
Assert ($report.ExitCode -eq 0 -and $report.Scope -ceq 'audit-notifications' -and $report.Results.Count -eq 1 -and $report.Results[0].Status -ceq $case.Status) 'Each selected native case has one accurate result and narrow scope.'
|
|
Assert ($after.Type -ceq 'DWord' -and $after.Value -eq $case.Desired -and $report.Current[0].Before.Policy.Value -eq $case.Desired) 'Native DWORD readback and public current state match the exact intended threshold.'
|
|
Assert ($report.PrivacyChannelPlan.Count -eq 0 -and $report.EventGeneration -match 'Not verified') 'No privacy-channel operation or warning event claim is implied.'
|
|
$journalPath=Join-Path $backup 'before.jsonl'
|
|
if($case.Status -eq 'Applied'){
|
|
$journal=@(Get-Content $journalPath|ConvertFrom-Json)
|
|
Assert ($journal.Count -eq 1 -and $journal[0].Target.Name -ceq $name -and $journal[0].Target.Path -ceq $path -and (Key $journal[0].Before.Policy) -ceq (Key $prior)) 'The one native change has exact typed original journal evidence.'
|
|
Assert ((Key $report.Results[0].Before.Policy) -ceq (Key $prior) -and (Key $report.Results[0].After.Policy) -ceq (Key $after)) 'Applied result binds exact native before and after policy.'
|
|
}else{Assert (-not(Test-Path $journalPath)) 'An earlier existing warning is preserved without a write journal.'}
|
|
Assert ((Key (Unselected)) -ceq (Key $unselected) -and (Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $masks)) 'Each public Configure preserves siblings, ACL, channels, service, audit masks, OneSettings and CrashOnAuditFail.'
|
|
}
|
|
$repeatPath=Join-Path $root 'repeat.json';$repeatBackup=Join-Path $root 'repeat-backup'
|
|
Public 'repeat' ($base+@('-NotificationAction','Configure','-WarningPercent','90','-Auto','-BackupPath',$repeatBackup,'-ResultsPath',$repeatPath))
|
|
$repeat=Get-Content $repeatPath -Raw|ConvertFrom-Json
|
|
Assert ($repeat.Results[0].Status -ceq 'AlreadyCompliant' -and (Warning).Value -eq 25 -and -not(Test-Path (Join-Path $repeatBackup 'before.jsonl'))) 'Repeated Configure is idempotent and preserves the earlier threshold.'
|
|
# Fixture-owned wrong type must remain wrong rather than being coerced and overwritten.
|
|
Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop
|
|
$null=New-ItemProperty -LiteralPath $path -Name $name -Value 'fixture-not-a-dword' -PropertyType String
|
|
$invalid=Warning;$invalidPath=Join-Path $root 'invalid.json';$invalidBackup=Join-Path $root 'invalid-backup'
|
|
Public 'invalid' ($base+@('-NotificationAction','Configure','-WarningPercent','90','-Auto','-BackupPath',$invalidBackup,'-ResultsPath',$invalidPath)) 1
|
|
$refused=Get-Content $invalidPath -Raw|ConvertFrom-Json
|
|
Assert ($refused.ExitCode -eq 1 -and $refused.Results[0].Status -ceq 'Failed' -and (Key (Warning)) -ceq (Key $invalid) -and -not(Test-Path (Join-Path $invalidBackup 'before.jsonl'))) 'Actual wrong type yields failure and is preserved without a native write journal.'
|
|
Save 'completed.json' @{Status='Passed';Assertions=$count;Scope='Actual named policy configuration only. No warning generation, log exhaustion, retention changes, GPO refresh, ingestion or Sigma proof.'}
|
|
}catch{$failure=$_.ToString();throw}finally{
|
|
try{
|
|
if((Warning).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop}
|
|
if($before.ValueExists){$null=New-ItemProperty -LiteralPath $path -Name $name -Value $before.Value -PropertyType $before.Type}
|
|
}catch{$cleanupErrors+=$_.ToString()}
|
|
$warningOk=$false;$otherOk=$false;$masksOk=$false
|
|
try{$warningOk=(Key (Warning)) -ceq (Key $before);$otherOk=(Key (Unselected)) -ceq (Key $unselected);$masksOk=(Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $masks)}catch{$cleanupErrors+=$_.ToString()}
|
|
Save 'cleanup.json' @{Failure=$failure;Errors=$cleanupErrors;WarningRestored=$warningOk;UnselectedPreserved=$otherOk;All59MasksPreserved=$masksOk;Complete=($warningOk -and $otherOk -and $masksOk -and -not $cleanupErrors.Count)}
|
|
if(-not $warningOk -or -not $otherOk -or -not $masksOk -or $cleanupErrors.Count){throw 'Native warning fixture cleanup failed; inspect retained evidence.'}
|
|
}
|
|
Write-Host "PASS: $count native public Security warning assertions and exact cleanup."
|
|
exit 0
|