mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-05 14:04:47 +02:00
Integrate verified configuration results for review
# Conflicts: # WELA.ps1
This commit is contained in:
commit
f5a19a45fd
7 files changed
+761
-352
No files matched your search
@@ -0,0 +1,25 @@
|
||||
name: Configuration result regressions
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
configuration-results:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Mocked regressions in Windows PowerShell 5.1
|
||||
shell: powershell
|
||||
run: ./tests/Test-ConfigurationResults.ps1
|
||||
- name: Read-only Windows smoke in Windows PowerShell 5.1
|
||||
shell: powershell
|
||||
run: ./tests/Test-ConfigurationReadOnlyWindows.ps1
|
||||
- name: Mocked regressions in PowerShell 7
|
||||
shell: pwsh
|
||||
run: ./tests/Test-ConfigurationResults.ps1
|
||||
- name: Read-only Windows smoke in PowerShell 7
|
||||
shell: pwsh
|
||||
run: ./tests/Test-ConfigurationReadOnlyWindows.ps1
|
||||
@@ -38,6 +38,7 @@ jobs:
|
||||
mkdir -p release-binaries
|
||||
Copy-Item -Path WELA.ps1 -Destination release-binaries/
|
||||
Copy-Item -Recurse -Path ./config -Destination release-binaries/
|
||||
Copy-Item -Recurse -Path ./scripts -Destination release-binaries/
|
||||
|
||||
- name: Set Artifact Name
|
||||
if: contains(matrix.info.os, 'windows') == true
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
[switch]$Auto,
|
||||
[ValidateSet("PreserveOrAudit", "Audit", "Deny")]
|
||||
[string]$OutgoingNtlmMode = "PreserveOrAudit",
|
||||
[switch]$DryRun,
|
||||
[string]$BackupPath,
|
||||
[string]$ResultsPath,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
@@ -19,6 +22,7 @@ $SecurityRulesPath = Join-Path $ScriptRoot "config/security_rules.json"
|
||||
$EidMappingPath = Join-Path $ScriptRoot "config/eid_subcategory_mapping.csv"
|
||||
$AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt"
|
||||
$SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
|
||||
. (Join-Path $ScriptRoot "scripts/Configuration.ps1")
|
||||
|
||||
# 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。
|
||||
$PowerShellPolicyRoots = @(
|
||||
@@ -1036,7 +1040,11 @@ function Get-WelaDomainNtlmState {
|
||||
|
||||
function Set-WelaDomainNtlmAudit {
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param ([switch]$Auto)
|
||||
param ([switch]$Auto, $Context)
|
||||
if ($Context) {
|
||||
Set-WelaNtlmConfigurationControl -Context $Context -Scope Domain -WhatIf:$WhatIfPreference
|
||||
return
|
||||
}
|
||||
$state = Get-WelaDomainNtlmState
|
||||
Write-Host "Domain NTLM auditing: $($state.Description)"
|
||||
if (-not $state.Applicable) {
|
||||
@@ -1084,10 +1092,23 @@ function Set-RegistryConfig {
|
||||
[array]$RegPaths,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$Auto
|
||||
[switch]$Auto,
|
||||
$Context
|
||||
)
|
||||
|
||||
foreach ($reg in $RegPaths) {
|
||||
if ($Context) {
|
||||
if ($PSCmdlet.ShouldProcess("$($reg.Path)\$($reg.Name)", "Set to $($reg.Value)")) {
|
||||
Set-WelaRegistryControl -Context $Context -Path $reg.Path -Name $reg.Name -Value $reg.Value
|
||||
} else {
|
||||
$Context.Results.Add([pscustomobject]@{
|
||||
Id = "Registry/$($reg.Path)/$($reg.Name)"; Kind = 'Registry'
|
||||
Target = @{ Path = $reg.Path; Name = $reg.Name }; Desired = $reg.Value
|
||||
Before = $null; After = $null; Status = 'Skipped'; Diagnostic = 'ShouldProcess declined the change.'
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
try {
|
||||
$currentValue = "Not Set"
|
||||
$pathExists = Test-Path $reg.Path
|
||||
@@ -1190,8 +1211,13 @@ function Set-WelaOutgoingNtlmPolicy {
|
||||
param (
|
||||
[ValidateSet('PreserveOrAudit', 'Audit', 'Deny')]
|
||||
[string]$Mode = 'PreserveOrAudit',
|
||||
[switch]$Auto
|
||||
[switch]$Auto,
|
||||
$Context
|
||||
)
|
||||
if ($Context) {
|
||||
Set-WelaNtlmConfigurationControl -Context $Context -Scope Outgoing -Mode $Mode -WhatIf:$WhatIfPreference
|
||||
return
|
||||
}
|
||||
$path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0'
|
||||
$name = 'RestrictSendingNTLMTraffic'
|
||||
$state = Get-WelaOutgoingNtlmState
|
||||
@@ -1245,70 +1271,21 @@ function Set-WelaOutgoingNtlmPolicy {
|
||||
|
||||
function ConfigureAuditSettings {
|
||||
param (
|
||||
[switch] $Auto,
|
||||
[switch]$Auto, [switch]$Debug, [switch]$DryRun, [string]$BackupPath, [string]$ResultsPath,
|
||||
[ValidateSet("PreserveOrAudit", "Audit", "Deny")]
|
||||
[string] $OutgoingNtlmMode = "PreserveOrAudit",
|
||||
[switch] $Debug
|
||||
[string]$OutgoingNtlmMode = "PreserveOrAudit"
|
||||
)
|
||||
|
||||
if (-not (TestWindows)) {
|
||||
Write-Host "[ERROR] 'configure' changes Windows settings and can only run on Windows." -ForegroundColor Red
|
||||
return
|
||||
if (-not (TestWindows)) { throw "'configure' can only run on Windows." }
|
||||
if (-not (TestAdministrator)) { throw 'This script requires Administrator privileges.' }
|
||||
# Never use the debug cache to decide whether mutating controls are compliant.
|
||||
if ($Debug) { Write-Host 'configure always reads live state; the auditpol debug cache is not used.' -ForegroundColor Yellow }
|
||||
$context = New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
|
||||
if (-not $DryRun) { Write-Host "Recovery journal: $($context.BackupPath)" }
|
||||
|
||||
foreach ($log in @('Security', 'Microsoft-Windows-PowerShell/Operational', 'Windows PowerShell')) {
|
||||
Set-WelaEventLogControl -Context $context -Log $log -Property MaximumSizeInBytes -Desired 1073741824
|
||||
}
|
||||
|
||||
# 管理者権限の確認
|
||||
if (-not (TestAdministrator)) {
|
||||
Write-Error "This script requires Administrator privileges"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (CollectAuditpol -UseCached:$Debug)) {
|
||||
return
|
||||
}
|
||||
|
||||
# ログサイズ定数
|
||||
$oneGB = 1073741824
|
||||
$oneTwentyEightMB = 134217728
|
||||
|
||||
# セキュリティおよびPowerShellログを1GBに設定
|
||||
Write-Host "Configuring Event Logs..."
|
||||
Write-Host ""
|
||||
$largeLogs = @(
|
||||
"Security",
|
||||
"Microsoft-Windows-PowerShell/Operational",
|
||||
"Windows PowerShell"
|
||||
)
|
||||
|
||||
foreach ($log in $largeLogs) {
|
||||
try {
|
||||
$logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop
|
||||
$currentSize = [math]::Floor($logInfo.MaximumSizeInBytes / 1MB)
|
||||
$newSize = 1024
|
||||
Write-Host "Log: $log"
|
||||
if ($currentSize -ge $newSize) {
|
||||
Write-Host "[SKIPPED] $log : Current size ($currentSize MB) is already greater than or equal to $newSize MB." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
continue
|
||||
}
|
||||
if ($Auto) {
|
||||
$response = "Y"
|
||||
} else {
|
||||
$response = Read-Host "Your current setting is $currentSize MB. Do you want to change it to 1024 MB? (Y/n)"
|
||||
}
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
wevtutil sl $log /ms:$oneGB 2>&1 | Out-Null
|
||||
Write-Host "[OK] $log : 1024 MB" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[SKIPPED] $log" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[ERROR] $log : $_" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# その他の重要なログを128MBに設定
|
||||
$mediumLogs = @(
|
||||
"System",
|
||||
"Application",
|
||||
@@ -1335,202 +1312,37 @@ function ConfigureAuditSettings {
|
||||
)
|
||||
|
||||
foreach ($log in $mediumLogs) {
|
||||
try {
|
||||
$logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop
|
||||
$currentSize = [math]::Floor($logInfo.MaximumSizeInBytes / 1MB)
|
||||
$newSize = 128
|
||||
Write-Host "Log: $log"
|
||||
if ($currentSize -ge $newSize) {
|
||||
Write-Host "[SKIPPED] $log : Current size ($currentSize MB) is already greater than or equal to $newSize MB." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
continue
|
||||
}
|
||||
if ($Auto) {
|
||||
$response = "Y"
|
||||
} else {
|
||||
$response = Read-Host "Your current setting is $currentSize MB. Do you want to change it to 128 MB? (Y/n)"
|
||||
}
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
wevtutil sl $log /ms:$oneTwentyEightMB 2>&1 | Out-Null
|
||||
Write-Host "[OK] $log : 128 MB" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[SKIPPED] $log" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[ERROR] $log : $_" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
Set-WelaEventLogControl -Context $context -Log $log -Property MaximumSizeInBytes -Desired 134217728
|
||||
}
|
||||
foreach ($log in @('Microsoft-Windows-TaskScheduler/Operational', 'Microsoft-Windows-DriverFrameworks-UserMode/Operational', 'Microsoft-Windows-Crypto-DPAPI/Debug')) {
|
||||
Set-WelaEventLogControl -Context $context -Log $log -Property IsEnabled -Desired $true
|
||||
}
|
||||
|
||||
# 特定のログの有効化
|
||||
Write-Host "Enabling Event Logs..."
|
||||
Write-Host ""
|
||||
foreach ($log in @("Microsoft-Windows-TaskScheduler/Operational", "Microsoft-Windows-DriverFrameworks-UserMode/Operational", "Microsoft-Windows-Crypto-DPAPI/Debug")) {
|
||||
try {
|
||||
$logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop
|
||||
$currentState = if ($logInfo.IsEnabled) { "Enabled" } else { "Disabled" }
|
||||
$newState = "Enabled"
|
||||
Write-Host "Log: $log"
|
||||
if ($currentState -eq $newState) {
|
||||
Write-Host "[SKIPPED] $log : Already Enabled." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
continue
|
||||
}
|
||||
if ($Auto) {
|
||||
$response = "Y"
|
||||
} else {
|
||||
$response = Read-Host "Your current setting is $currentState. Do you want to change it to Enabled? (Y/n)"
|
||||
}
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
wevtutil sl $log /e:true 2>&1 | Out-Null
|
||||
Write-Host "[OK] Enabled: $log" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[SKIPPED] $log" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[ERROR] Failed to enable $log : $_" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# PowerShell ロギングの設定
|
||||
Write-Host "Configuring PowerShell Logging..."
|
||||
Write-Host ""
|
||||
# 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。
|
||||
# 32bit の PowerShell 用に Wow6432Node 側も併せて設定する。
|
||||
$regPaths = @()
|
||||
foreach ($root in $script:PowerShellPolicyRoots) {
|
||||
$regPaths += @{Path = "$root\ModuleLogging"; Name = "EnableModuleLogging"; Value = 1}
|
||||
$regPaths += @{Path = "$root\ScriptBlockLogging"; Name = "EnableScriptBlockLogging"; Value = 1}
|
||||
$regPaths += @{Path = "$root\ModuleLogging"; Name = 'EnableModuleLogging'; Value = 1}
|
||||
$regPaths += @{Path = "$root\ScriptBlockLogging"; Name = 'EnableScriptBlockLogging'; Value = 1}
|
||||
}
|
||||
Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto
|
||||
|
||||
# モジュール名レジストリの設定
|
||||
Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto -Context $context
|
||||
foreach ($root in $script:PowerShellPolicyRoots) {
|
||||
try {
|
||||
$moduleLoggingPath = "$root\ModuleLogging\ModuleNames"
|
||||
$currentValue = "Not Set"
|
||||
$pathExists = Test-Path $moduleLoggingPath
|
||||
if ($pathExists) {
|
||||
$prop = Get-ItemProperty -Path $moduleLoggingPath -Name "*" -ErrorAction SilentlyContinue
|
||||
if ($prop) {
|
||||
$currentValue = $prop."*"
|
||||
}
|
||||
}
|
||||
Write-Host "Registry: $moduleLoggingPath"
|
||||
if ($currentValue -eq "*") {
|
||||
Write-Host "[SKIPPED] Module logging : Already set to * (all modules)." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
} else
|
||||
{
|
||||
if ($Auto)
|
||||
{
|
||||
$response = "Y"
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = Read-Host "Your current setting is $currentValue. Do you want to change it to * (all modules)? (Y/n)"
|
||||
}
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y")
|
||||
{
|
||||
if (-not $pathExists)
|
||||
{
|
||||
New-Item -Path $moduleLoggingPath -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $moduleLoggingPath -Name "*" -Value "*" -Type String
|
||||
Write-Host "[OK] Module logging enabled for all modules" -ForegroundColor Green
|
||||
}
|
||||
else
|
||||
{
|
||||
Write-Host "[SKIPPED] Module logging" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[ERROR] Failed to configure module names: $_" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
Set-WelaRegistryControl -Context $context -Path "$root\ModuleLogging\ModuleNames" -Name '*' -Value '*' -Type String
|
||||
}
|
||||
Set-WelaRegistryControl -Context $context -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit' `
|
||||
-Name ProcessCreationIncludeCmdLine_Enabled -Value 1
|
||||
|
||||
# コマンドライン監査の有効化
|
||||
Write-Host "Enabling Command Line Auditing..."
|
||||
Write-Host ""
|
||||
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit"
|
||||
$valueName = "ProcessCreationIncludeCmdLine_Enabled"
|
||||
try {
|
||||
$currentValue = "Not Set"
|
||||
if (Test-Path $regPath) {
|
||||
$prop = Get-ItemProperty -Path $regPath -Name $valueName -ErrorAction SilentlyContinue
|
||||
$currentValue = $prop.$valueName
|
||||
}
|
||||
Write-Host "Registry: $regPath"
|
||||
if ($currentValue -eq 1) {
|
||||
Write-Host "[SKIPPED] Command Line Auditing : Already Enabled." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
} else
|
||||
{
|
||||
if ($Auto)
|
||||
{
|
||||
$response = "Y"
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = Read-Host "Your current setting is $currentValue. Do you want to change it to 1 (Enabled)? (Y/n)"
|
||||
}
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y")
|
||||
{
|
||||
$regPath = $regPath -replace "HKLM:", "HKLM"
|
||||
$arguments = "add $regPath /v $valueName /f /t REG_DWORD /d 1"
|
||||
$process = Start-Process -FilePath "reg.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL"
|
||||
if ($process.ExitCode -eq 0)
|
||||
{
|
||||
Write-Host "[OK] Command line auditing enabled" -ForegroundColor Green
|
||||
}
|
||||
else
|
||||
{
|
||||
Write-Host "[ERROR] Command line auditing failed (ExitCode: $( $process.ExitCode ))" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Write-Host "[SKIPPED] Command line auditing" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[ERROR] Failed to check command line auditing: $_" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Outgoing restriction and audit-only modes must be selected independently.
|
||||
Set-WelaOutgoingNtlmPolicy -Mode $OutgoingNtlmMode -Auto:$Auto
|
||||
|
||||
# NTLM認証の監査設定
|
||||
Write-Host "Configuring NTLM Audit Settings..."
|
||||
Write-Host ""
|
||||
# NTLM audit/restriction decisions share the recovery and verification context.
|
||||
Set-WelaOutgoingNtlmPolicy -Mode $OutgoingNtlmMode -Auto:$Auto -Context $context
|
||||
$regPaths = @(
|
||||
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0"; Name = "AuditReceivingNTLMTraffic"; Value = 2}
|
||||
)
|
||||
Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto
|
||||
|
||||
Set-WelaDomainNtlmAudit -Auto:$Auto
|
||||
|
||||
# LDAP query logging (Directory Service EventID 1644) - domain controllers only.
|
||||
# "15 Field Engineering" = 5 makes expensive / inefficient LDAP searches log as 1644, which surfaces
|
||||
# BloodHound / SharpHound-style directory reconnaissance. Only applied where the NTDS role is present.
|
||||
if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters") {
|
||||
Write-Host "Configuring LDAP query logging (1644) on this domain controller..."
|
||||
Write-Host ""
|
||||
Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto -Context $context
|
||||
Set-WelaDomainNtlmAudit -Auto:$Auto -Context $context
|
||||
if (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters') {
|
||||
Set-RegistryConfig -RegPaths @(
|
||||
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics"; Name = "15 Field Engineering"; Value = 5}
|
||||
) -Auto:$Auto
|
||||
@{Path = 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics'; Name = '15 Field Engineering'; Value = 5}
|
||||
) -Auto:$Auto -Context $context
|
||||
}
|
||||
|
||||
# 監査ポリシーの設定
|
||||
Write-Host "Configuring Audit Policies..."
|
||||
Write-Host ""
|
||||
$auditPolicies = @(
|
||||
@{Category = "Account Logon"; Name = "Credential Validation"; GUID = "0CCE923F-69AE-11D9-BED3-505054503030"},
|
||||
@{Category = "Account Logon"; Name = "Kerberos Authentication Service"; GUID = "0CCE9242-69AE-11D9-BED3-505054503030"},
|
||||
@@ -1568,112 +1380,11 @@ function ConfigureAuditSettings {
|
||||
@{Category = "System"; Name = "Other System Events"; GUID = "0CCE9214-69AE-11D9-BED3-505054503030"}
|
||||
)
|
||||
|
||||
$currentAuditPol = GetAuditpol
|
||||
|
||||
foreach ($policy in $auditPolicies)
|
||||
{
|
||||
$newSetting = "Success and Failure"
|
||||
$currentSetting = if ($currentAuditPol.ContainsKey($policy.GUID))
|
||||
{
|
||||
$currentAuditPol[$policy.GUID]
|
||||
}
|
||||
else
|
||||
{
|
||||
"Unknown"
|
||||
}
|
||||
|
||||
Write-Host "Audit Policy: $( $policy.Category ) - $( $policy.Name )"
|
||||
if ($currentSetting -eq $newSetting)
|
||||
{
|
||||
Write-Host "[SKIPPED] $( $policy.Category ) - $( $policy.Name ) : Already set to $newSetting." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
continue
|
||||
}
|
||||
if ($Auto) {
|
||||
$response = "Y"
|
||||
} else {
|
||||
$response = Read-Host "Your current setting is $currentSetting. Do you want to change it to $newSetting? (Y/n)"
|
||||
}
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
$arguments = "/set /subcategory:{$($policy.GUID)} /success:enable /failure:enable"
|
||||
$process = Start-Process -FilePath "auditpol.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL"
|
||||
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Write-Host "[OK] $($policy.Category) - $($policy.Name)" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host "[ERROR] $($policy.Category) - $($policy.Name) (ExitCode: $($process.ExitCode))" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host "[SKIPPED] $($policy.Category) - $($policy.Name)" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
foreach ($policy in $auditPolicies) {
|
||||
Set-WelaAuditPolicyControl -Context $context -Policy $policy
|
||||
}
|
||||
|
||||
# AD CS AuditFilter の設定
|
||||
Write-Host "Configuring AD CS Audit Settings..."
|
||||
try {
|
||||
$installed = (Get-WindowsFeature -Name AD-Certificate).InstallState -eq "Installed"
|
||||
} catch {
|
||||
$installed = $false
|
||||
}
|
||||
|
||||
if ($installed) {
|
||||
try {
|
||||
$csRootKey = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\"
|
||||
$caName = (Get-ItemProperty $csRootKey -ErrorAction Stop).Active
|
||||
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\$caName"
|
||||
$prop = Get-ItemProperty -Path $regPath -Name "AuditFilter" -ErrorAction SilentlyContinue
|
||||
$currentValue = if ($null -ne $prop) { [int]$prop.AuditFilter } else { "Not Set" }
|
||||
if ($currentValue -eq 127) {
|
||||
Write-Host "[OK] AuditFilter is already 127" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
$proceed = $false
|
||||
if ($Auto) {
|
||||
$proceed = $true
|
||||
}
|
||||
else {
|
||||
$response = Read-Host "Do you want to set AuditFilter to 127 and restart Certificate Services? (Y/n)"
|
||||
$proceed = ($response -eq "" -or $response -match "^[Yy]$")
|
||||
}
|
||||
|
||||
if ($proceed) {
|
||||
try {
|
||||
# AuditFilter の設定
|
||||
& certutil.exe -setreg "CA\AuditFilter" 127 >$null 2>&1
|
||||
# 証明書サービスの再起動
|
||||
Restart-Service -Name "CertSvc" -Force -ErrorAction Stop
|
||||
# 反映確認
|
||||
$propAfter = Get-ItemProperty -Path $regPath -Name "AuditFilter" -ErrorAction SilentlyContinue
|
||||
$newValue = if ($null -ne $propAfter) { [int]$propAfter.AuditFilter } else { $null }
|
||||
|
||||
if ($newValue -eq 127) {
|
||||
Write-Host "[OK] AuditFilter set to 127 and CertSvc restarted" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host "[ERROR] AuditFilter did not apply as expected (current: $newValue)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[ERROR] Failed to set AuditFilter or restart CertSvc: $_" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[SKIP] No changes applied to AuditFilter"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "[ERROR] Failed to process AD CS audit settings: $_" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[INFO] AD Certificate Services is not installed. Skipping." -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Configuration completed successfully" -ForegroundColor Green
|
||||
Set-WelaCertificateAuditControl -Context $context
|
||||
Complete-WelaConfiguration -Context $context -ResultsPath $ResultsPath
|
||||
}
|
||||
|
||||
$logo = @"
|
||||
@@ -1992,11 +1703,14 @@ switch ($Cmd.ToLower()) {
|
||||
if ($Help){
|
||||
Write-Host "Configure Windows Event Log audit settings based on the YamatoSecurity baseline"
|
||||
Write-Host ""
|
||||
Write-Host "Usage: ./WELA.ps1 configure [-Auto] [-OutgoingNtlmMode <PreserveOrAudit|Audit|Deny>]"
|
||||
Write-Host "Usage: ./WELA.ps1 configure [-Auto] [-DryRun] [-BackupPath <new-directory>] [-ResultsPath <json-file>] [-OutgoingNtlmMode <PreserveOrAudit|Audit|Deny>]"
|
||||
Write-Host ""
|
||||
Write-Host "Options:"
|
||||
Write-Host " -Auto Automatically configure without prompts"
|
||||
Write-Host " -OutgoingNtlmMode PreserveOrAudit (default): audit, preserving existing deny; Audit: explicitly replace deny; Deny: opt into enforcement"
|
||||
Write-Host " -DryRun Read live state and report proposed changes without writing Windows settings"
|
||||
Write-Host " -BackupPath New directory for the pre-change recovery journal (unique default beside WELA)"
|
||||
Write-Host " -ResultsPath Save structured per-control outcomes as JSON"
|
||||
Write-Host ""
|
||||
Write-Host "Note: only the YamatoSecurity baseline is currently supported for 'configure'."
|
||||
Write-Host ""
|
||||
@@ -2007,7 +1721,14 @@ switch ($Cmd.ToLower()) {
|
||||
Write-Host "Re-run with '-Baseline YamatoSecurity' (or omit -Baseline) if that is what you want."
|
||||
break
|
||||
}
|
||||
ConfigureAuditSettings -Auto:$Auto -Debug:$Debug -OutgoingNtlmMode $OutgoingNtlmMode
|
||||
try {
|
||||
$report = ConfigureAuditSettings -Auto:$Auto -Debug:$Debug -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath -OutgoingNtlmMode $OutgoingNtlmMode
|
||||
$report
|
||||
if ($report.ExitCode -ne 0) { exit $report.ExitCode }
|
||||
} catch {
|
||||
Write-Host "[Failed] Configuration aborted: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
"configure-sacl" {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Verified configuration and recovery
|
||||
|
||||
`configure` reads live state, records each proposed write before executing it,
|
||||
checks native exit codes, and reads the resulting state. It returns an object with
|
||||
`ExitCode`, `DryRun`, `BackupPath`, `Failed`, `Skipped`, and a `Results` array.
|
||||
`-ResultsPath` also saves that object as JSON. The command exits with status 1 when
|
||||
any control fails or changes again before the final verification. A fatal preflight
|
||||
or result-file error also exits with status 1.
|
||||
|
||||
```powershell
|
||||
# Read live settings; do not change Windows settings, restart services or create a journal.
|
||||
.\WELA.ps1 configure -DryRun -ResultsPath .\proposed-results.json
|
||||
|
||||
# Apply with interactive approval for each change, including the CA restart.
|
||||
.\WELA.ps1 configure -BackupPath C:\WELA-Recovery\run-001 -ResultsPath .\results.json
|
||||
|
||||
# Apply the existing WELA choices without individual prompts.
|
||||
.\WELA.ps1 configure -Auto -ResultsPath .\results.json
|
||||
```
|
||||
|
||||
Keep the complete WELA directory, including `scripts/Configuration.ps1`. Choose a
|
||||
recovery path whose parent directory is writable only by the operators who manage
|
||||
these settings. The backup directory must not already exist. Without `-BackupPath`,
|
||||
a unique directory is created beside WELA. `-Debug` does not substitute cached
|
||||
audit policy data during configuration. `-DryRun` may write the explicitly requested
|
||||
result file, but performs no Windows configuration writes.
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| Applied | Write succeeded and immediate read-back matched. |
|
||||
| AlreadyCompliant | The initial live value already met the requirement; no write. |
|
||||
| Skipped | Dry run, operator decline, or no configured local CA. |
|
||||
| Failed | State could not be read, journaling failed, write/restart failed, or verification failed. |
|
||||
| Overridden | A value verified earlier became noncompliant by the final read. Cause is unknown. |
|
||||
|
||||
Unknown and unavailable channels are reported as failed observations rather than
|
||||
silently claiming that logging is enabled. Partial runs and runs with skipped
|
||||
controls do not claim universal success. Verification is an observation at that
|
||||
moment; it does not prove future GPO persistence, event production, collection or
|
||||
Sigma rule coverage. A zero exit code with skipped controls is not full compliance.
|
||||
|
||||
Audit policy reads use GUIDs and numeric flags from the Windows
|
||||
[AuditQuerySystemPolicy API](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-auditquerysystempolicy).
|
||||
`auditpol /get /r` contains localized labels and no numeric setting column; it is
|
||||
not parsed as though it were `auditpol /backup` output. Registry writes use terminating errors and verify
|
||||
both the value and registry type. Log sizes retain larger existing buffers. A CA
|
||||
is detected from its configured registry state; certutil must succeed before a
|
||||
restart is attempted, and the restart must return to Running. A stopped CA is not
|
||||
started automatically. A restart failure remains failed even if the registry value
|
||||
was already written.
|
||||
|
||||
## Recovery journal and rollback design
|
||||
|
||||
Each line of `before.jsonl` records the computer, timestamp, control identity,
|
||||
requested setting and exact pre-change state. Registry entries include whether the
|
||||
key/value existed and the previous registry type. Event-log entries capture size or
|
||||
enabled state; audit policies capture the numeric mask; CA entries also capture
|
||||
service state. A journal write failure prevents that control's mutation. The
|
||||
journal is per control, not a full system backup, and can contain records for failed
|
||||
or declined downstream actions. Save the final result file alongside it.
|
||||
|
||||
This change provides a guarded **manual recovery procedure**, not an automatic
|
||||
rollback command. Automatic bulk rollback could overwrite a later administrator or
|
||||
GPO change and could interrupt certificate services. Before recovery:
|
||||
|
||||
1. Use an elevated shell on the journal's recorded computer. Review the specific
|
||||
failed or applied control and capture its current live state.
|
||||
2. Compare current state with the recorded requested/verified after-state. If it
|
||||
differs, stop and determine whether another writer made an intentional change.
|
||||
Do not blindly replay a journal or restore an entire audit policy backup.
|
||||
3. Restore only the intended controls, normally in reverse application order:
|
||||
- **EventLog:** `wevtutil sl <log> /ms:<previous-bytes>` or `/e:<previous-bool>`.
|
||||
Review shrinking buffers or disabling a channel before proceeding.
|
||||
- **AuditPolicy:** `auditpol /set /subcategory:{<guid>} /success:<enable|disable>
|
||||
/failure:<enable|disable>`. Previous mask bit 1 means success, bit 2 means
|
||||
failure. Restore that subcategory, not unrelated policy.
|
||||
- **Registry:** restore the previous value using its recorded registry type.
|
||||
If the value did not exist, remove only that value. Preserve unrelated values
|
||||
and never recursively delete a newly created parent key. Binary and multistring
|
||||
old values must be reconstructed with their original types from the JSON.
|
||||
- **CertificateService:** restore the active CA's previous AuditFilter value (or
|
||||
its original absence) and separately approve the necessary service restart.
|
||||
Do not start a CA that was deliberately stopped. A failed restart can leave
|
||||
the registry and running service out of sync; an operator must resolve this.
|
||||
4. Check every native exit code and read the restored state. Keep the recovery
|
||||
commands and observations with the original journal.
|
||||
|
||||
A future automated rollback command should require the same host and control
|
||||
identity, validate journal schema and allowlisted types, check current state against
|
||||
recorded after-state, refuse unexpected drift, journal recovery itself, and require
|
||||
explicit approval for CA restarts. It should never import the whole registry or
|
||||
force a Group Policy setting. These are design constraints, not implemented claims.
|
||||
|
||||
## Testing
|
||||
|
||||
`tests/Test-ConfigurationResults.ps1` uses mock Windows APIs and disposable temp
|
||||
journals. It exercises nonzero native exits and stderr, false-success writes,
|
||||
read-back, idempotence, final drift, dry runs, journal failure, locale-independent native audit flags, and CA write/restart failure. It does not change Windows settings.
|
||||
`tests/Test-ConfigurationReadOnlyWindows.ps1` runs real read-only Windows audit-policy API and `auditpol /get` queries
|
||||
and a child `cmd.exe` diagnostic/exit test. CI runs both scripts in Windows PowerShell
|
||||
5.1 and PowerShell 7. Mutating behavior still requires isolated Windows/CA lab
|
||||
validation; mock and read-only tests do not establish end-to-end event production.
|
||||
@@ -0,0 +1,329 @@
|
||||
# Execution helpers for configure. Compatible with Windows PowerShell 5.1.
|
||||
function Invoke-WelaNative {
|
||||
param([string]$FilePath, [string[]]$Arguments)
|
||||
# Windows PowerShell sends native stderr through the error stream. Collect it
|
||||
# without treating stderr alone as failure; the process exit code is decisive.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
$null = Get-Command $FilePath -ErrorAction Stop
|
||||
$global:LASTEXITCODE = $null
|
||||
$output = @(& $FilePath @Arguments 2>&1)
|
||||
$exitCode = $global:LASTEXITCODE # Capture immediately, before invoking anything else.
|
||||
$diagnostic = ($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine
|
||||
if ($null -eq $exitCode -or $exitCode -ne 0) {
|
||||
throw "$FilePath $($Arguments -join ' ') failed (exit: $exitCode). $diagnostic"
|
||||
}
|
||||
[pscustomobject]@{ ExitCode = $exitCode; Output = $output; Diagnostic = $diagnostic }
|
||||
}
|
||||
|
||||
function New-WelaConfigurationContext {
|
||||
param([switch]$Auto, [switch]$DryRun, [string]$BackupPath)
|
||||
if (-not $DryRun) {
|
||||
if (-not $BackupPath) {
|
||||
$BackupPath = Join-Path $script:ScriptRoot ("wela-backup-{0}-{1}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'), [guid]::NewGuid().ToString('N'))
|
||||
}
|
||||
# Refuse reuse: a prior run's recovery evidence must never be overwritten.
|
||||
$null = New-Item -ItemType Directory -Path $BackupPath -ErrorAction Stop
|
||||
$BackupPath = (Resolve-Path -LiteralPath $BackupPath -ErrorAction Stop).Path
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Auto = [bool]$Auto; DryRun = [bool]$DryRun; BackupPath = $BackupPath
|
||||
Results = New-Object 'System.Collections.Generic.List[object]'
|
||||
Checks = New-Object 'System.Collections.Generic.List[object]'
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-WelaConfigurationControl {
|
||||
param($Context, [string]$Id, [string]$Kind, $Target, $Desired,
|
||||
[scriptblock]$Read, [scriptblock]$Compliant, [scriptblock]$Apply,
|
||||
[string]$Description = '')
|
||||
$result = [pscustomobject][ordered]@{
|
||||
Id = $Id; Kind = $Kind; Target = $Target; Desired = $Desired
|
||||
Before = $null; After = $null; Status = 'Failed'; Diagnostic = ''
|
||||
}
|
||||
try {
|
||||
$result.Before = & $Read
|
||||
if (& $Compliant $result.Before) {
|
||||
$result.Status = 'AlreadyCompliant'
|
||||
$result.After = $result.Before
|
||||
} elseif ($Context.DryRun) {
|
||||
$result.Status = 'Skipped'; $result.Diagnostic = 'Dry run: change required; no write or restart performed.'
|
||||
} else {
|
||||
$proceed = $Context.Auto
|
||||
if (-not $proceed) {
|
||||
$response = Read-Host "$Id : $Description Apply this change? (Y/n)"
|
||||
$proceed = ($response -eq '' -or $response -match '^[Yy]$')
|
||||
}
|
||||
if (-not $proceed) {
|
||||
$result.Status = 'Skipped'; $result.Diagnostic = 'Declined by operator.'
|
||||
} else {
|
||||
# Persist the exact pre-change value before any mutation. A journal
|
||||
# failure stops this control, including service restarts.
|
||||
$entry = [ordered]@{
|
||||
Version = 1; ComputerName = $env:COMPUTERNAME
|
||||
RecordedUtc = [DateTime]::UtcNow.ToString('o')
|
||||
Id = $Id; Kind = $Kind; Target = $Target
|
||||
Before = $result.Before; Desired = $Desired
|
||||
}
|
||||
$entry | ConvertTo-Json -Depth 12 -Compress |
|
||||
Add-Content -LiteralPath (Join-Path $Context.BackupPath 'before.jsonl') -Encoding UTF8 -ErrorAction Stop
|
||||
$applied = @(& $Apply)
|
||||
$result.Diagnostic = ($applied | ForEach-Object {
|
||||
if ($_.PSObject.Properties['Diagnostic']) { $_.Diagnostic } else { $_.ToString() }
|
||||
}) -join [Environment]::NewLine
|
||||
$result.After = & $Read
|
||||
if (-not (& $Compliant $result.After)) {
|
||||
throw "Post-apply verification did not match the requested state. $($result.Diagnostic)"
|
||||
}
|
||||
$result.Status = 'Applied'
|
||||
}
|
||||
}
|
||||
if ($result.Status -in @('Applied', 'AlreadyCompliant')) {
|
||||
$Context.Checks.Add([pscustomobject]@{ Result = $result; Read = $Read; Compliant = $Compliant })
|
||||
}
|
||||
} catch {
|
||||
$result.Status = 'Failed'; $result.Diagnostic = $_.ToString()
|
||||
}
|
||||
$Context.Results.Add($result)
|
||||
$color = if ($result.Status -eq 'Failed') { 'Red' } elseif ($result.Status -eq 'Skipped') { 'Yellow' } else { 'Green' }
|
||||
Write-Host "[$($result.Status)] $Id $($result.Diagnostic)" -ForegroundColor $color
|
||||
}
|
||||
|
||||
function Complete-WelaConfiguration {
|
||||
param($Context, [string]$ResultsPath)
|
||||
# A second read detects a value that was compliant earlier but changed during
|
||||
# this run. It does not establish whether GPO or another writer caused drift.
|
||||
foreach ($check in $Context.Checks) {
|
||||
try {
|
||||
$check.Result.After = & $check.Read
|
||||
if (-not (& $check.Compliant $check.Result.After)) {
|
||||
$check.Result.Status = 'Overridden'
|
||||
$check.Result.Diagnostic = 'State was compliant earlier but changed before the final check; cause unknown.'
|
||||
}
|
||||
} catch {
|
||||
$check.Result.Status = 'Failed'
|
||||
$check.Result.Diagnostic = "Final verification failed: $_"
|
||||
}
|
||||
}
|
||||
$failed = @($Context.Results | Where-Object { $_.Status -in @('Failed', 'Overridden') }).Count
|
||||
$skipped = @($Context.Results | Where-Object { $_.Status -eq 'Skipped' }).Count
|
||||
$report = [pscustomobject][ordered]@{
|
||||
ExitCode = $(if ($failed) { 1 } else { 0 }); DryRun = $Context.DryRun
|
||||
BackupPath = $Context.BackupPath; Failed = $failed; Skipped = $skipped
|
||||
Results = @($Context.Results.ToArray())
|
||||
}
|
||||
if ($ResultsPath) {
|
||||
try { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
|
||||
catch { $report.ExitCode = 1; Write-Host "[Failed] Writing results: $_" -ForegroundColor Red }
|
||||
}
|
||||
if ($report.ExitCode) { Write-Host "Configuration incomplete: $failed failed or overridden control(s). Review results and recovery journal." -ForegroundColor Red }
|
||||
elseif ($Context.DryRun) { Write-Host 'Dry run completed. No Windows configuration was changed.' -ForegroundColor Cyan }
|
||||
elseif ($skipped) { Write-Host "Configuration completed with $skipped skipped control(s)." -ForegroundColor Yellow }
|
||||
else { Write-Host 'Configuration completed; all requested controls verified.' -ForegroundColor Green }
|
||||
return $report
|
||||
}
|
||||
|
||||
function Set-WelaEventLogControl {
|
||||
param($Context, [string]$Log, [string]$Property, $Desired)
|
||||
$read = { (Get-WinEvent -ListLog $Log -ErrorAction Stop).$Property }.GetNewClosure()
|
||||
$test = if ($Property -eq 'MaximumSizeInBytes') {
|
||||
{ param($value) $value -ge $Desired }.GetNewClosure()
|
||||
} else { { param($value) $value -eq $Desired }.GetNewClosure() }
|
||||
$argument = if ($Property -eq 'MaximumSizeInBytes') { "/ms:$Desired" } else { '/e:true' }
|
||||
$apply = { Invoke-WelaNative -FilePath 'wevtutil.exe' -Arguments @('sl', $Log, $argument) }.GetNewClosure()
|
||||
Invoke-WelaConfigurationControl -Context $Context -Id "EventLog/$Log/$Property" -Kind EventLog `
|
||||
-Target @{ Log = $Log; Property = $Property } -Desired $Desired -Read $read -Compliant $test -Apply $apply
|
||||
}
|
||||
|
||||
function Get-WelaRegistryState {
|
||||
param([string]$Path, [string]$Name)
|
||||
if (-not (Test-Path -LiteralPath $Path -ErrorAction Stop)) {
|
||||
return [pscustomobject]@{ KeyExists = $false; ValueExists = $false; Value = $null; Type = $null }
|
||||
}
|
||||
$key = Get-Item -LiteralPath $Path -ErrorAction Stop
|
||||
if ($key.GetValueNames() -notcontains $Name) {
|
||||
return [pscustomobject]@{ KeyExists = $true; ValueExists = $false; Value = $null; Type = $null }
|
||||
}
|
||||
[pscustomobject]@{
|
||||
KeyExists = $true; ValueExists = $true
|
||||
Value = $key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||
Type = $key.GetValueKind($Name).ToString()
|
||||
}
|
||||
}
|
||||
|
||||
function New-WelaRegistryKey {
|
||||
param([string]$Path)
|
||||
if (Test-Path -LiteralPath $Path -ErrorAction Stop) { return }
|
||||
$separator = $Path.TrimEnd('\').LastIndexOf('\')
|
||||
if ($separator -lt 1) { throw "Registry root is unavailable: $Path" }
|
||||
$parent = $Path.Substring(0, $separator)
|
||||
# Registry New-Item without Force requires its immediate parent. Build only
|
||||
# missing ancestors; never run New-Item -Force against an existing key.
|
||||
New-WelaRegistryKey -Path $parent
|
||||
$null = New-Item -Path $Path -ErrorAction Stop
|
||||
}
|
||||
|
||||
function Set-WelaRegistryControl {
|
||||
param($Context, [string]$Path, [string]$Name, $Value, [string]$Type = 'DWord')
|
||||
$read = { Get-WelaRegistryState -Path $Path -Name $Name }.GetNewClosure()
|
||||
$test = { param($state) $state.ValueExists -and $state.Value -eq $Value -and $state.Type -eq $Type }.GetNewClosure()
|
||||
$apply = {
|
||||
New-WelaRegistryKey -Path $Path
|
||||
Set-ItemProperty -LiteralPath $Path -Name $Name -Value $Value -Type $Type -ErrorAction Stop
|
||||
}.GetNewClosure()
|
||||
Invoke-WelaConfigurationControl -Context $Context -Id "Registry/$Path/$Name" -Kind Registry `
|
||||
-Target @{ Path = $Path; Name = $Name } -Desired @{ Value = $Value; Type = $Type } `
|
||||
-Read $read -Compliant $test -Apply $apply
|
||||
}
|
||||
|
||||
function Initialize-WelaConfigurationAuditApi {
|
||||
if ('Wela.ConfigurationAuditApi' -as [type]) { return }
|
||||
# Querying the Windows API avoids localized auditpol /get CSV (six columns;
|
||||
# unlike /backup output, it has no numeric Setting Value column).
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace Wela {
|
||||
public static class ConfigurationAuditApi {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct AuditPolicyInformation {
|
||||
public Guid Subcategory;
|
||||
public UInt32 Information;
|
||||
public Guid Category;
|
||||
}
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.U1)]
|
||||
private static extern bool AuditQuerySystemPolicy(
|
||||
[In] Guid[] subcategories, UInt32 count, out IntPtr policy);
|
||||
[DllImport("advapi32.dll")]
|
||||
private static extern void AuditFree(IntPtr buffer);
|
||||
public static UInt32 Query(Guid subcategory) {
|
||||
IntPtr buffer = IntPtr.Zero;
|
||||
if (!AuditQuerySystemPolicy(new Guid[] { subcategory }, 1, out buffer)) {
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
try {
|
||||
if (buffer == IntPtr.Zero) throw new InvalidOperationException("Audit policy query returned no buffer.");
|
||||
AuditPolicyInformation policy = (AuditPolicyInformation)Marshal.PtrToStructure(buffer, typeof(AuditPolicyInformation));
|
||||
if (policy.Subcategory != subcategory) throw new InvalidOperationException("Audit policy query returned a different subcategory.");
|
||||
return policy.Information;
|
||||
} finally {
|
||||
if (buffer != IntPtr.Zero) AuditFree(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ -ErrorAction Stop
|
||||
}
|
||||
|
||||
function Get-WelaNativeAuditPolicy {
|
||||
param([string]$Guid)
|
||||
Initialize-WelaConfigurationAuditApi
|
||||
return [Wela.ConfigurationAuditApi]::Query([guid]$Guid)
|
||||
}
|
||||
|
||||
function Get-WelaAuditPolicyMask {
|
||||
param([string]$Guid)
|
||||
$flags = Get-WelaNativeAuditPolicy -Guid $Guid
|
||||
if ($flags -notin @(0, 1, 2, 3, 4)) { throw "Unexpected audit policy flags $flags for $Guid." }
|
||||
# POLICY_AUDIT_EVENT_NONE is 4; the success/failure mask is zero.
|
||||
return [int]($flags -band 3)
|
||||
}
|
||||
|
||||
function Set-WelaAuditPolicyControl {
|
||||
param($Context, $Policy)
|
||||
$guid = $Policy.GUID
|
||||
$read = { Get-WelaAuditPolicyMask -Guid $guid }.GetNewClosure()
|
||||
$apply = { Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments @('/set', "/subcategory:{$guid}", '/success:enable', '/failure:enable') }.GetNewClosure()
|
||||
Invoke-WelaConfigurationControl -Context $Context -Id "AuditPolicy/$($Policy.Name)" -Kind AuditPolicy `
|
||||
-Target @{ Guid = $guid } -Desired 3 -Read $read -Compliant { param($value) $value -eq 3 } -Apply $apply
|
||||
}
|
||||
|
||||
function Set-WelaCertificateAuditControl {
|
||||
param($Context)
|
||||
$root = 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration'
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $root -ErrorAction Stop)) {
|
||||
$Context.Results.Add([pscustomobject]@{ Id = 'ADCS/AuditFilter'; Kind = 'CertificateService'; Target = $root; Desired = 127; Before = $null; After = $null; Status = 'Skipped'; Diagnostic = 'No configured local CA.' })
|
||||
return
|
||||
}
|
||||
$caName = (Get-ItemProperty -LiteralPath $root -Name Active -ErrorAction Stop).Active
|
||||
if (-not $caName) { throw 'CA configuration has no active CA name.' }
|
||||
$path = Join-Path $root $caName
|
||||
$read = {
|
||||
[pscustomobject]@{
|
||||
Registry = Get-WelaRegistryState -Path $path -Name AuditFilter
|
||||
ServiceStatus = (Get-Service -Name CertSvc -ErrorAction Stop).Status.ToString()
|
||||
}
|
||||
}.GetNewClosure()
|
||||
$test = { param($value) $value.Registry.ValueExists -and $value.Registry.Value -eq 127 -and $value.Registry.Type -eq 'DWord' -and $value.ServiceStatus -eq 'Running' }
|
||||
$apply = {
|
||||
$state = Get-Service -Name CertSvc -ErrorAction Stop
|
||||
if ($state.Status -ne 'Running') { throw 'CertSvc is not running; refusing to start a previously stopped CA. Start it deliberately before retrying.' }
|
||||
Invoke-WelaNative -FilePath 'certutil.exe' -Arguments @('-setreg', 'CA\AuditFilter', '127')
|
||||
Restart-Service -Name CertSvc -Force -ErrorAction Stop
|
||||
$service = Get-Service -Name CertSvc -ErrorAction Stop
|
||||
$service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running, [TimeSpan]::FromSeconds(30))
|
||||
}
|
||||
Invoke-WelaConfigurationControl -Context $Context -Id 'ADCS/AuditFilter' -Kind CertificateService `
|
||||
-Target @{ Path = $path; Name = 'AuditFilter'; Service = 'CertSvc' } -Desired 127 `
|
||||
-Read $read -Compliant $test -Apply $apply -Description 'Set AuditFilter=127 and restart Certificate Services.'
|
||||
} catch {
|
||||
$Context.Results.Add([pscustomobject]@{ Id = 'ADCS/AuditFilter'; Kind = 'CertificateService'; Target = $root; Desired = 127; Before = $null; After = $null; Status = 'Failed'; Diagnostic = $_.ToString() })
|
||||
}
|
||||
}
|
||||
|
||||
function Set-WelaNtlmConfigurationControl {
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
$Context,
|
||||
[ValidateSet('Outgoing', 'Domain')][string]$Scope,
|
||||
[ValidateSet('PreserveOrAudit', 'Audit', 'Deny')][string]$Mode = 'PreserveOrAudit'
|
||||
)
|
||||
$path = if ($Scope -eq 'Outgoing') { 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' } else { 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' }
|
||||
$name = if ($Scope -eq 'Outgoing') { 'RestrictSendingNTLMTraffic' } else { 'AuditNTLMInDomain' }
|
||||
$desired = if ($Scope -eq 'Domain') { 7 } elseif ($Mode -eq 'Deny') { 2 } else { 1 }
|
||||
$id = "Registry/$path/$name"
|
||||
$state = $null
|
||||
$skipReason = ''
|
||||
$status = 'Skipped'
|
||||
try {
|
||||
$state = if ($Scope -eq 'Outgoing') { Get-WelaOutgoingNtlmState } else { Get-WelaDomainNtlmState }
|
||||
Write-Host "$Scope NTLM: $($state.Description)"
|
||||
if ($Scope -eq 'Outgoing') { Write-Host "Policy source: $($state.PolicySource)" }
|
||||
if ($Scope -eq 'Domain' -and -not $state.Applicable) {
|
||||
if ($state.Description -notlike 'Not applicable*') { throw "Domain NTLM applicability is unknown: $($state.Description)" }
|
||||
$skipReason = $state.Description
|
||||
} elseif (-not $state.Readable) {
|
||||
throw "$Scope NTLM current state could not be read: $($state.Description)"
|
||||
} elseif ($Scope -eq 'Outgoing' -and $Mode -eq 'PreserveOrAudit' -and $state.Value -eq 2) {
|
||||
$skipReason = 'Preserved existing Deny all enforcement (2); use -OutgoingNtlmMode Audit to explicitly replace it.'
|
||||
} elseif ($Scope -eq 'Outgoing' -and $Mode -eq 'PreserveOrAudit' -and $null -ne $state.Value -and $state.Value -notin @(0, 1, 2)) {
|
||||
$skipReason = "Preserved unknown outgoing NTLM value ($($state.Value)); select an explicit mode after policy review."
|
||||
}
|
||||
if (-not $skipReason) {
|
||||
if ($Scope -eq 'Outgoing' -and $Mode -eq 'Deny') {
|
||||
Write-Warning 'Explicit Deny mode can break NTLM authentication. This is enforcement, not audit-only configuration.'
|
||||
}
|
||||
if (-not $PSCmdlet.ShouldProcess($id, "Set $Scope NTLM policy to $desired")) {
|
||||
$skipReason = 'ShouldProcess declined the NTLM change.'
|
||||
} else {
|
||||
# Shared runner owns prompts, dry-run suppression, exact registry
|
||||
# before-state journal, type/value read-back and final drift check.
|
||||
Set-WelaRegistryControl -Context $Context -Path $path -Name $name -Value $desired
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
$status = 'Failed'
|
||||
$skipReason = $_.ToString()
|
||||
}
|
||||
$Context.Results.Add([pscustomobject][ordered]@{
|
||||
Id = $id; Kind = 'Registry'; Target = @{ Path = $path; Name = $name }
|
||||
Desired = @{ Value = $desired; Type = 'DWord' }; Before = $state; After = $state
|
||||
Status = $status; Diagnostic = $skipReason
|
||||
})
|
||||
$color = if ($status -eq 'Failed') { 'Red' } else { 'Yellow' }
|
||||
Write-Host "[$status] $id $skipReason" -ForegroundColor $color
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Read-only smoke test of real Windows commands, independent of the mock suite.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:OS -ne 'Windows_NT') { throw 'Run this smoke test on Windows.' }
|
||||
. (Join-Path (Split-Path $PSScriptRoot -Parent) 'scripts/Configuration.ps1')
|
||||
$mask = Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030'
|
||||
if ($mask -notin @(0, 1, 2, 3)) { throw "Unexpected process-creation audit mask: $mask" }
|
||||
# Also exercise the real read-only auditpol command and its native exit status.
|
||||
$csv = Invoke-WelaNative -FilePath auditpol.exe -Arguments @('/get', '/subcategory:{0CCE922B-69AE-11D9-BED3-505054503030}', '/r')
|
||||
if ($csv.Diagnostic -notmatch '0CCE922B-69AE-11D9-BED3-505054503030') { throw 'auditpol query returned no requested subcategory.' }
|
||||
$caught = ''
|
||||
try { Invoke-WelaNative -FilePath $env:ComSpec -Arguments @('/d', '/c', 'echo WELA-smoke-diagnostic 1>&2 & exit /b 9') }
|
||||
catch { $caught = $_.ToString() }
|
||||
if ($caught -notmatch 'exit: 9' -or $caught -notmatch 'WELA-smoke-diagnostic') {
|
||||
throw "Native exit/stderr capture failed: $caught"
|
||||
}
|
||||
Write-Host "Read-only Windows smoke checks passed (process creation audit mask: $mask). No Windows settings changed."
|
||||
@@ -0,0 +1,215 @@
|
||||
# No Windows settings are changed. Run with powershell.exe 5.1 or pwsh.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = Split-Path $PSScriptRoot -Parent
|
||||
$script:ScriptRoot = $repo
|
||||
# Load trusted source functions into the same scope as the mocks. Windows
|
||||
# PowerShell 5.1 otherwise resolves a script-local original ahead of global mocks.
|
||||
$definitions = Get-Content -LiteralPath (Join-Path $repo 'scripts/Configuration.ps1') -Raw
|
||||
Invoke-Expression ($definitions -replace '(?m)^function ', 'function global:')
|
||||
$script:passed = 0
|
||||
function Assert($Condition, [string]$Message) {
|
||||
if (-not $Condition) { throw "FAIL: $Message" }
|
||||
$script:passed++
|
||||
}
|
||||
function New-TestContext([switch]$DryRun) {
|
||||
$path = Join-Path ([IO.Path]::GetTempPath()) ('wela-results-test-' + [guid]::NewGuid().ToString('N'))
|
||||
if (-not $DryRun) { $script:cleanup.Add($path) }
|
||||
New-WelaConfigurationContext -Auto -DryRun:$DryRun -BackupPath $path
|
||||
}
|
||||
$script:cleanup = New-Object 'System.Collections.Generic.List[string]'
|
||||
try {
|
||||
foreach ($path in @('WELA.ps1', 'scripts/Configuration.ps1')) {
|
||||
$parseErrors = $null; $tokens = $null
|
||||
$null = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo $path), [ref]$tokens, [ref]$parseErrors)
|
||||
Assert ($parseErrors.Count -eq 0) "Parser accepts $path"
|
||||
}
|
||||
|
||||
# An actual child process exercises exit capture and stderr retention. The
|
||||
# child only emits text and exits; it never calls Windows configuration tools.
|
||||
$engine = (Get-Process -Id $PID).Path
|
||||
$caught = ''
|
||||
try { Invoke-WelaNative -FilePath $engine -Arguments @('-NoProfile', '-Command', "[Console]::Error.WriteLine('injected native diagnostic'); exit 7") }
|
||||
catch { $caught = $_.ToString() }
|
||||
Assert ($caught -match 'exit: 7' -and $caught -match 'injected native diagnostic') 'Native failure retains exit code and stderr'
|
||||
$ok = Invoke-WelaNative -FilePath $engine -Arguments @('-NoProfile', '-Command', "[Console]::Error.WriteLine('non-fatal diagnostic'); exit 0")
|
||||
Assert ($ok.ExitCode -eq 0 -and $ok.Diagnostic -match 'non-fatal diagnostic') 'Stderr alone is not a native failure'
|
||||
|
||||
$script:state = 1; $script:writes = 0
|
||||
$read = { $script:state }; $test = { param($value) $value -eq 2 }
|
||||
$apply = { $script:writes++; $script:state = 2 }
|
||||
$c = New-TestContext
|
||||
Invoke-WelaConfigurationControl $c test Registry @{ Path = 'mock'; Name = 'value' } 2 $read $test $apply
|
||||
Assert ($c.Results[0].Status -eq 'Applied' -and $script:writes -eq 1) 'Changed state is read back before Applied'
|
||||
$journal = Get-Content -LiteralPath (Join-Path $c.BackupPath 'before.jsonl') | ConvertFrom-Json
|
||||
Assert ($journal.Before -eq 1 -and $journal.Desired -eq 2) 'Journal contains exact before and requested state'
|
||||
Invoke-WelaConfigurationControl $c repeated Registry @{} 2 $read $test $apply
|
||||
Assert ($c.Results[1].Status -eq 'AlreadyCompliant' -and $script:writes -eq 1) 'Rerun is idempotent'
|
||||
$r = Complete-WelaConfiguration $c
|
||||
Assert ($r.ExitCode -eq 0) 'Verified controls produce successful overall status'
|
||||
$script:state = 1
|
||||
$r = Complete-WelaConfiguration $c
|
||||
Assert ($r.ExitCode -eq 1 -and $r.Results[0].Status -eq 'Overridden') 'Final check detects observed drift without attributing its cause'
|
||||
|
||||
$c = New-TestContext -DryRun
|
||||
$script:writes = 0
|
||||
Invoke-WelaConfigurationControl $c dry Registry @{} 2 $read $test $apply
|
||||
Assert ($c.Results[0].Status -eq 'Skipped' -and $script:writes -eq 0) 'Dry run never invokes mutation'
|
||||
Assert (-not (Test-Path -LiteralPath $c.BackupPath)) 'Dry run creates no backup or journal'
|
||||
|
||||
$c = New-TestContext
|
||||
Invoke-WelaConfigurationControl $c false_success Registry @{} 2 $read $test { }
|
||||
Assert ($c.Results[0].Status -eq 'Failed') 'Successful write command with wrong read-back is Failed'
|
||||
Assert ((Complete-WelaConfiguration $c).ExitCode -eq 1) 'Read-back failure makes overall status nonzero'
|
||||
|
||||
$c = New-TestContext
|
||||
$c.BackupPath = Join-Path $c.BackupPath 'missing-parent'
|
||||
$script:writes = 0
|
||||
Invoke-WelaConfigurationControl $c journal_failed Registry @{} 2 $read $test $apply
|
||||
Assert ($c.Results[0].Status -eq 'Failed' -and $script:writes -eq 0) 'Journal failure prevents mutation'
|
||||
|
||||
# Registry provider failures and false-success writes use the same verified
|
||||
# control runner; no actual registry provider is touched in these tests.
|
||||
$script:registryValue = 0; $script:registryWrites = 0; $script:registryThrows = $true
|
||||
function global:Get-WelaRegistryState {
|
||||
param($Path, $Name)
|
||||
[pscustomobject]@{ KeyExists = $true; ValueExists = $true; Value = $script:registryValue; Type = 'DWord' }
|
||||
}
|
||||
function global:Test-Path {
|
||||
param($LiteralPath, $Path, $ErrorAction)
|
||||
if ($LiteralPath -like 'HKLM:*') { return $true }
|
||||
Microsoft.PowerShell.Management\Test-Path -LiteralPath $(if ($LiteralPath) { $LiteralPath } else { $Path })
|
||||
}
|
||||
function global:Set-ItemProperty {
|
||||
param($LiteralPath, $Name, $Value, $Type, $ErrorAction)
|
||||
$script:registryWrites++
|
||||
if ($script:registryThrows) { throw 'Injected registry access denied' }
|
||||
$script:registryValue = $Value
|
||||
}
|
||||
$c = New-TestContext
|
||||
Set-WelaRegistryControl $c 'HKLM:\mock' Value 1
|
||||
Assert ($c.Results[0].Status -eq 'Failed' -and $c.Results[0].Diagnostic -match 'access denied') 'Registry write errors produce failed results'
|
||||
$script:registryThrows = $false
|
||||
$c = New-TestContext
|
||||
Set-WelaRegistryControl $c 'HKLM:\mock' Value 1
|
||||
Assert ($c.Results[0].Status -eq 'Applied' -and $c.Results[0].After.Value -eq 1) 'Registry writes require verified value and type'
|
||||
$beforeWrites = $script:registryWrites
|
||||
Set-WelaRegistryControl $c 'HKLM:\mock' Value 1
|
||||
Assert ($c.Results[1].Status -eq 'AlreadyCompliant' -and $script:registryWrites -eq $beforeWrites) 'Registry reruns preserve compliant values'
|
||||
|
||||
# Missing nested registry parents must be created individually, retaining
|
||||
# existing parent keys/values. Mock provider rejects children without parents.
|
||||
$script:mockKeys = @{'HKLM:' = $true; 'HKLM:\SOFTWARE' = $true}
|
||||
$script:createdKeys = New-Object 'System.Collections.Generic.List[string]'
|
||||
function global:Test-Path {
|
||||
param($LiteralPath, $Path, $ErrorAction)
|
||||
if ($LiteralPath -like 'HKLM:*') { return $script:mockKeys.ContainsKey($LiteralPath) }
|
||||
Microsoft.PowerShell.Management\Test-Path -LiteralPath $(if ($LiteralPath) { $LiteralPath } else { $Path })
|
||||
}
|
||||
function global:New-Item {
|
||||
param($Path, $ItemType, [switch]$Force, $ErrorAction)
|
||||
if ($Path -notlike 'HKLM:*') { return Microsoft.PowerShell.Management\New-Item @PSBoundParameters }
|
||||
if ($Force) { throw 'Test refuses Force on registry keys' }
|
||||
if ($script:mockKeys.ContainsKey($Path)) { throw 'Existing parent would be recreated' }
|
||||
$parent = $Path.Substring(0, $Path.LastIndexOf('\'))
|
||||
if (-not $script:mockKeys.ContainsKey($parent)) { throw "Missing registry parent: $parent" }
|
||||
$script:createdKeys.Add($Path); $script:mockKeys[$Path] = $true
|
||||
}
|
||||
New-WelaRegistryKey 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging'
|
||||
Assert ($script:createdKeys.Count -eq 5 -and $script:mockKeys.ContainsKey('HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging')) 'Missing registry ancestors are created safely in order'
|
||||
New-WelaRegistryKey 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging'
|
||||
Assert ($script:createdKeys.Count -eq 5) 'Existing registry parents are preserved on rerun'
|
||||
|
||||
# Function stubs stand in for the Windows APIs from this point onward.
|
||||
$script:logSize = 1048576; $script:nativeFails = $true; $script:nativeWrites = 0
|
||||
function global:Get-WinEvent { param($ListLog, $ErrorAction) [pscustomobject]@{ MaximumSizeInBytes = $script:logSize; IsEnabled = $false } }
|
||||
function global:Invoke-WelaNative {
|
||||
param($FilePath, $Arguments)
|
||||
$script:nativeWrites++
|
||||
if ($script:nativeFails) { throw 'wevtutil.exe failed (exit: 5). Injected access denied' }
|
||||
$script:logSize = 134217728
|
||||
[pscustomobject]@{ ExitCode = 0; Output = @(); Diagnostic = 'mock success' }
|
||||
}
|
||||
$c = New-TestContext
|
||||
Set-WelaEventLogControl $c Security MaximumSizeInBytes 134217728
|
||||
$r = Complete-WelaConfiguration $c
|
||||
Assert ($r.ExitCode -eq 1 -and $r.Results[0].Diagnostic -match 'access denied') 'Injected wevtutil failure survives through the final report'
|
||||
$script:nativeFails = $false
|
||||
$c = New-TestContext
|
||||
Set-WelaEventLogControl $c Security MaximumSizeInBytes 134217728
|
||||
Assert ($c.Results[0].Status -eq 'Applied') 'Event log helper reads verified size'
|
||||
Set-WelaEventLogControl $c Security MaximumSizeInBytes 134217728
|
||||
Assert ($c.Results[1].Status -eq 'AlreadyCompliant') 'Event log helper avoids repeated writes'
|
||||
|
||||
# Compile the interop declaration without invoking Windows APIs on this host.
|
||||
Initialize-WelaConfigurationAuditApi
|
||||
Assert ($null -ne ('Wela.ConfigurationAuditApi' -as [type])) 'Audit query interop compiles'
|
||||
function global:Get-WelaNativeAuditPolicy { param($Guid) return 3 }
|
||||
Assert ((Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030') -eq 3) 'Audit policy uses native numeric flags independent of locale'
|
||||
function global:Get-WelaNativeAuditPolicy { param($Guid) return 4 }
|
||||
Assert ((Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030') -eq 0) 'Native NONE flag normalizes to no success/failure audit'
|
||||
function global:Get-WelaNativeAuditPolicy { param($Guid) return 16 }
|
||||
$caught = ''
|
||||
try { Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030' } catch { $caught = $_.ToString() }
|
||||
Assert ($caught -ne '') 'Unexpected native flags cannot be marked compliant'
|
||||
|
||||
# Extract ConfigureAuditSettings without running the WELA command dispatcher.
|
||||
$tokens = $null; $errors = $null
|
||||
$ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors)
|
||||
$configure = $ast.Find({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'ConfigureAuditSettings' }, $false)
|
||||
Assert ($configure.Extent.Text -notmatch 'Configuration completed successfully|Start-Process|Out-Null') 'Configure has no unverified native execution or unconditional success'
|
||||
|
||||
# Run the actual configure dispatcher in a child process with only the
|
||||
# configuration function replaced by a harmless failed-report fixture.
|
||||
$dispatch = $ast.Find({ param($node) $node -is [Management.Automation.Language.SwitchStatementAst] -and $node.Condition.Extent.Text -eq '$Cmd.ToLower()' }, $false)
|
||||
$clause = @($dispatch.Clauses | Where-Object { $_.Item1.Value -eq 'configure' })[0].Item2.Extent.Text
|
||||
$child = 'function ConfigureAuditSettings { [pscustomobject]@{ ExitCode = 1; Failed = 1; Results = @() } }; & ' + $clause
|
||||
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($child))
|
||||
$childOutput = @(& $engine -NoProfile -EncodedCommand $encoded 2>&1)
|
||||
$childExit = $global:LASTEXITCODE
|
||||
Assert ($childExit -eq 1) 'The actual configure dispatcher returns nonzero for a failed control report'
|
||||
|
||||
# CA-specific wrapper: registry read succeeds, certutil succeeds, restart
|
||||
# fails. All APIs below are mocks, including Test-Path for the mock CA only.
|
||||
$realTestPath = (Get-Command Test-Path).Name
|
||||
function global:Test-Path {
|
||||
param($LiteralPath, $Path, $ErrorAction)
|
||||
if ($LiteralPath -like 'HKLM:*') { return $true }
|
||||
Microsoft.PowerShell.Management\Test-Path -LiteralPath $(if ($LiteralPath) { $LiteralPath } else { $Path })
|
||||
}
|
||||
function global:Get-ItemProperty { param($LiteralPath, $Name, $ErrorAction) [pscustomobject]@{ Active = 'MockCA' } }
|
||||
function global:Join-Path {
|
||||
param($Path, $ChildPath)
|
||||
if ($Path -like 'HKLM:*') { return "$Path\$ChildPath" }
|
||||
Microsoft.PowerShell.Management\Join-Path -Path $Path -ChildPath $ChildPath
|
||||
}
|
||||
$script:filter = 0; $script:restartCalls = 0; $script:filterType = 'DWord'
|
||||
function global:Get-WelaRegistryState { param($Path, $Name) [pscustomobject]@{ ValueExists = $true; Value = $script:filter; Type = $script:filterType; KeyExists = $true } }
|
||||
function global:Get-Service { param($Name, $ErrorAction) [pscustomobject]@{ Status = 'Running' } }
|
||||
function global:Restart-Service { param($Name, [switch]$Force, $ErrorAction) $script:restartCalls++; throw 'Injected CertSvc restart failure' }
|
||||
function global:Invoke-WelaNative { param($FilePath, $Arguments) $script:filter = 127; [pscustomobject]@{ ExitCode = 0; Diagnostic = 'mock certutil' } }
|
||||
$c = New-TestContext
|
||||
Set-WelaCertificateAuditControl $c
|
||||
$r = Complete-WelaConfiguration $c
|
||||
Assert ($r.ExitCode -eq 1 -and $r.Results[0].Diagnostic -match 'restart failure') 'CA restart failure cannot report success even when registry now equals 127'
|
||||
$script:filter = 0; $script:restartCalls = 0
|
||||
function global:Invoke-WelaNative { param($FilePath, $Arguments) throw 'certutil failed (exit: 5)' }
|
||||
$c = New-TestContext
|
||||
Set-WelaCertificateAuditControl $c
|
||||
Assert ($c.Results[0].Status -eq 'Failed' -and $script:restartCalls -eq 0) 'Failed certutil never restarts the CA'
|
||||
$c = New-TestContext -DryRun
|
||||
Set-WelaCertificateAuditControl $c
|
||||
Assert ($c.Results[0].Status -eq 'Skipped' -and $script:restartCalls -eq 0) 'CA dry run never writes or restarts'
|
||||
|
||||
$script:filter = '127'; $script:filterType = 'String'
|
||||
$c = New-TestContext -DryRun
|
||||
Set-WelaCertificateAuditControl $c
|
||||
Assert ($c.Results[0].Status -eq 'Skipped') 'REG_SZ 127 is not accepted as a compliant CA DWORD AuditFilter'
|
||||
|
||||
Write-Host "$script:passed configuration-result regression assertions passed. No Windows settings changed."
|
||||
} finally {
|
||||
foreach ($path in $script:cleanup) {
|
||||
if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $path) {
|
||||
Remove-Item -LiteralPath $path -Recurse -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user