Private/Get-AuditPolicyTable.ps1
|
<#
.SYNOPSIS Locale-invariant advanced audit policy helpers (private). .DESCRIPTION Get-AuditPolicyTable resolves per-subcategory audit policy settings keyed by subcategory GUID (GUIDs are locale-invariant; canonical English names come from the static map below). Resolution order: 1. AuditQuerySystemPolicy API (advapi32) 2. auditpol /backup CSV (elevated) 3. auditpol /get CSV (English Inclusion text only, non-admin) 4. Unavailable (all Values $null) .NOTES GUID map verified against MS-GPAC and dsccommunity/AuditPolicyDsc ($AuditSubcategoryToGUIDHash, edited from "auditpol /list /subcategory:* /v"). #> $script:AuditSubcategoryMap = @{ 'Security State Change' = '{0CCE9210-69AE-11D9-BED3-505054503030}' 'Security System Extension' = '{0CCE9211-69AE-11D9-BED3-505054503030}' 'System Integrity' = '{0CCE9212-69AE-11D9-BED3-505054503030}' 'IPsec Driver' = '{0CCE9213-69AE-11D9-BED3-505054503030}' 'Other System Events' = '{0CCE9214-69AE-11D9-BED3-505054503030}' 'Logon' = '{0CCE9215-69AE-11D9-BED3-505054503030}' 'Logoff' = '{0CCE9216-69AE-11D9-BED3-505054503030}' 'Account Lockout' = '{0CCE9217-69AE-11D9-BED3-505054503030}' 'IPsec Main Mode' = '{0CCE9218-69AE-11D9-BED3-505054503030}' 'IPsec Quick Mode' = '{0CCE9219-69AE-11D9-BED3-505054503030}' 'IPsec Extended Mode' = '{0CCE921A-69AE-11D9-BED3-505054503030}' 'Special Logon' = '{0CCE921B-69AE-11D9-BED3-505054503030}' 'Other Logon/Logoff Events' = '{0CCE921C-69AE-11D9-BED3-505054503030}' 'Network Policy Server' = '{0CCE9243-69AE-11D9-BED3-505054503030}' 'User / Device Claims' = '{0CCE9247-69AE-11D9-BED3-505054503030}' 'Group Membership' = '{0CCE9249-69AE-11D9-BED3-505054503030}' 'File System' = '{0CCE921D-69AE-11D9-BED3-505054503030}' 'Registry' = '{0CCE921E-69AE-11D9-BED3-505054503030}' 'Kernel Object' = '{0CCE921F-69AE-11D9-BED3-505054503030}' 'SAM' = '{0CCE9220-69AE-11D9-BED3-505054503030}' 'Certification Services' = '{0CCE9221-69AE-11D9-BED3-505054503030}' 'Application Generated' = '{0CCE9222-69AE-11D9-BED3-505054503030}' 'Handle Manipulation' = '{0CCE9223-69AE-11D9-BED3-505054503030}' 'File Share' = '{0CCE9224-69AE-11D9-BED3-505054503030}' 'Filtering Platform Packet Drop' = '{0CCE9225-69AE-11D9-BED3-505054503030}' 'Filtering Platform Connection' = '{0CCE9226-69AE-11D9-BED3-505054503030}' 'Other Object Access Events' = '{0CCE9227-69AE-11D9-BED3-505054503030}' 'Detailed File Share' = '{0CCE9244-69AE-11D9-BED3-505054503030}' 'Removable Storage' = '{0CCE9245-69AE-11D9-BED3-505054503030}' 'Central Policy Staging' = '{0CCE9246-69AE-11D9-BED3-505054503030}' 'Sensitive Privilege Use' = '{0CCE9228-69AE-11D9-BED3-505054503030}' 'Non Sensitive Privilege Use' = '{0CCE9229-69AE-11D9-BED3-505054503030}' 'Other Privilege Use Events' = '{0CCE922A-69AE-11D9-BED3-505054503030}' 'Process Creation' = '{0CCE922B-69AE-11D9-BED3-505054503030}' 'Process Termination' = '{0CCE922C-69AE-11D9-BED3-505054503030}' 'DPAPI Activity' = '{0CCE922D-69AE-11D9-BED3-505054503030}' 'RPC Events' = '{0CCE922E-69AE-11D9-BED3-505054503030}' 'Plug and Play Events' = '{0CCE9248-69AE-11D9-BED3-505054503030}' 'Token Right Adjusted Events' = '{0CCE924A-69AE-11D9-BED3-505054503030}' 'Audit Policy Change' = '{0CCE922F-69AE-11D9-BED3-505054503030}' 'Authentication Policy Change' = '{0CCE9230-69AE-11D9-BED3-505054503030}' 'Authorization Policy Change' = '{0CCE9231-69AE-11D9-BED3-505054503030}' 'MPSSVC Rule-Level Policy Change' = '{0CCE9232-69AE-11D9-BED3-505054503030}' 'Filtering Platform Policy Change' = '{0CCE9233-69AE-11D9-BED3-505054503030}' 'Other Policy Change Events' = '{0CCE9234-69AE-11D9-BED3-505054503030}' 'User Account Management' = '{0CCE9235-69AE-11D9-BED3-505054503030}' 'Computer Account Management' = '{0CCE9236-69AE-11D9-BED3-505054503030}' 'Security Group Management' = '{0CCE9237-69AE-11D9-BED3-505054503030}' 'Distribution Group Management' = '{0CCE9238-69AE-11D9-BED3-505054503030}' 'Application Group Management' = '{0CCE9239-69AE-11D9-BED3-505054503030}' 'Other Account Management Events' = '{0CCE923A-69AE-11D9-BED3-505054503030}' 'Directory Service Access' = '{0CCE923B-69AE-11D9-BED3-505054503030}' 'Directory Service Changes' = '{0CCE923C-69AE-11D9-BED3-505054503030}' 'Directory Service Replication' = '{0CCE923D-69AE-11D9-BED3-505054503030}' 'Detailed Directory Service Replication' = '{0CCE923E-69AE-11D9-BED3-505054503030}' 'Credential Validation' = '{0CCE923F-69AE-11D9-BED3-505054503030}' 'Kerberos Service Ticket Operations' = '{0CCE9240-69AE-11D9-BED3-505054503030}' 'Other Account Logon Events' = '{0CCE9241-69AE-11D9-BED3-505054503030}' 'Kerberos Authentication Service' = '{0CCE9242-69AE-11D9-BED3-505054503030}' } $script:AuditPolicyTableCache = $null function Copy-AuditPolicyTable { <# .SYNOPSIS Clones an audit policy table (new hashtable + shallow-cloned entry objects). .DESCRIPTION Cache entries are PSCustomObjects returned by reference; without a clone a caller mutating .Value/.Source would corrupt the session cache. Entries contain only scalars, so a shallow .PSObject.Copy() per entry suffices. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [hashtable]$Table ) $copy = @{} foreach ($key in $Table.Keys) { $copy[$key] = $Table[$key].PSObject.Copy() } return $copy } function Get-AuditSubcategoryGuid { [CmdletBinding()] [OutputType([string])] param( [Parameter()] [string]$Name ) if ([string]::IsNullOrWhiteSpace($Name)) { return $null } if ($script:AuditSubcategoryMap.ContainsKey($Name)) { return $script:AuditSubcategoryMap[$Name] } foreach ($key in $script:AuditSubcategoryMap.Keys) { if ($key -ieq $Name) { return $script:AuditSubcategoryMap[$key] } } return $null } function ConvertFrom-AuditPolicyBackupCsv { <# .SYNOPSIS Parses an 'auditpol /backup' CSV into a locale-invariant GUID -> value map (private). .DESCRIPTION Returns a hashtable keyed by lowercase subcategory GUID (no braces) with the numeric Setting Value (0-3) for each row. Locale-dependent columns (header row, subcategory names, inclusion text) are never interpreted: the header row is skipped positionally and replaced by fixed English headers (WinLogKit PR #58 / AuditPolicyDsc pattern), rows are matched by GUID and numeric value only (which also drops the Option: rows), and rows are merged per GUID: the System row wins when present, otherwise the per-user row for that GUID is used (per-user-only subcategories are not dropped). Accepts either a file -Path or raw -Lines (first line = header in both cases). #> [CmdletBinding(DefaultParameterSetName = 'Path')] [OutputType([hashtable])] param( [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory = $true, ParameterSetName = 'Lines')] [AllowEmptyCollection()] [AllowEmptyString()] [string[]]$Lines ) $values = @{} $rawLines = @() if ($PSCmdlet.ParameterSetName -eq 'Path') { # Backup file is ANSI/system codepage on localized Windows: read WITHOUT # -Encoding (forcing UTF8 corrupts it, salt#68354). $rawLines = @(Get-Content -LiteralPath $Path) } else { $rawLines = @($Lines) } $dataLines = @($rawLines | Select-Object -Skip 1 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($dataLines.Count -eq 0) { return $values } $rows = @($dataLines | ConvertFrom-Csv -Header 'Machine', 'Target', 'Subcategory', 'Guid', 'Inclusion', 'Exclusion', 'Value') $guidPattern = '^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$' $candidates = @() foreach ($row in $rows) { $rowGuid = '' if ($null -ne $row.Guid) { $rowGuid = ($row.Guid -replace '[{}]', '').Trim() } $rowValue = '' if ($null -ne $row.Value) { $rowValue = $row.Value.Trim() } # GUID/Value filters drop the Option: rows if ($rowGuid -match $guidPattern -and $rowValue -match '^[0-3]$') { $candidates += $row } } # Localized Windows may localize the Target column. # auditpol emits the System row before the per-user rows, but the merge below # is order-independent: per GUID the System row wins when present, otherwise # the per-user row for THAT GUID is used (per-user-only subcategories must # not be dropped just because other GUIDs have System rows). $seen = @{} $seenFromSystem = @{} foreach ($row in $candidates) { $key = (($row.Guid -replace '[{}]', '').Trim()).ToLowerInvariant() $isSystemRow = ($row.Target -eq 'System') if ($seen.ContainsKey($key)) { # First-wins among same-target duplicates; a later System row # overrides an already-recorded per-user row for the same GUID. if ($isSystemRow -and -not $seenFromSystem[$key]) { $values[$key] = [int]($row.Value.Trim()) $seenFromSystem[$key] = $true } continue } $seen[$key] = $true $seenFromSystem[$key] = $isSystemRow $values[$key] = [int]($row.Value.Trim()) } return $values } function Get-AuditPolicyTable { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter()] [string[]]$Subcategory, [Parameter()] [switch]$Force ) $subsetRequested = ($PSBoundParameters.ContainsKey('Subcategory') -and $null -ne $Subcategory -and $Subcategory.Count -gt 0) if (-not $subsetRequested -and -not $Force -and $null -ne $script:AuditPolicyTableCache) { # Clone on read: callers must not be able to mutate the cached entries return (Copy-AuditPolicyTable -Table $script:AuditPolicyTableCache) } if ($subsetRequested) { $names = @($Subcategory) } else { $names = @($script:AuditSubcategoryMap.Keys) } # Table keyed by lowercase GUID without braces $guidToName = @{} foreach ($name in $names) { $guid = Get-AuditSubcategoryGuid -Name $name if ($null -eq $guid) { Write-Verbose "Get-AuditPolicyTable: unknown subcategory name '$name' (skipped)" continue } $key = ($guid -replace '[{}]', '').ToLowerInvariant() $guidToName[$key] = $name } $table = @{} foreach ($key in $guidToName.Keys) { $table[$key] = [PSCustomObject]@{ Guid = ('{' + $key.ToUpperInvariant() + '}') Name = $guidToName[$key] Value = $null Success = $false Failure = $false Source = 'Unavailable' } } $isWindowsHost = $false try { $isWindowsHost = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( [System.Runtime.InteropServices.OSPlatform]::Windows) } catch { # RuntimeInformation missing on old .NET Framework (PS 5.1) $isWindowsHost = ($env:OS -eq 'Windows_NT') } if (-not $isWindowsHost) { Write-Verbose 'Get-AuditPolicyTable: not a Windows host; audit policy not applicable' if (-not $subsetRequested) { $script:AuditPolicyTableCache = Copy-AuditPolicyTable -Table $table } return $table } $resolved = $false $auditpol = $null # --- Strategy 1: AuditQuerySystemPolicy API (HardeningLens pattern) --- if (-not ([bool]('AuditPolicyNative' -as [type]))) { $auditPolicyNativeSource = @' using System; using System.Runtime.InteropServices; public static class AuditPolicyNative { [StructLayout(LayoutKind.Sequential)] public struct AUDIT_POLICY_INFORMATION { public Guid AuditSubCategoryGuid; public UInt32 AuditingInformation; public Guid AuditCategoryGuid; } [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.U1)] private static extern bool AuditQuerySystemPolicy([In] Guid[] pSubCategoryGuids, UInt32 PolicyCount, out IntPtr ppAuditPolicy); [DllImport("advapi32.dll")] private static extern void AuditFree(IntPtr buffer); public static AUDIT_POLICY_INFORMATION[] QueryPolicies(Guid[] guids, out int win32Error) { win32Error = 0; IntPtr buffer = IntPtr.Zero; if (!AuditQuerySystemPolicy(guids, (UInt32)guids.Length, out buffer)) { win32Error = Marshal.GetLastWin32Error(); return null; } try { AUDIT_POLICY_INFORMATION[] result = new AUDIT_POLICY_INFORMATION[guids.Length]; IntPtr current = buffer; int size = Marshal.SizeOf(typeof(AUDIT_POLICY_INFORMATION)); for (int i = 0; i < guids.Length; i++) { result[i] = (AUDIT_POLICY_INFORMATION)Marshal.PtrToStructure(current, typeof(AUDIT_POLICY_INFORMATION)); current = new IntPtr(current.ToInt64() + size); } return result; } finally { AuditFree(buffer); } } } '@ try { Add-Type -TypeDefinition $auditPolicyNativeSource -ErrorAction Stop } catch { # Add-Type -TypeDefinition is blocked under Constrained Language Mode (JEA) Write-Verbose "Get-AuditPolicyTable: Add-Type AuditPolicyNative failed: $($_.Exception.Message)" } } if ([bool]('AuditPolicyNative' -as [type])) { try { $guidList = New-Object 'System.Collections.Generic.List[guid]' foreach ($key in $guidToName.Keys) { $guidList.Add([guid]$key) } $win32Error = 0 $policies = [AuditPolicyNative]::QueryPolicies($guidList.ToArray(), [ref]$win32Error) if ($null -ne $policies) { for ($i = 0; $i -lt $policies.Count; $i++) { $key = $policies[$i].AuditSubCategoryGuid.ToString() if (-not $table.ContainsKey($key)) { continue } # POLICY_AUDIT_EVENT_SUCCESS=1, FAILURE=2, NONE=4; 4 and 0 both mean No Auditing $raw = [int]$policies[$i].AuditingInformation $success = [bool]($raw -band 1) $failure = [bool]($raw -band 2) $value = 0 if ($success) { $value += 1 } if ($failure) { $value += 2 } $entry = $table[$key] $entry.Value = $value $entry.Success = $success $entry.Failure = $failure $entry.Source = 'Api' $resolved = $true } } else { # 5 = ACCESS_DENIED, 1314 = PRIVILEGE_NOT_HELD: fall through to next strategy Write-Verbose "Get-AuditPolicyTable: AuditQuerySystemPolicy failed (win32 $win32Error)" } } catch { Write-Verbose "Get-AuditPolicyTable: API strategy failed: $($_.Exception.Message)" } } # --- Strategy 2: auditpol /backup CSV (elevated) --- if (-not $resolved) { $auditpol = Get-Command 'auditpol.exe' -ErrorAction SilentlyContinue if ($null -eq $auditpol) { $auditpol = Get-Command 'auditpol' -ErrorAction SilentlyContinue } if ($null -ne $auditpol) { $tempFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('wsa-audit-{0}.csv' -f [guid]::NewGuid().ToString('N')) try { $null = & $auditpol.Source '/backup' "/file:$tempFile" 2>&1 if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $tempFile)) { $backupValues = ConvertFrom-AuditPolicyBackupCsv -Path $tempFile foreach ($key in $backupValues.Keys) { if (-not $table.ContainsKey($key)) { continue } $value = $backupValues[$key] $entry = $table[$key] $entry.Value = $value $entry.Success = [bool]($value -band 1) $entry.Failure = [bool]($value -band 2) $entry.Source = 'BackupCsv' $resolved = $true } } else { Write-Verbose "Get-AuditPolicyTable: auditpol /backup failed (exit $LASTEXITCODE); elevation required" } } catch { Write-Verbose "Get-AuditPolicyTable: backup CSV strategy failed: $($_.Exception.Message)" } finally { if (Test-Path -LiteralPath $tempFile -ErrorAction SilentlyContinue) { Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue } } } } # --- Strategy 3: auditpol /get CSV (non-admin, English text only) --- if (-not $resolved -and $null -ne $auditpol) { try { $output = @(& $auditpol.Source '/get' '/category:*' '/r' 2>$null) if ($LASTEXITCODE -eq 0 -and $output.Count -gt 1) { $lines = @($output | Select-Object -Skip 1 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) $rows = @($lines | ConvertFrom-Csv -Header 'Machine', 'Target', 'Subcategory', 'Guid', 'Inclusion', 'Exclusion') foreach ($row in $rows) { if ($null -eq $row.Guid) { continue } $key = (($row.Guid -replace '[{}]', '').Trim()).ToLowerInvariant() if (-not $table.ContainsKey($key)) { continue } $inclusion = '' if ($null -ne $row.Inclusion) { $inclusion = $row.Inclusion.Trim() } # Interpret ONLY canonical English text; localized text stays honestly unknown $value = $null if ($inclusion -ieq 'Success and Failure') { $value = 3 } elseif ($inclusion -ieq 'Success') { $value = 1 } elseif ($inclusion -ieq 'Failure') { $value = 2 } elseif ($inclusion -ieq 'No Auditing') { $value = 0 } $entry = $table[$key] if ($null -ne $value) { $entry.Value = $value $entry.Success = [bool]($value -band 1) $entry.Failure = [bool]($value -band 2) } $entry.Source = 'GetQuery' $resolved = $true } } else { Write-Verbose "Get-AuditPolicyTable: auditpol /get failed (exit $LASTEXITCODE)" } } catch { Write-Verbose "Get-AuditPolicyTable: get CSV strategy failed: $($_.Exception.Message)" } } if (-not $resolved) { Write-Verbose 'Get-AuditPolicyTable: all resolution strategies failed; returning Unavailable table' } if (-not $subsetRequested) { # Cache keeps its own copy so caller mutations of the returned table # cannot corrupt the session cache $script:AuditPolicyTableCache = Copy-AuditPolicyTable -Table $table } return $table } |