Add source-specific Windows audit privilege and integrity controls (#412)

* Add opt-in source-profile audit integrity controls

* Reference PR 412 in audit-integrity changelogs
This commit is contained in:
田中ザック Isaac Mathis authored and GitHub committed 2026-09-20 14:03:18 +09:00
1 parent 55cc427c61
commit 7719063f6f
13 files changed
+772 -3

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
# Explicit source-profile audit integrity. Direct assignments are not token membership.
function Initialize-WelaIntegrityNative {
if (-not ('Wela.AuditIntegrityNative' -as [type])) {
Add-Type -Path (Join-Path $PSScriptRoot 'AuditIntegrityNative.cs') -ErrorAction Stop
}
}
function Get-WelaIntegrityHolders {
param([ValidateSet('SeAuditPrivilege','SeSecurityPrivilege')][string]$Right)
Initialize-WelaIntegrityNative
return ,@([Wela.AuditIntegrityNative]::Holders($Right))
}
function Get-WelaIntegrityAccountRights {
param([string]$Sid)
Initialize-WelaIntegrityNative
return ,@([Wela.AuditIntegrityNative]::Rights($Sid))
}
function Set-WelaIntegrityAccountRight {
param([string]$Sid,[ValidateSet('SeAuditPrivilege','SeSecurityPrivilege')][string]$Right,[bool]$Grant)
Initialize-WelaIntegrityNative
[Wela.AuditIntegrityNative]::Change($Sid,$Right,$Grant)
}
function Get-WelaIntegrityHost {
if ($env:OS -ne 'Windows_NT') { throw 'Audit integrity requires Windows.' }
$os=Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
$computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
if ($os.ProductType -notin @(1,2,3) -or $computer.DomainRole -notin @(0,1,2,3,4,5) -or $null -eq $computer.PartOfDomain -or [string]$os.BuildNumber -notmatch '^\d+$') { throw 'Unknown Windows role/build.' }
$role=$null
if ($os.ProductType -eq 1 -and $computer.DomainRole -in @(0,1)) { $role='Client' }
elseif ($os.ProductType -eq 2 -and $computer.DomainRole -in @(4,5) -and $computer.PartOfDomain) { $role='DomainController' }
elseif ($os.ProductType -eq 3 -and $computer.DomainRole -eq 3 -and $computer.PartOfDomain) { $role='MemberServer' }
elseif ($os.ProductType -eq 3 -and $computer.DomainRole -eq 2 -and -not $computer.PartOfDomain) { $role='StandaloneServer' }
if (-not $role -or ($computer.DomainRole -eq 1 -and -not $computer.PartOfDomain) -or ($computer.DomainRole -eq 0 -and $computer.PartOfDomain)) { throw 'Conflicting Windows role observations.' }
if (-not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell for this workflow.' }
$ca='Unknown'
try { $ca=if (@(Get-CimInstance Win32_Service -Filter "Name='CertSvc'" -ErrorAction Stop).Count) { 'Installed' } else { 'NotInstalled' } } catch { }
[pscustomobject]@{Status='Known';ComputerName=[string]$computer.Name;Role=$role;Build=[int]$os.BuildNumber;DomainJoined=[bool]$computer.PartOfDomain;DomainRole=[int]$computer.DomainRole;CertificateAuthority=$ca}
}
function Get-WelaIntegritySnapshot {
param([string[]]$ObserveSids=@())
$errors=@();$hostState=$null;$rights=@();$accounts=@();$crash=$null
try { $hostState=Get-WelaIntegrityHost } catch { $hostState=[pscustomobject]@{Status='Unknown';Diagnostic=$_.Exception.Message};$errors+=$_.Exception.Message }
if ($hostState.Status -eq 'Known') {
foreach ($right in @('SeAuditPrivilege','SeSecurityPrivilege')) {
try { $holders=@(Get-WelaIntegrityHolders $right | ForEach-Object {$_} | Sort-Object -Unique);$rights+=[pscustomobject]@{Name=$right;Holders=$holders;Status='Known'} }
catch { $rights+=[pscustomobject]@{Name=$right;Holders=@();Status='Unknown'};$errors+="$right : $($_.Exception.Message)" }
}
$sids=@(@('S-1-5-19','S-1-5-20','S-1-5-32-544') + @($rights | ForEach-Object {$_.Holders}) + @($ObserveSids) | Sort-Object -Unique)
foreach ($sid in $sids) {
try { $assigned=@(Get-WelaIntegrityAccountRights $sid | ForEach-Object {$_} | Sort-Object -Unique);$accounts+=[pscustomobject]@{Sid=$sid;Rights=$assigned;Status='Known'} }
catch { $accounts+=[pscustomobject]@{Sid=$sid;Rights=@();Status='Unknown'};$errors+="$sid : $($_.Exception.Message)" }
}
if (-not $errors.Count) {
foreach ($right in $rights) {
$fromAccounts=@($accounts | Where-Object { $_.Rights -contains $right.Name } | ForEach-Object {$_.Sid} | Sort-Object -Unique)
if (($right.Holders -join '|') -cne ($fromAccounts -join '|')) { $errors+='LSA holder and per-account observations differ; retry a consistent snapshot.' }
}
}
try { $crash=Get-WelaRegistryState -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name CrashOnAuditFail }
catch { $errors+="CrashOnAuditFail : $($_.Exception.Message)" }
}
[pscustomobject]@{Host=$hostState;Rights=$rights;Accounts=$accounts;CrashOnAuditFail=$crash;Errors=$errors}
}
function Get-WelaIntegrityProfiles {
$catalog=Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/audit_integrity_profiles.json') -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if ($catalog.SchemaVersion -ne 1) { throw 'Unsupported integrity profile catalog.' }
return @($catalog.Profiles)
}
function Get-WelaIntegrityStateKey {
param($Snapshot)
ConvertTo-Json -InputObject $Snapshot -Depth 14 -Compress
}
function Get-WelaIntegrityPrincipal {
param([string]$Sid)
$name=switch ($Sid) { 'S-1-5-19' {'LOCAL SERVICE'} 'S-1-5-20' {'NETWORK SERVICE'} 'S-1-5-32-544' {'BUILTIN\Administrators'} default {$null} }
if (-not $name -and $env:OS -eq 'Windows_NT') { try { $name=([Security.Principal.SecurityIdentifier]::new($Sid)).Translate([Security.Principal.NTAccount]).Value } catch { } }
[pscustomobject]@{Sid=$Sid;Name=$name;NameStatus=$(if ($name) {'Resolved'} else {'Unresolved; SID remains authoritative'})}
}
function Get-WelaIntegrityPlan {
param($Snapshot,[string]$Profile,[switch]$AllowPrivilegeRemoval)
$source=$null;$blockers=@($Snapshot.Errors);$operations=@();$rows=@()
if ($Profile) {
$matches=@(Get-WelaIntegrityProfiles | Where-Object Id -eq $Profile)
if ($matches.Count -ne 1) { throw "Unknown audit integrity profile: $Profile" }
$source=$matches[0]
if ($Snapshot.Host.Status -ne 'Known' -or $source.Role -ne $Snapshot.Host.Role -or $Snapshot.Host.Build -lt $source.MinBuild -or $Snapshot.Host.Build -gt $source.MaxBuild) { $blockers+='Selected source profile does not match the actual Windows role/build.' }
}
foreach ($right in @('SeAuditPrivilege','SeSecurityPrivilege')) {
$observed=@($Snapshot.Rights | Where-Object Name -eq $right)
$current=@(if ($observed.Count -eq 1) { $observed[0].Holders })
$desired=$null; if ($source -and $null -ne $source.$right) { $desired=@($source.$right | Sort-Object -Unique) }
$adds=@();$removes=@();$mode='Preserve'
if ($null -ne $desired) {
$mode='Exact';$adds=@($desired | Where-Object { $current -notcontains $_ });$removes=@($current | Where-Object { $desired -notcontains $_ })
if ($removes.Count -and -not $AllowPrivilegeRemoval) { $blockers+="$right has extra principals; inspect service/dependency exceptions and explicitly use -AllowPrivilegeRemoval before revoking them." }
foreach ($sid in $adds) { $operations+=[pscustomobject]@{Kind='Right';Right=$right;Sid=$sid;Grant=$true} }
foreach ($sid in $removes) { $operations+=[pscustomobject]@{Kind='Right';Right=$right;Sid=$sid;Grant=$false} }
}
$rows+=[pscustomobject]@{Name=$right;Mode=$mode;SourceSetting=$(if (-not $source) {'NoSourceSelected'} elseif ($null -eq $desired) {'OmittedBySource'} else {'ExplicitRequirement'});ObservedStatus=$(if ($observed.Count) {$observed[0].Status} else {'Unknown'});Current=$current;Desired=$desired;Add=@($adds|ForEach-Object {Get-WelaIntegrityPrincipal $_});Remove=@($removes|ForEach-Object {Get-WelaIntegrityPrincipal $_})}
}
$crash=$Snapshot.CrashOnAuditFail;$crashStatus='Unknown'
if ($crash -and $crash.KeyExists) {
if (-not $crash.ValueExists) { $crashStatus='Absent; no explicit value observed' }
elseif ($crash.Type -eq 'DWord' -and $crash.Value -in @(0,1)) { $crashStatus=if ($crash.Value -eq 0) {'Disabled'} else {'Enabled'} }
elseif ($crash.Type -eq 'DWord' -and $crash.Value -eq 2) { $crashStatus='RecoveryRequired';$blockers+='CrashOnAuditFail=2 is a recovery state. This workflow never resets it or clears the Security log.' }
else { $blockers+='Unknown CrashOnAuditFail type/value; preserve it for manual review.' }
} else { $blockers+='LSA registry key/state could not be verified.' }
$crashMode='Preserve';$crashDesired=$null
if ($source -and $null -ne $source.CrashOnAuditFail) {
if ($source.CrashOnAuditFail -ne 0) { throw 'Only reviewed disabled CrashOnAuditFail profiles are supported.' }
$crashMode='Exact';$crashDesired=0
if ($crashStatus -ne 'Disabled') { $operations+=[pscustomobject]@{Kind='Registry';Name='CrashOnAuditFail';Desired=0} }
}
$rows+=[pscustomobject]@{Name='CrashOnAuditFail';Mode=$crashMode;SourceSetting=$(if (-not $source) {'NoSourceSelected'} elseif ($null -eq $crashDesired) {'OmittedBySource'} else {'ExplicitRequirement'});ObservedStatus=$crashStatus;Current=$crash;Desired=$crashDesired;Add=@();Remove=@()}
[pscustomobject]@{Profile=$source;Before=$Snapshot;Controls=$rows;Operations=$operations;Blockers=@($blockers|Select-Object -Unique);AllowPrivilegeRemoval=[bool]$AllowPrivilegeRemoval;
Exceptions=@('IIS application pools can require SeAuditPrivilege.','AD FS service identities can require SeAuditPrivilege.','Exchange Servers can require SeSecurityPrivilege on DCs.','Other application dependencies must be reviewed before any removal.');
VerificationScope='Local LSA direct assignments and typed registry readback only. Existing tokens, GPO persistence, benign event generation, ingestion and effective user/group access are not verified.'}
}
function Copy-WelaIntegrityExpected {
param($Snapshot)
Get-WelaIntegrityStateKey $Snapshot | ConvertFrom-Json
}
function Update-WelaIntegrityExpected {
param($Snapshot,$Operation)
if ($Operation.Kind -eq 'Registry') {
$Snapshot.CrashOnAuditFail.ValueExists=$true;$Snapshot.CrashOnAuditFail.Value=0;$Snapshot.CrashOnAuditFail.Type='DWord'
} else {
$right=@($Snapshot.Rights|Where-Object Name -eq $Operation.Right)[0]
$account=@($Snapshot.Accounts|Where-Object Sid -eq $Operation.Sid)[0]
if (-not $right -or -not $account) { throw 'Missing affected principal in the recovery snapshot.' }
if ($Operation.Grant) { $right.Holders=@(@($right.Holders)+$Operation.Sid|Sort-Object -Unique);$account.Rights=@(@($account.Rights)+$Operation.Right|Sort-Object -Unique) }
else { $right.Holders=@($right.Holders|Where-Object {$_ -ne $Operation.Sid});$account.Rights=@($account.Rights|Where-Object {$_ -ne $Operation.Right}) }
}
}
function Set-WelaIntegrityControls {
param($Context,$Plan)
$expected=Copy-WelaIntegrityExpected $Plan.Before
foreach ($operation in $Plan.Operations) { Update-WelaIntegrityExpected $expected $operation }
$state=@{Plan=$Plan;Expected=$expected;ObserveSids=@($Plan.Before.Accounts.Sid)}
$read={param($s) Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids}
$test={param($snapshot,$s) -not $s.Plan.Blockers.Count -and (Get-WelaIntegrityStateKey $snapshot) -ceq (Get-WelaIntegrityStateKey $s.Expected)}
$apply={
param($s)
if ($s.Plan.Blockers.Count) { throw ($s.Plan.Blockers -join ' ') }
$expected=Copy-WelaIntegrityExpected $s.Plan.Before
if ((Get-WelaIntegrityStateKey (Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids)) -cne (Get-WelaIntegrityStateKey $expected)) { throw 'Audit-integrity state changed since planning; no write was sent.' }
foreach ($operation in $s.Plan.Operations) {
$fresh=Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids
if ((Get-WelaIntegrityStateKey $fresh) -cne (Get-WelaIntegrityStateKey $expected)) { throw 'Audit-integrity state changed before the next write; remaining operations stopped.' }
if ($operation.Kind -eq 'Right') { Set-WelaIntegrityAccountRight -Sid $operation.Sid -Right $operation.Right -Grant $operation.Grant }
else { Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name CrashOnAuditFail -Value 0 -Type DWord -ErrorAction Stop }
Update-WelaIntegrityExpected $expected $operation
$after=Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids
if ((Get-WelaIntegrityStateKey $after) -cne (Get-WelaIntegrityStateKey $expected)) { throw 'Audit-integrity write/readback or unrelated-right preservation did not verify; remaining operations stopped. Review the recovery journal.' }
}
}
Invoke-WelaConfigurationControl -Context $Context -Id 'AuditIntegrity/LocalPolicy' -Kind 'AuditIntegritySet' -Target @('SeAuditPrivilege','SeSecurityPrivilege','CrashOnAuditFail') -Desired $Plan `
-Read $read -Compliant $test -Apply $apply -CallbackState $state -Description 'Apply the exact listed local privilege additions/removals and selected CrashOnAuditFail policy. Review every affected SID and service exception first.'
}
function Invoke-WelaIntegrityCommand {
param([ValidateSet('Audit','Plan','Configure')][string]$Action='Audit',[string]$Profile,[switch]$AllowPrivilegeRemoval,[switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath)
if ($Action -ne 'Audit' -and -not $Profile) { throw 'Plan and Configure require an explicit -IntegrityProfile.' }
if ($AllowPrivilegeRemoval -and (-not $Profile -or $Action -eq 'Audit')) { throw '-AllowPrivilegeRemoval requires an explicit profile with Plan or Configure.' }
if ($DryRun -and $Action -ne 'Configure') { throw '-DryRun requires IntegrityAction Configure; Audit and Plan are read-only.' }
$snapshot=Get-WelaIntegritySnapshot
$plan=Get-WelaIntegrityPlan -Snapshot $snapshot -Profile $Profile -AllowPrivilegeRemoval:$AllowPrivilegeRemoval
$report=[pscustomobject]@{Scope='audit-integrity-local-policy-only';ExitCode=$(if ($plan.Blockers.Count) {1} else {0});Action=$Action;Plan=$plan}
# Show complete affected principals before the shared runner asks for consent.
Write-Host "Observed host: $($snapshot.Host.ComputerName); role/build: $($snapshot.Host.Role)/$($snapshot.Host.Build); source profile: $Profile"
Write-Host 'Plan observations (before any configuration):'
foreach ($row in $plan.Controls) {
Write-Host "$($row.Name): $($row.Mode); $($row.SourceSetting); $($row.ObservedStatus)"
Write-Host (' Current: ' + (ConvertTo-Json -InputObject $row.Current -Depth 4 -Compress))
if ($row.Mode -eq 'Exact') { Write-Host (' Requested: ' + (ConvertTo-Json -InputObject $row.Desired -Compress)) }
foreach ($principal in $row.Add) { Write-Host " ADD: $($principal.Sid) ($($principal.Name))" }
foreach ($principal in $row.Remove) { Write-Host " REMOVE: $($principal.Sid) ($($principal.Name))" }
}
if (@($plan.Controls.Remove).Count) { Write-Host ($plan.Exceptions -join ' ') }
foreach ($blocker in $plan.Blockers) { Write-Host "Blocked: $blocker" -ForegroundColor Yellow }
Write-Host $plan.VerificationScope
if ($Action -eq 'Configure') {
# Refuse unresolved scope/recovery/removal decisions even before creating a journal directory.
if ($plan.Blockers.Count) { throw ($plan.Blockers -join ' ') }
$context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
Set-WelaIntegrityControls -Context $context -Plan $plan
$report=Complete-WelaConfiguration -Context $context -Scope 'audit-integrity-local-policy-only' -SuccessMessage 'Audit-integrity configuration completed. Applied/AlreadyCompliant rows verify local settings; skipped rows do not. Token, GPO and event evidence remain separate checks.'
$report|Add-Member NoteProperty Action $Action
$report|Add-Member NoteProperty Plan $plan
}
$report|Add-Member NoteProperty SigmaEvtxCredit 0
if ($ResultsPath) { $report|ConvertTo-Json -Depth 18|Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
return $report
}
+59
View File
@@ -0,0 +1,59 @@
// Local LSA policy only. No remote target, account deletion or all-rights replacement.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;
namespace Wela {
public static class AuditIntegrityNative {
[StructLayout(LayoutKind.Sequential)] struct LSA_UNICODE_STRING { public ushort Length; public ushort MaximumLength; public IntPtr Buffer; }
[StructLayout(LayoutKind.Sequential)] struct LSA_OBJECT_ATTRIBUTES { public int Length; public IntPtr RootDirectory; public IntPtr ObjectName; public uint Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; }
[DllImport("advapi32.dll")] static extern uint LsaOpenPolicy(IntPtr system, ref LSA_OBJECT_ATTRIBUTES attributes, uint access, out IntPtr handle);
[DllImport("advapi32.dll")] static extern uint LsaClose(IntPtr handle);
[DllImport("advapi32.dll")] static extern uint LsaFreeMemory(IntPtr memory);
[DllImport("advapi32.dll")] static extern uint LsaNtStatusToWinError(uint status);
[DllImport("advapi32.dll")] static extern uint LsaEnumerateAccountsWithUserRight(IntPtr handle, ref LSA_UNICODE_STRING right, out IntPtr buffer, out uint count);
[DllImport("advapi32.dll")] static extern uint LsaEnumerateAccountRights(IntPtr handle, byte[] sid, out IntPtr buffer, out uint count);
[DllImport("advapi32.dll")] static extern uint LsaAddAccountRights(IntPtr handle, byte[] sid, [In] LSA_UNICODE_STRING[] rights, uint count);
[DllImport("advapi32.dll")] static extern uint LsaRemoveAccountRights(IntPtr handle, byte[] sid, [MarshalAs(UnmanagedType.U1)] bool allRights, [In] LSA_UNICODE_STRING[] rights, uint count);
static void Check(uint status) { if (status != 0) throw new Win32Exception((int)LsaNtStatusToWinError(status), "Local LSA operation failed (NTSTATUS 0x" + status.ToString("X8") + ")."); }
static void CheckRight(string right) { if (right != "SeAuditPrivilege" && right != "SeSecurityPrivilege") throw new ArgumentException("Only the two audit-integrity rights are supported."); }
static IntPtr Open(bool write) {
LSA_OBJECT_ATTRIBUTES attributes = new LSA_OBJECT_ATTRIBUTES(); attributes.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
IntPtr handle; Check(LsaOpenPolicy(IntPtr.Zero, ref attributes, 0x00000801u | (write ? 0x00000010u : 0u), out handle)); return handle;
}
static LSA_UNICODE_STRING Text(string value) {
LSA_UNICODE_STRING text = new LSA_UNICODE_STRING(); text.Buffer = Marshal.StringToHGlobalUni(value);
text.Length = checked((ushort)(value.Length * 2)); text.MaximumLength = checked((ushort)(text.Length + 2)); return text;
}
static byte[] Sid(string value) { SecurityIdentifier sid = new SecurityIdentifier(value); byte[] bytes = new byte[sid.BinaryLength]; sid.GetBinaryForm(bytes,0); return bytes; }
public static string[] Holders(string right) {
CheckRight(right); IntPtr handle = Open(false); IntPtr buffer = IntPtr.Zero; LSA_UNICODE_STRING text = Text(right);
try {
uint count; uint status = LsaEnumerateAccountsWithUserRight(handle, ref text, out buffer, out count);
if (status == 0x8000001Au) return new string[0]; // STATUS_NO_MORE_ENTRIES, documented empty assignment.
Check(status); List<string> result = new List<string>();
for (uint i=0; i<count; i++) result.Add(new SecurityIdentifier(Marshal.ReadIntPtr(buffer, checked((int)i * IntPtr.Size))).Value);
result.Sort(StringComparer.Ordinal); return result.ToArray();
} finally { try { if (buffer != IntPtr.Zero) Check(LsaFreeMemory(buffer)); } finally { Marshal.FreeHGlobal(text.Buffer); Check(LsaClose(handle)); } }
}
public static string[] Rights(string sid) {
byte[] bytes = Sid(sid); IntPtr handle = Open(false); IntPtr buffer = IntPtr.Zero;
try {
uint count; uint status = LsaEnumerateAccountRights(handle, bytes, out buffer, out count);
if (status == 0xC0000034u) return new string[0]; // STATUS_OBJECT_NAME_NOT_FOUND: no LSA account rights.
Check(status); int size = Marshal.SizeOf(typeof(LSA_UNICODE_STRING)); List<string> result = new List<string>();
for (uint i=0; i<count; i++) { LSA_UNICODE_STRING text = (LSA_UNICODE_STRING)Marshal.PtrToStructure(IntPtr.Add(buffer, checked((int)i * size)), typeof(LSA_UNICODE_STRING)); result.Add(Marshal.PtrToStringUni(text.Buffer, text.Length/2)); }
result.Sort(StringComparer.Ordinal); return result.ToArray();
} finally { try { if (buffer != IntPtr.Zero) Check(LsaFreeMemory(buffer)); } finally { Check(LsaClose(handle)); } }
}
public static void Change(string sid, string right, bool grant) {
CheckRight(right); byte[] bytes = Sid(sid); IntPtr handle = Open(true); LSA_UNICODE_STRING text = Text(right);
try {
LSA_UNICODE_STRING[] rights = new LSA_UNICODE_STRING[] {text};
// AllRights is always false; only one named privilege is changed.
Check(grant ? LsaAddAccountRights(handle,bytes,rights,1) : LsaRemoveAccountRights(handle,bytes,false,rights,1));
} finally { Marshal.FreeHGlobal(text.Buffer); Check(LsaClose(handle)); }
}
}
}
+1 -1
View File
@@ -94,7 +94,7 @@ function Invoke-WelaConfigurationControl {
function Complete-WelaConfiguration {
param($Context, [string]$ResultsPath, $Plan,
[ValidateSet("native-windows-configuration", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only", "smb-audit-policies-only", "native-channel-settings-only", "wmi-namespace-sacl-only", "ad-object-sacl-only", "windows-powershell-transcription-policy-only", "wef-source-configuration-only", "wec-collector-subscriptions-only")]
[ValidateSet("native-windows-configuration", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only", "smb-audit-policies-only", "native-channel-settings-only", "wmi-namespace-sacl-only", "ad-object-sacl-only", "windows-powershell-transcription-policy-only", "wef-source-configuration-only", "wec-collector-subscriptions-only", "audit-integrity-local-policy-only")]
[string]$Scope = "native-windows-configuration",
[string]$SuccessMessage = 'Configuration completed; all requested controls verified.')
# A second read detects a value that was compliant earlier but changed during