mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-09-29 18:44:31 +02:00
170 lines
11 KiB
PowerShell
170 lines
11 KiB
PowerShell
. (Join-Path $PSScriptRoot '../scripts/TargetedSaclPlanning.ps1')
|
|
$script:SaclMode = 'Skip'
|
|
# Profile command + verified configuration integration. No Windows policy is touched.
|
|
$ErrorActionPreference = 'Stop'
|
|
# Keep mocks in the same script scope as dot-sourced helpers/imported commands;
|
|
# Windows PowerShell 5.1 resolves script-local originals ahead of global mocks.
|
|
$repo = Split-Path $PSScriptRoot -Parent
|
|
$script:ScriptRoot = $repo
|
|
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
|
|
. (Join-Path $repo 'scripts/Configuration.ps1')
|
|
$tokens = $null; $errors = $null
|
|
$ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors)
|
|
if ($errors.Count) { throw ($errors | Out-String) }
|
|
foreach ($name in @('Get-WelaSelectedContext', 'Show-WelaAuditProfilePrerequisites', 'Invoke-WelaProfileCommand', 'ConfigureAuditSettings')) {
|
|
$function = $ast.Find({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name }, $true)
|
|
. ([scriptblock]::Create($function.Extent.Text))
|
|
}
|
|
$script:assertions = 0
|
|
$script:cleanup = New-Object 'System.Collections.Generic.List[string]'
|
|
$data = Import-WelaAuditProfiles
|
|
$zero = @{}
|
|
foreach ($policy in $data.catalog) { $zero[$policy.guid] = 0 }
|
|
$shareGuid = ($data.catalog | Where-Object id -eq 'Detailed File Share').guid
|
|
$processGuid = ($data.catalog | Where-Object id -eq 'Process Creation').guid
|
|
$privilegeGuid = ($data.catalog | Where-Object id -eq 'Sensitive Privilege Use').guid
|
|
function Assert($Condition, [string]$Message) {
|
|
if (-not $Condition) { throw "FAIL: $Message" }
|
|
$script:assertions++
|
|
}
|
|
function Assert-Throws([scriptblock]$Action, [string]$Pattern) {
|
|
$caught = ''
|
|
try { & $Action | Out-Null } catch { $caught = $_.ToString() }
|
|
Assert ($caught -match $Pattern) "Expected failure matching '$Pattern', got '$caught'"
|
|
}
|
|
function Reset-Run([string]$Profile = 'cis-win11-v4-l1', [switch]$DryRun) {
|
|
$script:state = $zero.Clone()
|
|
$script:writes = @()
|
|
$script:precedenceValue = 0
|
|
$script:failGuid = ''
|
|
$script:concurrentGuid = ''
|
|
$script:Profile = $Profile
|
|
$script:Role = 'Client'; $script:Build = 26100
|
|
$script:hostBuild = 26100
|
|
$script:Baseline = $null; $script:IncludeOptional = $false
|
|
$script:Auto = $true; $script:DryRun = [bool]$DryRun
|
|
$script:BackupPath = Join-Path ([IO.Path]::GetTempPath()) ('wela-profile-integration-' + [guid]::NewGuid().ToString('N'))
|
|
$script:ResultsPath = $script:BackupPath + '-results.json'
|
|
$script:PlanPath = $null
|
|
$script:cleanup.Add($script:BackupPath)
|
|
$script:cleanup.Add($script:ResultsPath)
|
|
}
|
|
function Get-WelaRegistryState {
|
|
param($Path, $Name)
|
|
if ($Name -ne 'SCENoApplyLegacyAuditPolicy') { throw 'Unexpected registry read' }
|
|
[pscustomobject]@{ KeyExists = $true; ValueExists = $true; Value = $script:precedenceValue; Type = 'DWord' }
|
|
}
|
|
function New-WelaRegistryKey { param($Path) }
|
|
function Set-ItemProperty {
|
|
param($LiteralPath, $Name, $Value, $Type, $ErrorAction)
|
|
if ($Name -ne 'SCENoApplyLegacyAuditPolicy') { throw 'Unexpected registry write' }
|
|
$script:precedenceValue = $Value
|
|
}
|
|
function Get-CimInstance { param($Namespace, $ClassName, $ErrorAction) throw 'No RSoP fixture' }
|
|
function TestWindows { return $true }
|
|
function TestAdministrator { return $true }
|
|
function Get-WelaHostContext { [pscustomobject]@{ Role = 'Client'; Build = $script:hostBuild } }
|
|
function Get-WelaEffectiveAuditPolicy { return $script:state.Clone() }
|
|
function Get-WelaNativeAuditPolicy {
|
|
param($Guid)
|
|
if (-not $script:state.ContainsKey($Guid)) { throw "Mock missing policy: $Guid" }
|
|
return $script:state[$Guid]
|
|
}
|
|
function Invoke-WelaNative {
|
|
param($FilePath, $Arguments)
|
|
if ($FilePath -ne 'auditpol.exe' -or $Arguments[0] -ne '/set') { throw 'Unexpected native mutation' }
|
|
$guid = ($Arguments | Where-Object { $_ -like '/subcategory:*' }) -replace '^/subcategory:\{([^}]+)\}$', '$1'
|
|
$journal = Join-Path $script:BackupPath 'before.jsonl'
|
|
if (-not (Test-Path -LiteralPath $journal)) { throw 'Audit mutation occurred before journal' }
|
|
$entries = @(Get-Content -LiteralPath $journal | ConvertFrom-Json)
|
|
if ($entries[-1].Target.Guid -ne $guid) { throw 'Audit mutation occurred before matching journal entry' }
|
|
if ($guid -eq $script:failGuid) { throw 'Mock auditpol write failure' }
|
|
# Simulate a concurrent writer enabling Failure after WELA observed the policy.
|
|
if ($guid -eq $script:concurrentGuid) { $script:state[$guid] = $script:state[$guid] -bor 2 }
|
|
$mask = $script:state[$guid]
|
|
foreach ($arg in $Arguments) {
|
|
switch ($arg) {
|
|
'/success:enable' { $mask = $mask -bor 1 }
|
|
'/success:disable' { $mask = $mask -band 2 }
|
|
'/failure:enable' { $mask = $mask -bor 2 }
|
|
'/failure:disable' { $mask = $mask -band 1 }
|
|
}
|
|
}
|
|
$script:state[$guid] = $mask
|
|
$script:writes += [pscustomobject]@{ Guid = $guid; Arguments = $Arguments }
|
|
[pscustomobject]@{ ExitCode = 0; Diagnostic = ''; Output = @() }
|
|
}
|
|
try {
|
|
Reset-Run -DryRun
|
|
Invoke-WelaProfileCommand configure | Out-Null
|
|
$report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json
|
|
Assert ($script:writes.Count -eq 0 -and $report.DryRun) 'configure -Profile -DryRun makes no audit writes'
|
|
Assert ($report.Scope -eq 'advanced-audit-policy-and-precedence' -and $report.ProfileScope -eq 'advanced-audit-policy-only') 'Profile-only results declare their narrower scope'
|
|
Assert (-not (Test-Path -LiteralPath $script:BackupPath)) 'Profile dry run creates no journal directory'
|
|
Assert ($report.Results.Count -gt 0 -and @($report.Results | Where-Object Status -ne Skipped).Count -eq 0) 'Profile dry-run proposals remain explicit skipped results'
|
|
|
|
Reset-Run
|
|
$script:state[$shareGuid] = 1
|
|
$script:concurrentGuid = $processGuid
|
|
Invoke-WelaProfileCommand configure | Out-Null
|
|
$report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json
|
|
Assert ($script:state[$shareGuid] -eq 3) 'Minimum Failure preserves existing Success'
|
|
Assert ($script:state[$processGuid] -eq 3) 'Minimum Success preserves concurrently added Failure'
|
|
$processWrite = $script:writes | Where-Object Guid -eq $processGuid
|
|
Assert ($processWrite.Arguments -contains '/success:enable' -and @($processWrite.Arguments | Where-Object { $_ -like '*:disable' }).Count -eq 0) 'Minimum native command only enables required bits'
|
|
Assert ($report.ExitCode -eq 0) 'Minimum supersets pass final compliance verification'
|
|
Assert ($report.Profile -eq 'cis-win11-v4-l1' -and $report.Role -eq 'Client' -and $report.Build -eq 26100) 'Final report retains profile role and build'
|
|
Assert ($report.Version -and $report.SchemaSha256.Length -eq 64 -and $report.Provenance.Count -gt 0) 'Final report retains version and provenance'
|
|
$row = $report.Results | Where-Object { $_.Target.Guid -eq $shareGuid }
|
|
Assert ($row.Mode -eq 'minimum' -and $row.Evidence -and $row.SourceIds.Count -gt 0) 'Per-control mode and source evidence survive the context adapter'
|
|
$journal = @(Get-Content -LiteralPath (Join-Path $script:BackupPath 'before.jsonl') | ConvertFrom-Json)
|
|
Assert (@($journal | Where-Object { $_.Target.Guid -eq $shareGuid -and $_.Before -eq 1 -and $_.Desired.Mask -eq 2 -and $_.Desired.Mode -eq 'minimum' }).Count -eq 1) 'Minimum policy journal preserves before state and requirement semantics'
|
|
|
|
Reset-Run 'microsoft-sct-win11-24h2'
|
|
$script:state[$privilegeGuid] = 3
|
|
Invoke-WelaProfileCommand configure | Out-Null
|
|
Assert ($script:state[$privilegeGuid] -eq 1) 'Exact SCT mask remains exact rather than hardcoded SF'
|
|
$privilegeWrite = $script:writes | Where-Object Guid -eq $privilegeGuid
|
|
Assert ($privilegeWrite.Arguments -contains '/failure:disable') 'Exact Success deliberately clears unrequested Failure'
|
|
|
|
Reset-Run 'wela-2.2.0'
|
|
$script:IncludeOptional = $true
|
|
Invoke-WelaProfileCommand configure | Out-Null
|
|
$report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json
|
|
$kernel = $report.Results | Where-Object Id -eq 'AuditPolicy/Kernel Object'
|
|
Assert ($kernel.Prerequisites -match 'SACL' -and $kernel.Evidence -and $kernel.SourceIds.Count -gt 0) 'Added native controls retain dependency and source evidence after apply'
|
|
|
|
Reset-Run
|
|
$script:failGuid = $processGuid
|
|
Assert-Throws { Invoke-WelaProfileCommand configure } 'advanced audit policies failed'
|
|
$report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json
|
|
Assert ($report.ExitCode -eq 1 -and $report.Failed -eq 1) 'Profile native failure reaches final machine-readable result'
|
|
Assert ($script:writes.Count -gt 0) 'Other policy controls continue after one failure'
|
|
|
|
Reset-Run 'windows-defaults-reviewed-2026-09'
|
|
Assert-Throws { Invoke-WelaProfileCommand configure } 'reference'
|
|
Assert ($script:writes.Count -eq 0 -and -not (Test-Path -LiteralPath $script:BackupPath)) 'Reference-only defaults fail before journal creation or mutation'
|
|
|
|
Reset-Run
|
|
$script:state.Remove($processGuid)
|
|
Assert-Throws { Invoke-WelaProfileCommand configure } 'unknown current'
|
|
Assert ($script:writes.Count -eq 0 -and -not (Test-Path -LiteralPath $script:BackupPath)) 'Missing required state refuses the entire profile before mutation'
|
|
|
|
Reset-Run
|
|
$script:hostBuild = 19045
|
|
Assert-Throws { ConfigureAuditSettings -Auto -BackupPath $script:BackupPath } 'does not support'
|
|
Assert ($script:writes.Count -eq 0 -and -not (Test-Path -LiteralPath $script:BackupPath)) 'Legacy configure rejects unsupported hosts before any control or journal'
|
|
$referencePlan = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role Client -Build 26100 -Current $zero
|
|
$emptyContext = New-WelaConfigurationContext -DryRun
|
|
$broaderReport = Complete-WelaConfiguration -Context $emptyContext -Plan $referencePlan
|
|
Assert ($broaderReport.Scope -eq 'native-windows-configuration' -and $broaderReport.ProfileScope -eq 'advanced-audit-policy-only') 'Broad configure scope is not mislabeled as its audit-policy profile scope'
|
|
$engine = (Get-Process -Id $PID).Path
|
|
$helpOutput = & $engine -NoProfile -File (Join-Path $repo 'WELA.ps1') configure -Profile cis-win11-v4-l1 -Help 2>&1
|
|
Assert ($LASTEXITCODE -eq 0 -and ($helpOutput -join ' ') -match 'Usage:.*-Profile') 'Profile configure help returns usage without entering the Windows mutation path'
|
|
Write-Host "PASS: $script:assertions profile configuration integration assertions (mocked; no Windows changes)."
|
|
} finally {
|
|
foreach ($path in $script:cleanup) {
|
|
if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force }
|
|
}
|
|
}
|