Integrate versioned audit profiles with verified configuration

# Conflicts:
#	.github/workflows/release.yml
#	WELA.ps1
This commit is contained in:
Shirofune-Security committed 2026-09-18 22:00:30 +09:00
commit d480db5a76
10 files changed
+5578 -46

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
name: Audit profile regression tests
on:
push:
branches: ['**']
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
profiles:
runs-on: windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Test shared profiles in Windows PowerShell 5.1
shell: powershell
run: ./tests/audit-profiles.Tests.ps1
- name: Test shared profiles in PowerShell 7
shell: pwsh
run: ./tests/audit-profiles.Tests.ps1
- name: Read all effective policies using native API in Windows PowerShell 5.1
shell: powershell
run: ./tests/audit-profiles.Windows.Tests.ps1
- name: Read all effective policies using native API in PowerShell 7
shell: pwsh
run: ./tests/audit-profiles.Windows.Tests.ps1
+1
View File
@@ -39,6 +39,7 @@ jobs:
Copy-Item -Path WELA.ps1 -Destination release-binaries/
Copy-Item -Recurse -Path ./config -Destination release-binaries/
Copy-Item -Recurse -Path ./scripts -Destination release-binaries/
Copy-Item -Recurse -Path ./modules -Destination release-binaries/
- name: Set Artifact Name
if: contains(matrix.info.os, 'windows') == true
+2
View File
@@ -42,6 +42,8 @@ Windows event logs are a vital source of information for Digital Forensics and I
(DFIR) — WELA checks your audit policy and log file sizes against best-practice guidelines and
real-world Sigma-rule detectability, and can apply the recommended settings for you.
Advanced audit policy can also use [versioned WELA, Microsoft, CIS and ASD profiles](docs/audit-profiles.md) for shared audit, plan and configure behavior. Profiles cover advanced audit policy only.
## 📖 Documentation
All documentation now lives on a dedicated, searchable, multi-language site:
+101 -41
View File
@@ -3,6 +3,11 @@
[string]$OutType = "std",
[switch]$Debug,
[string]$Baseline,
[string]$Profile,
[ValidateSet("Client", "MemberServer", "DomainController", "ADCS")][string]$Role,
[int]$Build,
[string]$PlanPath,
[switch]$IncludeOptional,
[switch]$Auto,
[ValidateSet("PreserveOrAudit", "Audit", "Deny")]
[string]$OutgoingNtlmMode = "PreserveOrAudit",
@@ -23,6 +28,7 @@ $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")
Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop
# 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。
$PowerShellPolicyRoots = @(
@@ -279,6 +285,50 @@ function GetBaselineNames {
return @((GetBaselineConfig).baselines.PSObject.Properties.Name)
}
function Get-WelaSelectedContext {
if (($script:Role -and -not $script:Build) -or ($script:Build -and -not $script:Role)) {
throw "Specify both -Role and -Build, or neither to detect this Windows host."
}
if ($script:Role -and $script:Build) {
return [pscustomobject]@{ Role = $script:Role; Build = $script:Build }
}
Get-WelaHostContext
}
function Invoke-WelaProfileCommand {
param([string]$Command)
if ($script:Baseline) { throw "Use -Profile or -Baseline, not both. Versioned profiles cover advanced audit policy only." }
if (-not $script:Profile) { throw "Specify -Profile. Use './WELA.ps1 profiles' to list versioned profiles." }
$context = Get-WelaSelectedContext
$current = @{}
if (TestWindows) {
$actual = Get-WelaHostContext
if ($actual.Role -eq $context.Role -and $actual.Build -eq $context.Build) { $current = Get-WelaEffectiveAuditPolicy }
elseif ($Command -ne 'plan') { throw "Requested role/build does not match this Windows host." }
else { Write-Host "Planning for another role/build: effective state remains Unknown." }
}
elseif ($Command -ne 'plan') { throw "Audit and configure require Windows. Offline planning requires explicit -Role and -Build." }
$plan = Get-WelaAuditProfilePlan -Profile $script:Profile -Role $context.Role -Build $context.Build -Current $current -IncludeOptional:$script:IncludeOptional
Write-Host "Profile: $($plan.profile); role: $($plan.role); build: $($plan.build)"
Write-Host "Scope: advanced audit policy only. Channels, command-line capture, PowerShell, NTLM, SACLs, CA AuditFilter and forwarding are separate."
$result = $plan
if ($Command -eq 'configure') {
if (-not (TestAdministrator)) { throw "Configuring advanced audit policy requires Administrator privileges." }
Assert-WelaAuditProfileTarget -Plan $plan -Context $actual -Current $current
$configurationContext = New-WelaConfigurationContext -Auto:$script:Auto -DryRun:$script:DryRun -BackupPath $script:BackupPath
Set-WelaProfileAuditControls -Context $configurationContext -Plan $plan
$result = Complete-WelaConfiguration -Context $configurationContext -ResultsPath $script:ResultsPath -Plan $plan
$result.Results | Format-Table Id, Before, Desired, After, Status -AutoSize
} else {
$plan.policies | Format-Table id, mode, currentMask, requiredMask, action -AutoSize
}
if ($script:PlanPath) {
$result | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $script:PlanPath -Encoding UTF8 -ErrorAction Stop
Write-Host "Machine-readable result: $($script:PlanPath)"
}
if ($Command -eq 'configure' -and $result.ExitCode -ne 0) { throw "One or more advanced audit policies failed. See the effective-state results." }
}
function BuildAuditResult {
param (
[object[]] $all_rules,
@@ -299,8 +349,16 @@ function BuildAuditResult {
$auditpol = GetAuditpol
$auditResult = @()
$sharedPlan = $null
if ($baselineName -eq 'YamatoSecurity') {
$context = Get-WelaSelectedContext
$sharedPlan = Get-WelaAuditProfilePlan -Profile 'wela-2.2.0' -Role $context.Role -Build $context.Build -IncludeOptional:$script:IncludeOptional
Write-Host "Advanced audit recommendations: $($sharedPlan.profile), role $($sharedPlan.role), build $($sharedPlan.build). Other controls use the existing baseline metadata."
}
foreach ($item in $config.catalog) {
# The versioned profile owns all advanced-audit recommendations and canonical GUIDs.
if ($sharedPlan -and $item.currentSetting.type -eq 'auditpol') { continue }
$setting = $settings.($item.id)
if (-not $setting) {
throw "Baseline '$baselineName' has no entry for catalog id '$($item.id)'."
@@ -385,6 +443,26 @@ function BuildAuditResult {
)
}
if ($sharedPlan) {
foreach ($policy in $sharedPlan.policies) {
$rules = ApplyRules -rules $all_rules -guid $policy.guid
$current = if ($auditpol.ContainsKey($policy.guid)) { $auditpol[$policy.guid] } else { 'Unknown' }
if ($policy.mode -ne 'not-applicable' -and $enabledguid -contains $policy.guid) {
$rules | ForEach-Object { $_.applicable = $true }
}
if ($policy.mode -in @('exact', 'minimum') -and $policy.requiredMask -ne 0) {
$rules | ForEach-Object { $_.ideal = $true }
}
$legacyItem = $config.catalog | Where-Object { $_.subCategory -eq $policy.id -and $_.currentSetting.type -eq 'auditpol' } | Select-Object -First 1
$legacy = if ($legacyItem) { $settings.($legacyItem.id) } else { $null }
$defaultSetting = if ($legacy) { $legacy.defaultSetting } else { '' }
$volume = if ($legacy) { $legacy.volume } else { '' }
$note = (@($policy.prerequisites, $policy.note) | Where-Object { $_ }) -join ' '
$auditResult += [WELA]::New("Security Advanced ($($policy.category))", $policy.id, $current, [array]$rules,
$defaultSetting, $policy.recommendation, $volume, $note)
}
}
# どのカテゴリにも該当しなかったルールを取りこぼさない。
# 集計対象から黙って消えると、利用率の分母がルール総数と合わなくなる。
$covered = [System.Collections.Generic.HashSet[string]]::new()
@@ -1280,6 +1358,11 @@ function ConfigureAuditSettings {
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 }
# Reject unsupported roles/builds or unknown required policies before any writes.
$hostContext = Get-WelaHostContext
$effectivePolicy = Get-WelaEffectiveAuditPolicy
$profilePlan = Get-WelaAuditProfilePlan -Profile 'wela-2.2.0' -Role $hostContext.Role -Build $hostContext.Build -Current $effectivePolicy -IncludeOptional:$script:IncludeOptional
Assert-WelaAuditProfileTarget -Plan $profilePlan -Context $hostContext -Current $effectivePolicy
$context = New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
if (-not $DryRun) { Write-Host "Recovery journal: $($context.BackupPath)" }
@@ -1343,48 +1426,10 @@ function ConfigureAuditSettings {
) -Auto:$Auto -Context $context
}
$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"},
@{Category = "Account Logon"; Name = "Kerberos Service Ticket Operations"; GUID = "0CCE9240-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Computer Account Management"; GUID = "0CCE9236-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Distribution Group Management"; GUID = "0CCE9238-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Other Account Management Events"; GUID = "0CCE923A-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Security Group Management"; GUID = "0CCE9237-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "User Account Management"; GUID = "0CCE9235-69AE-11D9-BED3-505054503030"},
@{Category = "Detailed Tracking"; Name = "Plug and Play"; GUID = "0cce9248-69ae-11d9-bed3-505054503030"},
@{Category = "Detailed Tracking"; Name = "Process Creation"; GUID = "0CCE922B-69AE-11D9-BED3-505054503030"},
@{Category = "Detailed Tracking"; Name = "Process Termination"; GUID = "0CCE922C-69AE-11D9-BED3-505054503030"},
@{Category = "Detailed Tracking"; Name = "RPC Events"; GUID = "0CCE922E-69AE-11D9-BED3-505054503030"},
@{Category = "DS Access"; Name = "Directory Service Access"; GUID = "0CCE923B-69AE-11D9-BED3-505054503030"},
@{Category = "DS Access"; Name = "Directory Service Changes"; GUID = "0CCE923C-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Account Lockout"; GUID = "0CCE9217-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Logoff"; GUID = "0CCE9216-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Logon"; GUID = "0CCE9215-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Other Logon/Logoff Events"; GUID = "0CCE921C-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Special Logon"; GUID = "0CCE921B-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Certification Services"; GUID = "0CCE9221-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "File Share"; GUID = "0CCE9224-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Detailed File Share"; GUID = "0CCE9244-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Filtering Platform Connection"; GUID = "0CCE9226-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Other Object Access Events"; GUID = "0CCE9227-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Removable Storage"; GUID = "0CCE9245-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "SAM"; GUID = "0CCE9220-69AE-11D9-BED3-505054503030"},
@{Category = "Policy Change"; Name = "Audit Policy Change"; GUID = "0CCE922F-69AE-11D9-BED3-505054503030"},
@{Category = "Policy Change"; Name = "Authentication Policy Change"; GUID = "0CCE9230-69AE-11D9-BED3-505054503030"},
@{Category = "Policy Change"; Name = "Other Policy Change Events"; GUID = "0CCE9234-69AE-11D9-BED3-505054503030"},
@{Category = "Privilege Use"; Name = "Sensitive Privilege Use"; GUID = "0CCE9228-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "Security State Change"; GUID = "0CCE9210-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "Security System Extension"; GUID = "0CCE9211-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "System Integrity"; GUID = "0CCE9212-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "Other System Events"; GUID = "0CCE9214-69AE-11D9-BED3-505054503030"}
)
foreach ($policy in $auditPolicies) {
Set-WelaAuditPolicyControl -Context $context -Policy $policy
}
# Both audit display and mutation use the versioned role-aware profile.
Set-WelaProfileAuditControls -Context $context -Plan $profilePlan
Set-WelaCertificateAuditControl -Context $context
Complete-WelaConfiguration -Context $context -ResultsPath $ResultsPath
Complete-WelaConfiguration -Context $context -ResultsPath $ResultsPath -Plan $profilePlan
}
$logo = @"
@@ -1643,6 +1688,11 @@ function Get-WelaUserProfiles {
$usage = @"
Usage:
./WELA.ps1 profiles # List versioned advanced audit-policy profiles
./WELA.ps1 plan -Profile wela-2.2.0 -Role Client -Build 26100 -PlanPath plan.json
./WELA.ps1 audit-settings -Profile microsoft-sct-win11-24h2 -PlanPath audit.json
./WELA.ps1 configure -Profile asd-native-2021-10 -PlanPath result.json -Auto
# -Profile changes advanced audit policy ONLY. Optional controls need -IncludeOptional.
./WELA.ps1 audit-settings -Baseline YamatoSecurity # Audit current setting and show in stdout, save to csv
./WELA.ps1 audit-settings -Baseline ASD -OutType gui # Audit current setting and show in gui, save to csv
./WELA.ps1 audit-filesize -Baseline YamatoSecurity # Audit current file size and show in stdout, save to csv
@@ -1662,7 +1712,17 @@ Write-Host ""
Write-Host "WELA v$WELAVersion - $WELAReleaseName"
Write-Host ""
if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'configure') -and -not $Help) {
Invoke-WelaProfileCommand -Command $Cmd.ToLower()
return
}
switch ($Cmd.ToLower()) {
"profiles" {
(Import-WelaAuditProfiles).profiles | Select-Object id, version, scope, appliesTo | Format-List
}
"plan" { Invoke-WelaProfileCommand -Command 'plan' }
"audit" { Invoke-WelaProfileCommand -Command 'audit' }
"audit-settings" {
if ($Help -or [string]::IsNullOrEmpty($Baseline)){
Write-Host "Audit current Windows Event Log settings and compare with baseline"
File diff suppressed because it is too large. Load diff
+74
View File
@@ -0,0 +1,74 @@
# Versioned advanced audit-policy profiles
`audit-settings`, `plan`, and `configure` share `config/audit_profiles.json` for advanced Security audit policy. The ordinary `audit-settings -Baseline YamatoSecurity` and ordinary `configure` also use `wela-2.2.0`, eliminating a separate hard-coded configuration list. All 59 subcategories use canonical GUIDs, including categories missing from the older display catalog.
**Profile scope is advanced audit policy only.** Selecting Microsoft, CIS or ASD does not configure their PowerShell settings, command-line capture, channel buffers, NTLM policy, firewall logs, SACLs, CA AuditFilter, forwarding or retention. This is not a claim of full baseline compliance or detection coverage. Sysmon and external sensors are outside this feature. Ordinary `configure` without `-Profile` continues the existing broader WELA setup, with its advanced audit portion supplied by the shared profile.
## Commands
```powershell
# List exact profile ids and role/build applicability.
.\WELA.ps1 profiles
# Offline planning is available on any platform; unknown effective state stays Unknown.
.\WELA.ps1 plan -Profile wela-2.2.0 -Role Client -Build 26100 -PlanPath plan.json
# On Windows, omit Role/Build to detect this host and read effective auditpol values.
.\WELA.ps1 audit-settings -Profile microsoft-sct-win11-24h2 -PlanPath audit.json
# Apply ONLY advanced audit policy. Interactive unless -Auto is supplied.
.\WELA.ps1 configure -Profile asd-native-2021-10 -Auto -PlanPath result.json
# Select optional File System/Registry policy flags, without creating SACLs.
.\WELA.ps1 configure -Profile asd-native-2021-10 -IncludeOptional -PlanPath result.json
```
Supply both `-Role` and `-Build`, or omit both for Windows host detection. Roles are `Client`, `MemberServer`, `DomainController`, and `ADCS` (CA on a member server). Combined DC/CA deployments are not a separate profile: detection identifies them as DCs; review CA requirements separately. Build means the base build, for example 20348 (Server 2022), 26100 (Windows 11 24H2 / Server 2025), or 26200 (Windows 11 25H2). Live application checks the actual Windows host; a supplied role/build cannot authorize applying a mismatched plan. Versioned SCT profiles reject other base builds. Unsupported profiles/hosts and unreadable policies fail before writes.
The WELA and documentary guide profiles currently cover the reviewed Windows 11/Server 2022/Server 2025 range. Older/future operating systems require a reviewed applicability update. `-Baseline` retains the legacy display interface for non-Yamato guides; use `-Profile` to select the versioned shared definitions. Do not combine `-Baseline` and `-Profile`.
## Included sources
| Profile | Version / meaning |
| --- | --- |
| `wela-2.2.0` | Reviewed WELA development snapshot `8ef938f0966e86adc527395f50f907c43e843d1e`; retains the 34 existing success/failure policies, with irrelevant roles skipped and three SACL prerequisites optional |
| `windows-defaults-reviewed-2026-09` | Documentary effective-default model; **reference only**, cannot be applied or used to reset an OS |
| `microsoft-sct-win11-24h2`, `microsoft-sct-win11-25h2` | Official SCT Policy Analyzer settings, exact masks |
| `microsoft-sct-server2022`, `microsoft-sct-server2025-2602` | Official SCT member/DC settings; AD CS uses the member-server baseline |
| `microsoft-stronger-reviewed-2026-09` | Stronger audit recommendation column; minimum enabled flags, conditional IPsec opt-in; ambiguous unspecified success/failure values preserved |
| `microsoft-wef-reviewed-2026-09` | WEF Appendix A minimum audit policy, preserving explicit Not Configured |
| `microsoft-identity-reviewed-2026-09` | Identity collection's DC/CA advanced audit requirements only; other prerequisites remain separate |
| `cis-win11-v4-l1`, `cis-win11-v4-l2` | Historical Windows 11 Enterprise v4.0.0, retaining “includes” minimum semantics |
| `cis-server2022-v4-l1`, `cis-server2022-v4-l2` | Historical Server 2022 v4.0.0, role-aware DC requirements |
| `asd-native-2021-10` | ASD native fallback; optional object auditing and explicit Detailed File Share Not Configured |
CIS v4.0.0 is not the latest CIS edition. Defaults combine documentary evidence that is not a clean-install measurement, and some Server values are shared across roles. The defaults profile is deliberately blocked from application. Source URLs, versions, setting-level evidence, notes and prerequisites are in the JSON and exported plans. The catalog GUID reference is [Microsoft MS-GPAC](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/77878370-0712-47cd-997d-b07053429f6d).
## Policy semantics
Mask bits are Success `1`, Failure `2`, both `3`, neither `0`.
| Mode | Behavior |
| --- | --- |
| `exact` | Set the exact mask; may remove an existing success/failure flag |
| `minimum` | Bitwise OR with fresh effective state, preserving additional auditing |
| `unchanged` | Preserve current state; every omitted control becomes an explicit unchanged plan row |
| `not-configured` | Preserve effective policy; do not interpret it as disabled and do not remove a GPO |
| `optional` | Preserve unless `-IncludeOptional` is supplied; then set the explicit mask |
| `not-applicable` | Preserve; skip a subcategory outside the selected role |
For example Detailed File Share is exact S+F in WELA, minimum Failure in the reviewed CIS profiles, and Not Configured in ASD. These are deliberate differences, not a universal “enable everything” preset. Omitted values and unknown effective state are different: unknown state blocks application instead of becoming mask zero.
Effective policy is read through the Windows [AuditQuerySystemPolicy API](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-auditquerysystempolicy); localized `auditpol /get /r` text is not parsed. Apply reads effective state again, checks native command errors, then verifies each changed mask. A command that exits successfully but does not change effective policy is reported as failed. Group Policy can reapply after a successful verification: these are local effective-policy changes, not GPO authoring. Exported plan/current state and apply results include the profile version, schema SHA-256, source provenance, before/target/effective masks and failure details. This feature does not validate event generation, SACL correctness, ingestion, or Sigma field compatibility.
## Extending the schema and testing
Add a catalog entry with a unique GUID, category, supported roles and prerequisite text. Add or override profile controls using `mode`, `mask` (only for exact/minimum/optional), optional `note`, `evidence`, and `sourceIds`. A profile supplies `sourceIds`, an explicit role/build range, `omitted: unchanged`, and `scope: advanced-audit-policy-only`. Optional control source ids are added to profile provenance. Do not silently revise a published source version when its semantics change.
```powershell
# Pure tests, including injected native boundaries; no policy changes or elevation.
pwsh -NoProfile -File tests/audit-profiles.Tests.ps1
powershell -NoProfile -File tests/audit-profiles.Tests.ps1
```
The tests cover source/schema validation, role/build gating, exact/minimum/optional/NC behavior, locale-independent native policy reads, unknown-state refusal, fresh-state merging, idempotence, failed commands and verification, and the ordinary Yamato audit display. CI runs these on Windows PowerShell 5.1 and PowerShell 7. Source review and mocked tests are not substitutes for checking effective policy and benign event XML on isolated Windows clients, member servers, DCs and CAs.
+253
View File
@@ -0,0 +1,253 @@
# Requires Windows PowerShell 5.1 or PowerShell 7. No Windows dependency for schema/planning.
Set-StrictMode -Version 2.0
function Get-WelaProperty {
param($Object, [string]$Name, $Default = $null)
if ($null -ne $Object -and $null -ne $Object.PSObject.Properties[$Name]) { return $Object.$Name }
return $Default
}
function Import-WelaAuditProfiles {
[CmdletBinding()]
param([string]$Path = (Join-Path $PSScriptRoot '../config/audit_profiles.json'))
$data = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if ($data.schemaVersion -ne 1) { throw 'Unsupported audit profile schema version.' }
$roles = @('Client', 'MemberServer', 'DomainController', 'ADCS')
$ids = @{}; $guids = @{}; $profileIds = @{}
foreach ($policy in $data.catalog) {
if (-not $policy.id -or $ids.ContainsKey($policy.id)) { throw "Duplicate or empty policy id: $($policy.id)" }
if ($policy.guid -notmatch '^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$' -or $guids.ContainsKey($policy.guid)) { throw "Invalid or duplicate GUID: $($policy.guid)" }
if (@($policy.roles).Count -eq 0 -or @($policy.roles | Where-Object { $_ -notin $roles }).Count) { throw "Invalid policy roles: $($policy.id)" }
$ids[$policy.id] = $true; $guids[$policy.guid] = $true
}
foreach ($profile in $data.profiles) {
if (-not $profile.id -or $profileIds.ContainsKey($profile.id)) { throw "Duplicate or empty profile id: $($profile.id)" }
$profileIds[$profile.id] = $true
if ($profile.omitted -ne 'unchanged' -or $profile.scope -ne 'advanced-audit-policy-only' -or -not $profile.version) { throw "Invalid profile metadata: $($profile.id)" }
if (@($profile.sourceIds).Count -eq 0) { throw "Missing profile provenance: $($profile.id)" }
foreach ($source in $profile.sourceIds) {
if (-not $data.sources.PSObject.Properties[$source]) { throw "Unknown profile source: $source" }
}
if (@($profile.appliesTo).Count -eq 0) { throw "Missing applicability: $($profile.id)" }
foreach ($range in $profile.appliesTo) {
if (@($range.roles).Count -eq 0 -or @($range.roles | Where-Object { $_ -notin $roles }).Count -or $range.minBuild -lt 0 -or $range.maxBuild -lt $range.minBuild) { throw "Invalid applicability: $($profile.id)" }
}
$sets = @($profile.controls)
foreach ($override in $profile.roleOverrides.PSObject.Properties) {
if ($override.Name -notin $roles) { throw "Unknown role override: $($override.Name)" }
$sets += $override.Value
}
foreach ($set in $sets) {
foreach ($property in $set.PSObject.Properties) {
if (-not $ids.ContainsKey($property.Name)) { throw "Unknown audit policy: $($property.Name)" }
$control = $property.Value
foreach ($sourceId in @(Get-WelaProperty $control 'sourceIds' @())) {
if (-not $data.sources.PSObject.Properties[$sourceId]) { throw "Unknown control source: $sourceId" }
}
if ($control.mode -notin @('exact', 'minimum', 'unchanged', 'not-configured', 'optional', 'not-applicable')) { throw "Invalid mode: $($control.mode)" }
$hasMask = $null -ne $control.PSObject.Properties['mask']
if ($control.mode -in @('exact', 'minimum', 'optional')) {
if (-not $hasMask -or $control.mask -isnot [ValueType] -or $control.mask -is [bool] -or $control.mask -notin @(0, 1, 2, 3) -or [double]$control.mask -ne [int]$control.mask) { throw "Invalid mask: $($property.Name)" }
} elseif ($hasMask) { throw "Non-setting mode cannot have a mask: $($property.Name)" }
}
}
}
return $data
}
function Format-WelaAuditMask {
param($Mask)
if ($null -eq $Mask) { return 'Unknown' }
switch ([int]$Mask) { 0 { 'No Auditing' } 1 { 'Success' } 2 { 'Failure' } 3 { 'Success and Failure' } default { throw "Invalid mask: $Mask" } }
}
function Get-WelaAuditProfilePlan {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Profile,
[Parameter(Mandatory)][ValidateSet('Client', 'MemberServer', 'DomainController', 'ADCS')][string]$Role,
[Parameter(Mandatory)][ValidateRange(1, 999999)][int]$Build,
[hashtable]$Current = @{}, [switch]$IncludeOptional,
[string]$Path = (Join-Path $PSScriptRoot '../config/audit_profiles.json')
)
$data = Import-WelaAuditProfiles -Path $Path
$selected = @($data.profiles | Where-Object { $_.id -eq $Profile })
if ($selected.Count -ne 1) { throw "Unknown audit profile '$Profile'. Use -Cmd profiles to list profiles." }
$selected = $selected[0]
$matches = @($selected.appliesTo | Where-Object { $Role -in $_.roles -and $Build -ge $_.minBuild -and $Build -le $_.maxBuild })
if ($matches.Count -eq 0) { throw "Profile '$Profile' does not support role '$Role', build '$Build'." }
foreach ($value in $Current.Values) {
if ($null -ne $value -and ($value -is [bool] -or $value -notin @(0, 1, 2, 3))) { throw "Invalid effective audit mask: $value" }
}
$controls = @{}
foreach ($property in $selected.controls.PSObject.Properties) { $controls[$property.Name] = $property.Value }
$override = Get-WelaProperty $selected.roleOverrides $Role
if ($override) { foreach ($property in $override.PSObject.Properties) { $controls[$property.Name] = $property.Value } }
$rows = foreach ($policy in $data.catalog) {
$control = $controls[$policy.id]
$mode = if ($control) { $control.mode } else { 'unchanged' }
if ($Role -notin $policy.roles) { $mode = 'not-applicable' }
$mask = Get-WelaProperty $control 'mask'
$currentMask = if ($Current.ContainsKey($policy.guid)) { $Current[$policy.guid] } else { $null }
$desired = $null; $action = 'Preserve'; $compliance = 'Not assessed'
if ($mode -eq 'not-applicable') { $action = 'Not applicable'; $mask = $null }
elseif ($mode -eq 'optional' -and -not $IncludeOptional) { $action = 'Optional (not selected)' }
elseif ($mode -in @('exact', 'minimum', 'optional')) {
if ($null -eq $currentMask) { $action = 'Unknown'; $compliance = 'Unknown' }
else {
$desired = if ($mode -eq 'minimum') { [int]$currentMask -bor [int]$mask } else { [int]$mask }
$action = if ($currentMask -eq $desired) { 'No change' } else { 'Set' }
$compliance = if ($action -eq 'No change') { 'Compliant' } else { 'Drift' }
}
}
[pscustomobject][ordered]@{
id = $policy.id; guid = $policy.guid; category = $policy.category; mode = $mode
requiredMask = $mask; currentMask = $currentMask; targetMask = $desired
recommendation = if ($mode -in @('exact', 'minimum', 'optional')) { "$(Format-WelaAuditMask $mask) [$mode]" } else { $mode }
action = $action; compliance = $compliance; prerequisites = $policy.prerequisites
note = Get-WelaProperty $control 'note' ''; evidence = Get-WelaProperty $control 'evidence' ''
sourceIds = @(@($selected.sourceIds) + @(Get-WelaProperty $control 'sourceIds' @()) | Select-Object -Unique)
}
}
$sourceIds = @($rows | ForEach-Object { $_.sourceIds } | Select-Object -Unique)
$sources = foreach ($id in $sourceIds) { [pscustomobject]@{ id = $id; source = $data.sources.$id } }
[pscustomobject][ordered]@{
schemaVersion = 1; profile = $selected.id; version = $selected.version
scope = $selected.scope; role = $Role; build = $Build; includeOptional = [bool]$IncludeOptional
referenceOnly = [bool](Get-WelaProperty $selected 'referenceOnly' $false)
generatedUtc = [DateTime]::UtcNow.ToString('o'); schemaSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
note = Get-WelaProperty $selected 'note' ''; provenance = @($sources); policies = @($rows)
}
}
function Get-WelaEffectiveAuditPolicy {
[CmdletBinding()]
param()
# auditpol /get /r has localized text and no numeric mask column. Query the native API instead.
if (-not ('Wela.AuditProfiles.NativePolicy' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace Wela.AuditProfiles {
public static class NativePolicy {
[StructLayout(LayoutKind.Sequential)]
private struct PolicyInformation {
public Guid Subcategory;
public UInt32 Information;
public Guid Category;
}
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
private static extern bool AuditQuerySystemPolicy(
[In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] Guid[] subcategories,
UInt32 count, out IntPtr information);
[DllImport("advapi32.dll")]
private static extern void AuditFree(IntPtr buffer);
public static Dictionary<string, int> Read(Guid[] subcategories) {
IntPtr buffer = IntPtr.Zero;
try {
if (!AuditQuerySystemPolicy(subcategories, (UInt32)subcategories.Length, out buffer))
throw new Win32Exception(Marshal.GetLastWin32Error(), "AuditQuerySystemPolicy failed");
if (buffer == IntPtr.Zero) throw new InvalidOperationException("Audit policy API returned a null buffer.");
int size = Marshal.SizeOf(typeof(PolicyInformation));
var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < subcategories.Length; i++) {
var policy = (PolicyInformation)Marshal.PtrToStructure(IntPtr.Add(buffer, i * size), typeof(PolicyInformation));
// POLICY_AUDIT_EVENT_NONE = 4; success/failure are bits 1 and 2.
if (policy.Information > 4U) throw new InvalidOperationException("Unrecognized native audit flags.");
result.Add(policy.Subcategory.ToString().ToUpperInvariant(), (int)(policy.Information & 3U));
}
return result;
} finally { if (buffer != IntPtr.Zero) AuditFree(buffer); }
}
}
}
'@ -ErrorAction Stop
}
$catalog = (Import-WelaAuditProfiles).catalog
[guid[]]$guids = @($catalog | ForEach-Object { [guid]$_.guid })
$native = [Wela.AuditProfiles.NativePolicy]::Read($guids)
$current = @{}
foreach ($policy in $catalog) {
if (-not $native.ContainsKey($policy.guid)) { throw "Audit policy API omitted $($policy.id)." }
$current[$policy.guid] = $native[$policy.guid]
}
return $current
}
function Set-WelaEffectiveAuditPolicy {
param([ValidatePattern('^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$')][string]$Guid, [ValidateRange(0, 3)][int]$Mask)
$success = if ($Mask -band 1) { 'enable' } else { 'disable' }
$failure = if ($Mask -band 2) { 'enable' } else { 'disable' }
$output = & auditpol.exe /set "/subcategory:{$Guid}" "/success:$success" "/failure:$failure" 2>&1
if ($LASTEXITCODE -ne 0) { throw "auditpol /set failed ($LASTEXITCODE): $($output -join ' ')" }
}
function Get-WelaHostContext {
[CmdletBinding()]
param()
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
$system = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
if ([int]$os.ProductType -notin @(1, 2, 3) -or [int]$system.DomainRole -notin @(0, 1, 2, 3, 4, 5) -or [int]$os.BuildNumber -le 0) { throw 'Cannot determine a valid Windows role/build.' }
if (([int]$os.ProductType -eq 1 -and [int]$system.DomainRole -notin @(0, 1)) -or
([int]$os.ProductType -eq 2 -and [int]$system.DomainRole -notin @(4, 5)) -or
([int]$os.ProductType -eq 3 -and [int]$system.DomainRole -notin @(2, 3))) { throw 'Windows ProductType and DomainRole disagree.' }
$role = if ([int]$os.ProductType -eq 1) { 'Client' }
elseif ([int]$system.DomainRole -in @(4, 5)) { 'DomainController' }
elseif (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration') { 'ADCS' }
else { 'MemberServer' }
[pscustomobject]@{ Role = $role; Build = [int]$os.BuildNumber }
}
function Assert-WelaAuditProfileTarget {
[CmdletBinding()]
param([Parameter(Mandatory)]$Plan, [Parameter(Mandatory)]$Context, [Parameter(Mandatory)]$Current)
if ($Plan.referenceOnly) { throw 'Windows defaults are a reference, not an apply/restore profile.' }
if ($Context.Role -ne $Plan.role -or $Context.Build -ne $Plan.build) { throw 'Plan role/build does not match the actual Windows host.' }
if ($Current -isnot [hashtable]) { throw 'Effective policy reader did not return a GUID-to-mask map.' }
$selected = @($Plan.policies | Where-Object { $_.mode -in @('exact', 'minimum') -or ($_.mode -eq 'optional' -and $Plan.includeOptional) })
foreach ($policy in $selected) {
if (-not $Current.ContainsKey($policy.guid) -or $null -eq $Current[$policy.guid] -or $Current[$policy.guid] -notin @(0, 1, 2, 3)) { throw "Cannot apply with unknown current policy: $($policy.id). No policies changed." }
}
}
function Invoke-WelaAuditProfilePlan {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]$Plan,
[scriptblock]$ReadPolicy = { Get-WelaEffectiveAuditPolicy },
[scriptblock]$WritePolicy = { param($Guid, $Mask) Set-WelaEffectiveAuditPolicy -Guid $Guid -Mask $Mask },
[scriptblock]$ReadContext = { Get-WelaHostContext }
)
$hostContext = & $ReadContext
$before = & $ReadPolicy
Assert-WelaAuditProfileTarget -Plan $Plan -Context $hostContext -Current $before
$selected = @($Plan.policies | Where-Object { $_.mode -in @('exact', 'minimum') -or ($_.mode -eq 'optional' -and $Plan.includeOptional) })
$results = foreach ($policy in $selected) {
$initial = $before[$policy.guid]; $effective = $initial; $errorText = $null
$target = if ($policy.mode -eq 'minimum') { [int]$initial -bor [int]$policy.requiredMask } else { [int]$policy.requiredMask }
$status = 'No change'
if ($initial -ne $target) {
if ($PSCmdlet.ShouldProcess($policy.id, "Set audit policy to $(Format-WelaAuditMask $target)")) {
try {
& $WritePolicy $policy.guid $target | Out-Null
$verified = & $ReadPolicy
$effective = if ($verified.ContainsKey($policy.guid)) { $verified[$policy.guid] } else { $null }
if ($effective -ne $target) { throw 'Effective policy does not match the requested mask (GPO or command failure).' }
$status = 'Applied'
} catch { $status = 'Failed'; $errorText = $_.Exception.Message; $effective = $null }
} else { $status = 'Skipped' }
}
[pscustomobject]@{ id = $policy.id; guid = $policy.guid; mode = $policy.mode; beforeMask = $initial; targetMask = $target; effectiveMask = $effective; status = $status; error = $errorText }
}
[pscustomobject]@{
profile = $Plan.profile; scope = $Plan.scope; role = $Plan.role; build = $Plan.build
schemaSha256 = $Plan.schemaSha256; provenance = $Plan.provenance
success = (@($results | Where-Object { $_.status -eq 'Failed' }).Count -eq 0)
results = @($results)
}
}
Export-ModuleMember -Function Import-WelaAuditProfiles, Format-WelaAuditMask, Get-WelaAuditProfilePlan, Get-WelaEffectiveAuditPolicy, Set-WelaEffectiveAuditPolicy, Get-WelaHostContext, Assert-WelaAuditProfileTarget, Invoke-WelaAuditProfilePlan
+48 -5
View File
@@ -90,7 +90,7 @@ function Invoke-WelaConfigurationControl {
}
function Complete-WelaConfiguration {
param($Context, [string]$ResultsPath)
param($Context, [string]$ResultsPath, $Plan)
# 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) {
@@ -112,6 +112,14 @@ function Complete-WelaConfiguration {
BackupPath = $Context.BackupPath; Failed = $failed; Skipped = $skipped
Results = @($Context.Results.ToArray())
}
if ($Plan) {
$report | Add-Member NoteProperty Profile $Plan.profile
$report | Add-Member NoteProperty Role $Plan.role
$report | Add-Member NoteProperty Build $Plan.build
$report | Add-Member NoteProperty SchemaSha256 $Plan.schemaSha256
$report | Add-Member NoteProperty Provenance $Plan.provenance
$report | Add-Member NoteProperty Scope $Plan.scope
}
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 }
@@ -232,12 +240,47 @@ function Get-WelaAuditPolicyMask {
}
function Set-WelaAuditPolicyControl {
param($Context, $Policy)
param($Context, $Policy, [ValidateRange(0, 3)][int]$Mask = 3,
[ValidateSet('exact', 'minimum')][string]$Mode = 'exact')
$guid = $Policy.GUID
$read = { Get-WelaAuditPolicyMask -Guid $guid }.GetNewClosure()
$apply = { Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments @('/set', "/subcategory:{$guid}", '/success:enable', '/failure:enable') }.GetNewClosure()
$observed = @{ Mask = $null }
$read = {
$observed.Mask = Get-WelaAuditPolicyMask -Guid $guid
return $observed.Mask
}.GetNewClosure()
$test = {
param($value)
if ($Mode -eq 'minimum') { return ($value -band $Mask) -eq $Mask }
return $value -eq $Mask
}.GetNewClosure()
$apply = {
# Minimum requirements preserve the flags observed immediately before journaling.
$target = if ($Mode -eq 'minimum') { $observed.Mask -bor $Mask } else { $Mask }
$success = if ($target -band 1) { 'enable' } else { 'disable' }
$failure = if ($target -band 2) { 'enable' } else { 'disable' }
Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments @('/set', "/subcategory:{$guid}", "/success:$success", "/failure:$failure")
}.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
-Target @{ Guid = $guid } -Desired @{ Mask = $Mask; Mode = $Mode } -Read $read -Compliant $test -Apply $apply
}
function Set-WelaProfileAuditControls {
param($Context, $Plan)
# The caller must complete Assert-WelaAuditProfileTarget before any mutations.
foreach ($policy in $Plan.policies) {
if ($policy.mode -notin @('exact', 'minimum') -and -not ($policy.mode -eq 'optional' -and $Plan.includeOptional)) { continue }
$mode = if ($policy.mode -eq 'minimum') { 'minimum' } else { 'exact' }
Set-WelaAuditPolicyControl -Context $Context -Policy @{ GUID = $policy.guid; Name = $policy.id } -Mask $policy.requiredMask -Mode $mode
$row = $Context.Results[$Context.Results.Count - 1]
$row | Add-Member NoteProperty Profile $Plan.profile
$row | Add-Member NoteProperty Role $Plan.role
$row | Add-Member NoteProperty Build $Plan.build
$row | Add-Member NoteProperty Mode $policy.mode
$row | Add-Member NoteProperty Prerequisites $policy.prerequisites
$row | Add-Member NoteProperty Evidence $policy.evidence
$row | Add-Member NoteProperty SourceIds $policy.sourceIds
$row | Add-Member NoteProperty Note $policy.note
}
}
function Set-WelaCertificateAuditControl {
+115
View File
@@ -0,0 +1,115 @@
# Deterministic tests: no elevation, Windows policy writes, or Pester dependency.
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force
$script:Checks = 0
function Assert([bool]$Condition, [string]$Message) {
$script:Checks++
if (-not $Condition) { throw "Assertion failed: $Message" }
}
function Assert-Throws([scriptblock]$Action, [string]$Pattern) {
try { & $Action | Out-Null } catch { Assert ($_.Exception.Message -match $Pattern) "Expected '$Pattern', got '$($_.Exception.Message)'"; return }
throw "Expected exception matching '$Pattern'."
}
function Policy($Plan, $Id) { $Plan.policies | Where-Object { $_.id -eq $Id } }
$data = Import-WelaAuditProfiles
Assert ($data.catalog.Count -eq 59) 'all canonical audit subcategories are represented'
$zero = @{}
foreach ($policy in $data.catalog) { $zero[$policy.guid] = 0 }
foreach ($profile in $data.profiles) {
foreach ($range in $profile.appliesTo) {
foreach ($role in $range.roles) {
$plan = Get-WelaAuditProfilePlan -Profile $profile.id -Role $role -Build $range.minBuild -Current $zero
Assert ($plan.policies.Count -eq 59) "$($profile.id)/$role preserves omitted policies explicitly"
Assert ($plan.provenance.Count -gt 0 -and $plan.schemaSha256.Length -eq 64) 'versioned source and schema fingerprints'
}
}
}
$wela = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role Client -Build 26100 -Current $zero
foreach ($id in @('Process Termination', 'RPC Events', 'Detailed File Share', 'Other Policy Change Events')) {
$row = Policy $wela $id
Assert ($row.mode -eq 'exact' -and $row.targetMask -eq 3) "$id recommendation matches existing configure SF policy"
}
Assert ((Policy $wela 'File System').action -eq 'Optional (not selected)') 'optional controls preserve current state by default'
$opt = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role Client -Build 26100 -Current $zero -IncludeOptional
Assert ((Policy $opt 'File System').targetMask -eq 3) 'optional control is explicit opt-in'
Assert ((Policy $opt 'File System').prerequisites -match 'SACL') 'SACL dependency is visible'
Assert ((Policy $wela 'Directory Service Access').mode -eq 'not-applicable') 'DC auditing is role scoped'
$adcs = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role ADCS -Build 20348 -Current $zero
Assert ((Policy $adcs 'Certification Services').targetMask -eq 3) 'CA role is supported'
Assert ((Policy $adcs 'Certification Services').prerequisites -match 'AuditFilter') 'CA prerequisite not silently claimed applied'
$shareGuid = (Policy $wela 'Detailed File Share').guid
$current = $zero.Clone(); $current[$shareGuid] = 1
$cis = Get-WelaAuditProfilePlan -Profile cis-win11-v4-l1 -Role Client -Build 26100 -Current $current
Assert ((Policy $cis 'Detailed File Share').mode -eq 'minimum') 'CIS includes Failure is represented as minimum'
Assert ((Policy $cis 'Detailed File Share').targetMask -eq 3) 'minimum Failure preserves preexisting Success'
$asd = Get-WelaAuditProfilePlan -Profile asd-native-2021-10 -Role Client -Build 26100 -Current $current
Assert ((Policy $asd 'Detailed File Share').mode -eq 'not-configured') 'ASD explicit NC is retained'
Assert ($null -eq (Policy $asd 'Detailed File Share').targetMask) 'NC does not become disabled'
Assert ((Policy $asd 'RPC Events').mode -eq 'unchanged') 'omission is unchanged, not no-auditing'
$unknown = Get-WelaAuditProfilePlan -Profile cis-win11-v4-l1 -Role Client -Build 26100
Assert ($null -eq (Policy $unknown 'Detailed File Share').targetMask -and (Policy $unknown 'Detailed File Share').action -eq 'Unknown') 'unknown current does not become disabled before minimum merge'
Assert-Throws { Get-WelaAuditProfilePlan -Profile microsoft-sct-win11-24h2 -Role Client -Build 26200 } 'does not support'
Assert-Throws { Get-WelaAuditProfilePlan -Profile microsoft-sct-win11-24h2 -Role DomainController -Build 26100 } 'does not support'
Assert-Throws { Get-WelaAuditProfilePlan -Profile typo -Role Client -Build 26100 } 'Unknown audit profile'
# Inject a stateful native boundary, exercising actual selection, merge, verify and failure behavior.
$script:State = $zero.Clone(); $script:Writes = @()
$reader = { return $script:State.Clone() }
$writer = { param($Guid, $Mask) $script:Writes += $Guid; $script:State[$Guid] = $Mask }
$context = { [pscustomobject]@{ Role = 'Client'; Build = 26100 } }
$applied = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false
Assert $applied.success 'apply succeeds after verified effective reads'
Assert ($script:Writes.Count -gt 0) 'selected exact policies were applied'
Assert (@($applied.results | Where-Object { $_.status -eq 'Applied' -and $_.effectiveMask -ne $_.targetMask }).Count -eq 0) 'applied always means verified'
$count = $script:Writes.Count
$again = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false
Assert ($again.success -and $script:Writes.Count -eq $count) 'applying twice is idempotent using fresh current state'
# Apply a stale minimum plan after a preexisting Success flag is introduced: merge fresh state.
$script:State = $zero.Clone(); $script:State[$shareGuid] = 1
$minimum = Invoke-WelaAuditProfilePlan -Plan $cis -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false
Assert ($minimum.success -and $script:State[$shareGuid] -eq 3) 'fresh effective flags are preserved in minimum apply'
$script:State = $zero.Clone()
$failed = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy { throw 'command failed' } -ReadContext $context -Confirm:$false
Assert (-not $failed.success -and @($failed.results | Where-Object { $_.status -eq 'Failed' }).Count -gt 0) 'native failure is machine-readable'
$mismatch = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy { param($Guid, $Mask) } -ReadContext $context -Confirm:$false
Assert (-not $mismatch.success) 'zero exit without effective change does not count as success'
$script:Writes = @()
$whatIf = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -WhatIf
Assert ($script:Writes.Count -eq 0) 'WhatIf never invokes native writer'
Assert-Throws { Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy { @{} } -WritePolicy $writer -ReadContext $context -Confirm:$false } 'unknown current'
Assert ($script:Writes.Count -eq 0) 'unknown preflight refuses all writes'
Assert-Throws { Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext { [pscustomobject]@{ Role = 'DomainController'; Build = 26100 } } } 'actual Windows host'
$defaults = Get-WelaAuditProfilePlan -Profile windows-defaults-reviewed-2026-09 -Role Client -Build 26100 -Current $zero
Assert-Throws { Invoke-WelaAuditProfilePlan -Plan $defaults -ReadPolicy $reader -WritePolicy $writer -ReadContext $context } 'reference'
# Schema rejects bad policy names, duplicate GUIDs, invalid masks/modes, and unknown provenance.
$temp = Join-Path ([System.IO.Path]::GetTempPath()) ('wela-profile-test-' + [guid]::NewGuid().ToString() + '.json')
try {
foreach ($case in @('guid', 'mask', 'mode', 'source', 'unknown')) {
$copy = Get-Content (Join-Path $PSScriptRoot '../config/audit_profiles.json') -Raw | ConvertFrom-Json
switch ($case) {
'guid' { $copy.catalog[1].guid = $copy.catalog[0].guid }
'mask' { $copy.profiles[0].controls.'Process Creation'.mask = 7 }
'mode' { $copy.profiles[0].controls.'Process Creation'.mode = 'invented' }
'source' { $copy.profiles[0].sourceIds = @('unreviewed') }
'unknown' { $copy.profiles[0].controls | Add-Member NoteProperty 'Typo Policy' ([pscustomobject]@{ mode = 'exact'; mask = 3 }) }
}
$copy | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath $temp -Encoding UTF8
Assert-Throws { Import-WelaAuditProfiles -Path $temp } 'Invalid|Unknown|duplicate'
}
} finally { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue }
# Legacy Yamato audit display now takes recommendations from the shared profile, including omitted policies.
. (Join-Path $PSScriptRoot '../WELA.ps1') help -Role Client -Build 26100
function GetAuditpol { return @{} }
$legacy = BuildAuditResult -all_rules @() -Baseline YamatoSecurity -enabledguid @()
foreach ($id in @('Process Termination', 'RPC Events', 'Detailed File Share', 'Other Policy Change Events')) {
$entry = $legacy | Where-Object { $_.SubCategory -eq $id }
Assert ($entry.RecommendedSetting -eq 'Success and Failure [exact]') "legacy audit/settings shares $id recommendation"
}
Assert (@($legacy | Where-Object { $_.Category -like 'Security Advanced*' }).Count -eq 59) 'legacy display includes all canonical GUIDs'
# An unsupported legacy configure target must fail before reaching the old setup body.
function TestWindows { return $true }
function TestAdministrator { return $true }
function Get-WelaHostContext { [pscustomobject]@{ Role = 'Client'; Build = 19045 } }
function Get-WelaEffectiveAuditPolicy { return $zero.Clone() }
function CollectAuditpol { throw 'Reached the old configuration body before profile validation' }
Assert-Throws { ConfigureAuditSettings -Auto } 'does not support'
Write-Host "PASS: $script:Checks audit profile checks; no Windows settings changed."
+15
View File
@@ -0,0 +1,15 @@
# Read-only Windows smoke test: requires administrator or audit-policy query permission.
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force
$current = Get-WelaEffectiveAuditPolicy
$catalog = (Import-WelaAuditProfiles).catalog
if ($current.Count -ne $catalog.Count) { throw "Native API returned $($current.Count) policies; expected $($catalog.Count)." }
foreach ($policy in $catalog) {
if (-not $current.ContainsKey($policy.guid) -or $current[$policy.guid] -notin @(0, 1, 2, 3)) {
throw "Missing/invalid effective state: $($policy.id)"
}
}
$context = Get-WelaHostContext
$plan = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role $context.Role -Build $context.Build -Current $current
Assert-WelaAuditProfileTarget -Plan $plan -Context $context -Current $current
Write-Host "PASS: queried all $($current.Count) effective audit policies on $($context.Role) build $($context.Build); no settings changed."