mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-08 07:15:25 +02:00
Integrate versioned audit profiles with verified configuration
# Conflicts: # .github/workflows/release.yml # WELA.ps1
This commit is contained in:
commit
d480db5a76
10 files changed
+5578
-46
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
Reference in new issue
Block a user