mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-02 20:44:42 +02:00
199 lines
15 KiB
PowerShell
199 lines
15 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
$repo = Split-Path $PSScriptRoot -Parent
|
|
$script:ScriptRoot = $repo
|
|
. (Join-Path $repo 'scripts/Configuration.ps1')
|
|
. (Join-Path $repo 'scripts/SmbAuditing.ps1')
|
|
$script:assertions = 0; $script:cleanup = @()
|
|
$root = Join-Path ([IO.Path]::GetTempPath()) ('wela-smb-' + [guid]::NewGuid().ToString('N'))
|
|
$script:cleanup += $root
|
|
$null = New-Item -ItemType Directory -Path (Join-Path $root 'PolicyDefinitions') -Force
|
|
$savedWindir = $env:windir; $savedOS = $env:OS
|
|
$env:windir = $root
|
|
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
|
|
function Write-Admx([string]$Omit = '', [string]$WrongType = '') {
|
|
foreach ($component in @('LanmanServer', 'LanmanWorkstation')) {
|
|
$policies = @()
|
|
foreach ($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $component)) {
|
|
if ($definition.Name -eq $Omit) { continue }
|
|
$enabled = if ($definition.Name -eq $WrongType) { '<string>1</string>' } else { '<decimal value="1"/>' }
|
|
$policies += '<policy name="' + $definition.PolicyName + '" class="Machine" key="' + ($definition.Path -replace '^HKLM:\\', '') + '" valueName="' + $definition.Name + '"><supportedOn ref="windows:SUPPORTED_Windows_11_0"/><enabledValue>' + $enabled + '</enabledValue></policy>'
|
|
}
|
|
'<policyDefinitions xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions"><policies>' + ($policies -join '') + '</policies></policyDefinitions>' | Set-Content -LiteralPath (Join-Path (Join-Path $root 'PolicyDefinitions') "$component.admx") -Encoding UTF8
|
|
}
|
|
}
|
|
function Reset-Mocks {
|
|
$script:build = 26100; $script:productType = 1
|
|
$script:hostFails = $false; $script:registryFails = $false; $script:runtimeFails = $false; $script:writeFails = $false
|
|
$script:runtimeMissing = $false; $script:runtimeFollows = $true; $script:wrongTypeWrite = $false
|
|
$script:writes = 0; $script:keysCreated = 0; $script:registry = @{}; $script:runtime = @{}; $script:onPrompt = $null
|
|
foreach ($definition in Get-WelaSmbAuditDefinitions) {
|
|
$id = "$($definition.Component)/$($definition.Name)"
|
|
$script:registry[$id] = [pscustomobject]@{ KeyExists = $false; ValueExists = $false; Value = $null; Type = $null }
|
|
$script:runtime[$id] = $false
|
|
}
|
|
Write-Admx
|
|
}
|
|
function Get-CimInstance {
|
|
param($ClassName, $Property, $ErrorAction)
|
|
if ($script:hostFails) { throw 'OS query denied' }
|
|
[pscustomobject]@{ ProductType = $script:productType; BuildNumber = [string]$script:build; Version = "10.0.$script:build"; Caption = 'Mock Windows' }
|
|
}
|
|
function Get-WelaRegistryState {
|
|
param($Path, $Name)
|
|
if ($script:registryFails) { throw 'Policy read denied' }
|
|
$component = ($Path -split '\\')[-1]
|
|
$source = $script:registry["$component/$Name"]
|
|
if (-not $source) { throw "Unexpected policy path $Path/$Name" }
|
|
[pscustomobject]@{ KeyExists = $source.KeyExists; ValueExists = $source.ValueExists; Value = $source.Value; Type = $source.Type }
|
|
}
|
|
function New-WelaRegistryKey { param($Path) $script:keysCreated++ }
|
|
function Set-ItemProperty {
|
|
param($LiteralPath, $Name, $Value, $Type, $ErrorAction)
|
|
$definition = @(Get-WelaSmbAuditDefinitions | Where-Object { $_.Path -eq $LiteralPath -and $_.Name -eq $Name })
|
|
Assert ($definition.Count -eq 1 -and $Value -eq 1 -and $Type -eq 'DWord') 'Only six exact audit DWORD paths can be written'
|
|
$entries = @(Get-Content -LiteralPath (Join-Path $script:context.BackupPath 'before.jsonl') | ConvertFrom-Json)
|
|
Assert ($entries[-1].Target.Name -eq $Name -and $entries[-1].Target.Path -eq $LiteralPath) 'Matching recovery evidence precedes the write'
|
|
Assert ($entries[-1].Before.Capability.AdmxSha256 -and $entries[-1].Before.Policy) 'Journal retains registry state and ADMX evidence'
|
|
if ($script:writeFails) { throw 'Policy write denied' }
|
|
$script:writes++
|
|
$id = "$($definition[0].Component)/$Name"
|
|
$script:registry[$id] = [pscustomobject]@{ KeyExists = $true; ValueExists = $true; Value = 1; Type = $(if ($script:wrongTypeWrite) { 'String' } else { 'DWord' }) }
|
|
if ($script:runtimeFollows) { $script:runtime[$id] = $true }
|
|
}
|
|
function Get-Runtime($Component) {
|
|
if ($script:runtimeFails) { throw 'Runtime query denied' }
|
|
$values = @{ RequireSecuritySignature = $true; EnableInsecureGuestLogons = $false }
|
|
if (-not $script:runtimeMissing) {
|
|
foreach ($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $Component)) { $values[$definition.Name] = $script:runtime["$Component/$($definition.Name)"] }
|
|
}
|
|
[pscustomobject]$values
|
|
}
|
|
function Get-SmbClientConfiguration { param($ErrorAction) Get-Runtime 'LanmanWorkstation' }
|
|
function Get-SmbServerConfiguration { param($ErrorAction) Get-Runtime 'LanmanServer' }
|
|
function Read-Host { param($Prompt) if ($script:onPrompt) { & $script:onPrompt }; 'Y' }
|
|
function New-TestContext([switch]$DryRun, [switch]$Prompt) {
|
|
$path = Join-Path $root ([guid]::NewGuid().ToString('N'))
|
|
$script:context = New-WelaConfigurationContext -DryRun:$DryRun -Auto:(-not $Prompt) -BackupPath $path
|
|
return $script:context
|
|
}
|
|
try {
|
|
Reset-Mocks
|
|
foreach ($case in @(@(26100, 1), @(26200, 1), @(26100, 2), @(26100, 3))) {
|
|
$script:build = $case[0]; $script:productType = $case[1]
|
|
$plan = @(Get-WelaSmbAuditPlan)
|
|
Assert ($plan.Count -eq 6 -and @($plan | Where-Object Status -eq ChangeRequired).Count -eq 6) 'Reviewed client/server/DC builds require the six policies when exact ADMX exists'
|
|
}
|
|
Reset-Mocks
|
|
$script:build = 20348; $script:productType = 3
|
|
$plan = @(Get-WelaSmbAuditPlan)
|
|
Assert (@($plan | Where-Object Status -eq NotApplicable).Count -eq 6) 'Server 2022 stays unsupported even with copied newer ADMX'
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context $plan
|
|
Assert ($script:writes -eq 0 -and $script:keysCreated -eq 0 -and @($context.Results | Where-Object Status -eq Skipped).Count -eq 6) 'Unsupported host gets explicit skips and no created policy keys'
|
|
$script:build = 30000
|
|
Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 6) 'Unreviewed future builds are Unknown'
|
|
|
|
Reset-Mocks
|
|
Write-Admx -Omit AuditInsecureGuestLogon
|
|
Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 2) 'Missing local ADMX controls are individually gated'
|
|
Write-Admx -WrongType AuditInsecureGuestLogon
|
|
Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 2) 'An ADMX string value 1 does not authorize a DWORD policy write'
|
|
Remove-Item -LiteralPath (Join-Path $root 'PolicyDefinitions/LanmanServer.admx')
|
|
Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 4) 'Missing ADMX file blocks its component while preserving other controls'
|
|
'<!DOCTYPE policyDefinitions [<!ENTITY external SYSTEM "file:///not-readable">]><policyDefinitions>&external;</policyDefinitions>' | Set-Content -LiteralPath (Join-Path $root 'PolicyDefinitions/LanmanServer.admx')
|
|
Assert (@(Get-WelaSmbAuditPlan | Where-Object { $_.Definition.Component -eq 'LanmanServer' -and $_.Status -eq 'Unknown' }).Count -eq 3) 'ADMX external entities are rejected'
|
|
|
|
Reset-Mocks
|
|
$plan = @(Get-WelaSmbAuditPlan)
|
|
$context = New-TestContext -DryRun
|
|
Set-WelaSmbAuditControls $context $plan
|
|
Assert ($script:writes -eq 0 -and $script:keysCreated -eq 0 -and -not (Test-Path $context.BackupPath)) 'Dry run creates no registry policy or recovery directory'
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context $plan
|
|
Assert ($script:writes -eq 6 -and (Complete-WelaConfiguration $context -Scope smb-audit-policies-only).ExitCode -eq 0) 'All six exact policies are applied and runtime observations confirm them'
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan)
|
|
Assert ($script:writes -eq 6 -and @($context.Results | Where-Object Status -eq AlreadyCompliant).Count -eq 6) 'Confirmed policy/runtime state is idempotent'
|
|
$script:registry['LanmanServer/AuditInsecureGuestLogon'].Value = 0
|
|
Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1 -and $context.Results[2].Status -eq 'Overridden') 'Final policy refresh drift changes status and exit code'
|
|
|
|
Reset-Mocks
|
|
$script:runtimeFollows = $false
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan)
|
|
$result = Complete-WelaConfiguration $context
|
|
Assert ($script:writes -eq 6 -and $result.Failed -eq 0 -and $result.ExitCode -eq 0) 'Correct policy writes succeed independently of synchronous runtime activation'
|
|
Assert ($context.Results[0].After.Policy.Value -eq 1 -and $context.Results[0].After.Runtime.Value -eq $false) 'Successful policy read-back retains the actual false runtime observation'
|
|
Assert ($context.Results[0].After.PolicyRegistryConfigured -and $context.Results[0].After.RuntimeState -eq 'PendingVerification') 'Policy configuration and pending runtime verification are distinct'
|
|
Assert ($context.Results[0].After.Runtime.Diagnostic -match 'cause and activation timing are unknown') 'False runtime state does not assume a refresh will resolve it'
|
|
$plan = @(Get-WelaSmbAuditPlan)
|
|
Assert (@($plan | Where-Object Status -eq PolicyConfigured).Count -eq 6) 'Audit/plan reports configured policy despite pending runtime'
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context $plan
|
|
Assert ($script:writes -eq 6 -and @($context.Results | Where-Object Status -eq AlreadyCompliant).Count -eq 6) 'Pending runtime alone never causes redundant DWORD writes'
|
|
$env:OS = 'Windows_NT'
|
|
$pendingJson = Join-Path $root 'pending-runtime.json'
|
|
$pendingReport = Invoke-WelaSmbAuditCommand -Action Configure -Auto -BackupPath (Join-Path $root 'pending-runtime-backup') -ResultsPath $pendingJson
|
|
$savedPending = Get-Content $pendingJson -Raw | ConvertFrom-Json
|
|
Assert ($pendingReport.ExitCode -eq 0 -and $savedPending.RuntimeVerification.PendingVerification -eq 6 -and $savedPending.RuntimeVerification.Active -eq 0) 'Public JSON explicitly summarizes pending runtime without a policy failure'
|
|
Assert ($savedPending.VerificationScope -match 'Policy registry.*runtime activation.*separate') 'Public result success is explicitly scoped to the policy registry'
|
|
foreach ($id in @($script:runtime.Keys)) { $script:runtime[$id] = $true }
|
|
$later = @(Get-WelaSmbAuditPlan)
|
|
Assert (@($later | Where-Object { $_.Status -eq 'PolicyConfigured' -and $_.Before.RuntimeState -eq 'Active' }).Count -eq 6) 'A later independent audit observes runtime activation without rewriting policy'
|
|
$result = Complete-WelaConfiguration $context
|
|
Assert ($result.ExitCode -eq 0 -and $script:writes -eq 6 -and @($result.Results | Where-Object { $_.After.RuntimeState -eq 'Active' }).Count -eq 6) 'Final recheck records later activation independently of write success'
|
|
$script:runtime['LanmanServer/AuditClientDoesNotSupportEncryption'] = $false
|
|
$result = Complete-WelaConfiguration $context
|
|
Assert ($result.ExitCode -eq 0 -and $result.Results[0].After.RuntimeState -eq 'PendingVerification') 'Runtime returning false remains visible without relabeling an unchanged policy as overridden'
|
|
$script:runtimeFails = $true
|
|
Assert ((Complete-WelaConfiguration $context).Failed -eq 6) 'An actual runtime read error still fails the final verification instead of being treated as pending False'
|
|
$unknownReport = Invoke-WelaSmbAuditCommand -Action Configure -Auto -BackupPath (Join-Path $root 'unknown-runtime-backup')
|
|
Assert ($unknownReport.ExitCode -eq 1 -and $unknownReport.RuntimeVerification.Unknown -eq 6 -and $unknownReport.RuntimeVerification.Active -eq 0) 'Failed observations summarize as unknown and never reuse an older active runtime snapshot'
|
|
|
|
Reset-Mocks
|
|
$script:runtimeMissing = $true
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan)
|
|
Assert ((Complete-WelaConfiguration $context).ExitCode -eq 0) 'Exact ADMX permits registry-only configuration when runtime property is absent'
|
|
Assert ($context.Results[0].After.Runtime.Status -eq 'NotExposed' -and $context.Results[0].After.VerificationScope -like '*effective auditing not established*') 'Registry-only outcome never claims runtime confirmation'
|
|
Assert ($context.Results[0].After.RuntimeState -eq 'Unknown') 'An unavailable runtime property is Unknown rather than Active or pending False'
|
|
|
|
Reset-Mocks
|
|
$script:wrongTypeWrite = $true
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan)
|
|
Assert ((Complete-WelaConfiguration $context).Failed -eq 6) 'Read-back rejects wrong registry type despite a numeric match'
|
|
foreach ($errorKind in @('hostFails', 'registryFails', 'runtimeFails')) {
|
|
Reset-Mocks
|
|
Set-Variable -Name $errorKind -Scope Script -Value $true
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan)
|
|
Assert ($script:writes -eq 0 -and (Complete-WelaConfiguration $context).Failed -eq 6) "$errorKind becomes an explicit failure without writes"
|
|
}
|
|
Reset-Mocks
|
|
$script:runtime['LanmanServer/AuditInsecureGuestLogon'] = 'False'
|
|
Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 1) 'String False is not coerced into a true runtime Boolean'
|
|
Reset-Mocks
|
|
$script:writeFails = $true
|
|
$context = New-TestContext
|
|
Set-WelaSmbAuditControls $context @((Get-WelaSmbAuditPlan)[0])
|
|
Assert ((Complete-WelaConfiguration $context).Failed -eq 1) 'Write errors propagate to results'
|
|
Reset-Mocks
|
|
$script:onPrompt = { $script:registry['LanmanServer/AuditClientDoesNotSupportEncryption'].Value = 42 }
|
|
$context = New-TestContext -Prompt
|
|
Set-WelaSmbAuditControls $context @((Get-WelaSmbAuditPlan)[0])
|
|
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Policy changes during confirmation are not overwritten with stale recovery data'
|
|
|
|
Reset-Mocks
|
|
$env:OS = 'Windows_NT'
|
|
$json = Join-Path $root 'plan.json'
|
|
$report = Invoke-WelaSmbAuditCommand -Action Plan -ResultsPath $json
|
|
Assert ($report.ExitCode -eq 0 -and (Get-Content $json -Raw | ConvertFrom-Json).Controls.Count -eq 6) 'Public plan writes six complete controls to JSON'
|
|
$report = Invoke-WelaSmbAuditCommand -Action Configure -DryRun -BackupPath (Join-Path $root 'dry')
|
|
Assert ($report.DryRun -and $script:writes -eq 0 -and $report.Scope -eq 'smb-audit-policies-only') 'Public entrypoint keeps SMB scope and dry-run semantics'
|
|
Write-Host "PASS: $script:assertions SMB audit assertions (mocked policies/runtime; real temporary ADMX parsing)."
|
|
} finally {
|
|
$env:windir = $savedWindir; $env:OS = $savedOS
|
|
foreach ($path in $script:cleanup) { if (Test-Path $path) { Remove-Item -LiteralPath $path -Recurse -Force } }
|
|
}
|