mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-08 07:15:25 +02:00
Add opt-in WMI namespace audit SACL workflow
This commit is contained in:
1 parent
385f367ae5
commit
80db17891d
12 files changed
+644
-3
No files matched your search
@@ -0,0 +1,32 @@
|
||||
name: WMI namespace auditing regressions
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
paths:
|
||||
- 'WELA.ps1'
|
||||
- 'scripts/Configuration.ps1'
|
||||
- 'scripts/WmiNamespaceAuditing.ps1'
|
||||
- 'tests/WmiNamespaceAuditing*'
|
||||
- 'tests/fixtures/wmi-namespace-descriptor.json'
|
||||
- '.github/workflows/wmi-namespace-auditing.yml'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
wmi-namespace-auditing:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Mocked namespace SACL regression tests (Windows PowerShell 5.1)
|
||||
shell: powershell
|
||||
run: ./tests/WmiNamespaceAuditing.Tests.ps1
|
||||
- name: Native read-only and in-memory writer adapter (Windows PowerShell 5.1)
|
||||
shell: powershell
|
||||
run: ./tests/WmiNamespaceAuditing.Windows.Tests.ps1
|
||||
- name: Mocked namespace SACL regression tests (PowerShell 7)
|
||||
shell: pwsh
|
||||
run: ./tests/WmiNamespaceAuditing.Tests.ps1
|
||||
- name: Native read-only and in-memory writer adapter (PowerShell 7)
|
||||
shell: pwsh
|
||||
run: ./tests/WmiNamespaceAuditing.Windows.Tests.ps1
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
**改善:**
|
||||
|
||||
- ASDのガイドに基づく任意実行のWMI名前空間SACL監査・計画・設定を追加した。ローカル名前空間の明示的な選択と、子名前空間への継承の個別指定に対応する。完全なセキュリティ記述子の記録、特権・戻り値の確認、書き込み前の変更検出と読み戻し検証により、既存のアクセス権と未知の監査エントリを保持する。イベント生成と転送の検証は別途必要となる。 (#372) (@Shirofune-Security)
|
||||
- ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (#394) (@Shirofune-Security)
|
||||
- イベントログのサイズ監査と設定に共通のバイト単位プロファイルを導入し、AppLocker・ファイアウォールログの256 MiB、Setupの32 MiB、ASD推奨のSecurityログ2048 MiBに対応した。`-LogProfile`と`configure-eventlogs`で送信元と収集サーバーのサイズ・保存方式を選択できる。明示的に指定しない限り、既存の大きいバッファと保存方式は維持する。結果には検証した設定を記録し、未測定の保存日数は不明と表示する。 (#396) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
**Improvements:**
|
||||
|
||||
- Added opt-in WMI namespace SACL audit, plan and configure actions based on ASD guidance, with explicit local namespace selection and separate descendant-inheritance consent. Full descriptor journals, privilege/return-code checks, race guards and read-back verification preserve existing permissions and unknown audit entries; event generation and forwarding remain separate lab validation. (#372) (@Shirofune-Security)
|
||||
- Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (#394) (@Shirofune-Security)
|
||||
- Unified event-log size auditing and configuration with shared byte-based profiles, including 256 MiB AppLocker/firewall logs, 32 MiB Setup and ASD 2048 MiB Security. Added separate source and collector size/mode choices through `-LogProfile` and `configure-eventlogs`; larger buffers and existing retention modes are preserved unless explicitly changed. Results include verified state and unknown retention duration. (#396) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
[ValidateRange(16384, 32767)][int]$FirewallMinimumSizeKiB = 16384,
|
||||
[string]$HtmlPath,
|
||||
[ValidateSet('Audit', 'Plan', 'Configure')][string]$SmbAction = 'Audit',
|
||||
[ValidateSet('List', 'Audit', 'Plan', 'Configure')][string]$WmiAction = 'List',
|
||||
[string[]]$WmiNamespace,
|
||||
[switch]$WmiIncludeChildren,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
@@ -38,6 +41,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
|
||||
. (Join-Path $ScriptRoot "scripts/Configuration.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/WmiNamespaceAuditing.ps1")
|
||||
Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop
|
||||
Import-Module (Join-Path $ScriptRoot "modules/NativeProviders.psm1") -ErrorAction Stop
|
||||
Import-Module (Join-Path $ScriptRoot "modules/EventLogSettings.psm1") -ErrorAction Stop
|
||||
@@ -1666,6 +1670,10 @@ function Get-WelaUserProfiles {
|
||||
|
||||
$usage = @"
|
||||
Usage:
|
||||
./WELA.ps1 wmi-auditing -WmiAction List
|
||||
./WELA.ps1 wmi-auditing -WmiAction Plan -WmiNamespace root\cimv2 -ResultsPath wmi-plan.json
|
||||
./WELA.ps1 wmi-auditing -WmiAction Configure -WmiNamespace root\cimv2 -DryRun
|
||||
# Namespace SACLs are opt-in; descendants require -WmiIncludeChildren. See docs/wmi-namespace-auditing.md.
|
||||
./WELA.ps1 firewall-logging -FirewallAction Audit -ResultsPath firewall.json
|
||||
./WELA.ps1 firewall-logging -FirewallAction Plan -FirewallPathMode CisV4
|
||||
./WELA.ps1 firewall-logging -FirewallAction Configure -DryRun
|
||||
@@ -1705,8 +1713,12 @@ Write-Host ""
|
||||
# Reject unsupported dry-run requests before reaching any command's mutation path.
|
||||
if ($DryRun -and $Cmd -notin @('configure', 'configure-eventlogs') -and
|
||||
-not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure') -and
|
||||
-not ($Cmd -eq 'smb-auditing' -and $SmbAction -eq 'Configure')) {
|
||||
throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs, firewall-logging -FirewallAction Configure and smb-auditing -SmbAction Configure. No command was run."
|
||||
-not ($Cmd -eq 'smb-auditing' -and $SmbAction -eq 'Configure') -and
|
||||
-not ($Cmd -eq 'wmi-auditing' -and $WmiAction -eq 'Configure')) {
|
||||
throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs, firewall-logging -FirewallAction Configure smb-auditing -SmbAction Configure and wmi-auditing -WmiAction Configure. No command was run."
|
||||
}
|
||||
if (($WmiNamespace -or $WmiIncludeChildren -or $PSBoundParameters.ContainsKey('WmiAction')) -and $Cmd -ne 'wmi-auditing') {
|
||||
throw '-WmiAction, -WmiNamespace and -WmiIncludeChildren require wmi-auditing. No command was run.'
|
||||
}
|
||||
if ($Profile -and $Cmd -in @('eventlog-profiles', 'audit-filesize', 'configure-eventlogs')) {
|
||||
throw '-Profile selects advanced audit policy only. Use -LogProfile for event-log size/mode settings.'
|
||||
@@ -1724,6 +1736,19 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi
|
||||
}
|
||||
|
||||
switch ($Cmd.ToLower()) {
|
||||
'wmi-auditing' {
|
||||
if ($Help) {
|
||||
Write-Host 'Usage: ./WELA.ps1 wmi-auditing -WmiAction List|Audit|Plan|Configure [-WmiNamespace root\cimv2,root\subscription] [-WmiIncludeChildren] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath file.json]'
|
||||
Write-Host 'Select exact local namespaces explicitly. Default action List is read-only. Configure appends ASD success audit ACEs; descendant inheritance requires an explicit switch. No access permissions, audit policy or forwarding changes.'
|
||||
return
|
||||
}
|
||||
if ($Profile -or $Baseline) { throw 'wmi-auditing uses its own namespace selections, not -Profile or -Baseline.' }
|
||||
try {
|
||||
$report = Invoke-WelaWmiAuditCommand -Action $WmiAction -Namespace $WmiNamespace -IncludeChildren:$WmiIncludeChildren -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath
|
||||
$report
|
||||
if ($report.ExitCode) { exit $report.ExitCode }
|
||||
} catch { Write-Host "[Failed] WMI namespace auditing: $_" -ForegroundColor Red; exit 1 }
|
||||
}
|
||||
'firewall-logging' {
|
||||
if ($Help) {
|
||||
Write-Host 'Usage: ./WELA.ps1 firewall-logging [-FirewallAction Audit|Plan|Configure] [-FirewallPathMode Preserve|CisV4] [-FirewallMinimumSizeKiB 16384..32767] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath file.json]'
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Optional WMI namespace auditing
|
||||
|
||||
`wmi-auditing` appends reviewed success-audit entries to explicitly selected **local** WMI namespace SACLs. It does not run during ordinary `configure`, change namespace access permissions, create namespaces, enable remote WMI access, change audit policy, install a forwarding subscription, or grant rule-coverage credit. PowerShell 5.1 and PowerShell 7 on Windows use the same `System.Management` provider methods.
|
||||
|
||||
```powershell
|
||||
./WELA.ps1 wmi-auditing -WmiAction List
|
||||
./WELA.ps1 wmi-auditing -WmiAction Audit -WmiNamespace 'root\cimv2' -ResultsPath wmi-before.json
|
||||
./WELA.ps1 wmi-auditing -WmiAction Plan -WmiNamespace 'root\cimv2','root\subscription' -ResultsPath wmi-plan.json
|
||||
./WELA.ps1 wmi-auditing -WmiAction Configure -WmiNamespace 'root\cimv2' -DryRun
|
||||
./WELA.ps1 wmi-auditing -WmiAction Configure -WmiNamespace 'root\cimv2' -BackupPath C:\WelaBackups\wmi-change-001 -ResultsPath wmi-result.json
|
||||
# Explicitly opt in to the reference script's descendant inheritance:
|
||||
./WELA.ps1 wmi-auditing -WmiAction Configure -WmiNamespace 'root\subscription' -WmiIncludeChildren
|
||||
```
|
||||
|
||||
The default action is read-only `List`. Audit/Plan/Configure require exact namespace selections; wildcards, remote paths, unreviewed namespaces and empty selections are rejected. `-Auto` skips per-namespace confirmation after the operator has selected the scope. `-DryRun` is supported only with Configure, and calls no setter or journal writer. `-Profile` and `-Baseline` do not select WMI SACLs.
|
||||
|
||||
## Reference entries and scope
|
||||
|
||||
The entries come from the [ASD WMI script pinned at 59041b5](https://github.com/AustralianCyberSecurityCentre/windows_event_logging/blob/59041b5d4586789a751171fb752be1624ad5e3b4/events/wmi_auditing/wmi_auditing.ps1). Every new ACE has type 2 (system audit), success only. Existing failure entries and unfamiliar ACEs are retained.
|
||||
|
||||
| Namespace | Principal | Mask | Audited namespace rights | ASD flags |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `root\cimv2` | Everyone `S-1-1-0` | `0x40002` (262146) | Execute Methods, Edit Security | 64 |
|
||||
| `root\cimv2` | Interactive `S-1-5-4` | `0x1` | Enable Account / read | 64 |
|
||||
| `root\cimv2` | Network `S-1-5-2` | `0x1` | Enable Account / read | 64 |
|
||||
| `root\cimv2` | Batch `S-1-5-3` | `0x1` | Enable Account / read | 64 |
|
||||
| `root\SecurityCenter` | Everyone | `0x40001` (262145) | Enable Account / read, Edit Security | 66 |
|
||||
| `root\SecurityCenter2` | Everyone | `0x40001` (262145) | Enable Account / read, Edit Security | 66 |
|
||||
| `root\subscription` | Everyone | `0x4001E` (262174) | Execute Methods, Full Write, Partial Write, Provider Write, Edit Security | 66 |
|
||||
| `root\default` | Everyone | `0x4001F` (262175) | Read plus all preceding rights | 66 |
|
||||
|
||||
The numeric subscription mask includes Execute Methods even though the reference script's comment omits it. WELA uses the actual numeric mask. Flags 64 mean success on this namespace; 66 add container inheritance. **By default WELA uses 64 for every selection**, limiting new entries to that namespace. `-WmiIncludeChildren` enables the reference's flag 66 for the four applicable namespaces. This may propagate audit ACEs to inheriting descendants, including existing and future child namespaces; it does not grant access. Child ACL propagation is not enumerated, backed up or verified by this command, and is an explicit additional scope requiring a lab review. Existing inherited entries are retained in either mode. SecurityCenter namespaces are commonly absent on servers; absence is reported rather than treated as successful configuration.
|
||||
|
||||
## Privileges, preservation and results
|
||||
|
||||
Run elevated with **SeSecurityPrivilege assigned** for Audit/Plan/Configure. WELA enables this privilege in its process while accessing the descriptor, restores the previous token state afterward, and requests privileges for the local WMI connection. Without it a provider can return a DACL while omitting the SACL; WELA refuses that ambiguous read. List only enumerates the supported root child namespaces and reports Present, NotInstalled or Unknown.
|
||||
|
||||
Each GetSecurityDescriptor and SetSecurityDescriptor return code must be explicitly zero. Exceptions, denied/missing namespaces, incomplete descriptors, nonzero return codes, ineffective writes and failed read-back are failures. The journal stores the complete provider descriptor as JSON and MOF strings before the setter is called; nested entries cannot be truncated by the outer result serializer. Native objects are cloned rather than rebuilt from a shortened permission list. Existing ACEs, duplicate/unknown ACEs, DACL order, owner, group and other descriptor fields are preserved. The only control flag added is `SE_SACL_PRESENT` when needed. Provider representations that cannot round-trip unchanged fail verification; unknown entries are never deliberately simplified or discarded.
|
||||
|
||||
Immediately before writing, WELA reads the full descriptor again and refuses to overwrite a changed snapshot. Read-back checks all original fields/ACE multiplicities and every requested exact audit entry. The final check detects descriptor drift after verification. This is not an atomic transaction with other administrators or management software: changes between the last read and the provider write remain possible. No automatic rollback overwrites concurrent changes.
|
||||
|
||||
An exact existing entry is not duplicated. Different masks, audit outcomes, inheritance, object-specific ACEs or inherited ACEs are preserved and do not suppress the explicit requested entry. Result statuses use the shared configuration contract: Applied/AlreadyCompliant indicate observed SACL compliance, Skipped includes dry-run or declined changes, and Failed/Overridden produce exit code 1. Exit code 0 alone is not evidence of a write or successful event generation.
|
||||
|
||||
## Audit prerequisites and local/remote evidence
|
||||
|
||||
WELA separately observes the effective **Other Object Access Events** audit policy (success bit) without changing it. A missing/unknown prerequisite is visible in `Prerequisite`; a successful SACL update alone does not establish event readiness. Review audit precedence and the effective policy using the separate audit-policy workflow.
|
||||
|
||||
[Microsoft documents namespace auditing](https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces) as Security event **4662** for matching namespace access checks. It does not establish whether the subsequent provider operation succeeded. Interactive/Network/Batch SIDs select token membership, not a universal local/remote classification: validate the logon type, user SID, namespace and access mask in observed XML. Remote Enable (`0x20`) is not added to the DACL or the new audit mask. WMI-Activity/Operational telemetry is a separate evidence source and is not made equivalent to namespace Security events.
|
||||
|
||||
## Recovery and remaining lab verification
|
||||
|
||||
Keep the new backup directory and result JSON outside temporary folders. `before.jsonl` contains each selected namespace's original `DescriptorJson` and `DescriptorMof`, namespace name and proposed entries. Compare these with a fresh Audit export before making any recovery change. In an elevated WMI Control (`wmimgmt.msc`), select the exact namespace, Security > Advanced > Auditing, and remove only entries that this run added after confirming they were absent from the original descriptor. Restore changed audit flags/masks from the original export if necessary; retain unrelated owner/group/DACL and newer administrative changes. WELA deliberately provides no blind whole-descriptor restore. An existing matching ACE was not created by this run and must not be removed. If descendant inheritance was enabled, inspect affected child namespaces independently and use a pre-change machine snapshot if a complete rollback is needed.
|
||||
|
||||
CI uses synthetic descriptors, a real privileged read of root\cimv2, an in-memory native writer adapter and a read-only dry-run on Windows PowerShell 5.1/7. It never sends SetSecurityDescriptor to a live namespace. **Live SACL writes, benign local/remote event generation, child propagation and forwarding have not been verified by these tests.** Before deployment, use isolated patched snapshots of Windows 11, member server, domain controller and AD CS hosts; record descriptors/effective audit policy before and after, repeat configuration for idempotence, issue benign local and remote calls with known tokens, capture Security 4662 XML, and test the chosen WEF subscription and collector receipt. Validate namespace `ObjectName` and access masks, not EventID alone. These are pending acceptance labs, not claimed Sigma uplift.
|
||||
|
||||
Additional primary references: [SetSecurityDescriptor and preservation flags](https://learn.microsoft.com/en-us/windows/win32/wmisdk/setsecuritydescriptor-method-in-class---systemsecurity), [namespace access masks](https://learn.microsoft.com/en-us/windows/win32/wmisdk/namespace-access-rights-constants), [namespace inheritance flags](https://learn.microsoft.com/en-us/windows/win32/wmisdk/namespace-ace-flag-constants).
|
||||
@@ -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")]
|
||||
[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", "wmi-namespace-sacl-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
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
# Opt-in local namespace SACLs. No namespace DACL, audit policy, or remote-access changes.
|
||||
function Get-WelaWmiAuditDefinitions {
|
||||
param([string[]]$Namespace, [switch]$IncludeChildren)
|
||||
$source = 'https://github.com/AustralianCyberSecurityCentre/windows_event_logging/blob/59041b5d4586789a751171fb752be1624ad5e3b4/events/wmi_auditing/wmi_auditing.ps1'
|
||||
$rows = @(
|
||||
@('root\cimv2', 262146, 64, 'S-1-1-0'),
|
||||
@('root\cimv2', 1, 64, 'S-1-5-4'),
|
||||
@('root\cimv2', 1, 64, 'S-1-5-2'),
|
||||
@('root\cimv2', 1, 64, 'S-1-5-3'),
|
||||
@('root\SecurityCenter', 262145, 66, 'S-1-1-0'),
|
||||
@('root\SecurityCenter2', 262145, 66, 'S-1-1-0'),
|
||||
@('root\subscription', 262174, 66, 'S-1-1-0'),
|
||||
@('root\default', 262175, 66, 'S-1-1-0')
|
||||
)
|
||||
foreach ($selected in $Namespace) {
|
||||
if ($selected -notin @($rows | ForEach-Object { $_[0] })) { throw "Unsupported namespace '$selected'. Select exact local namespaces listed by wmi-auditing -WmiAction List; wildcards and remote paths are not accepted." }
|
||||
}
|
||||
foreach ($row in $rows) {
|
||||
if ($Namespace -and $row[0] -notin $Namespace) { continue }
|
||||
[pscustomobject][ordered]@{ Namespace = $row[0]; AccessMask = [uint32]$row[1]; AceType = 2
|
||||
AceFlags = $(if ($IncludeChildren) { [uint32]$row[2] } else { [uint32]64 }); Sid = $row[3]
|
||||
SourceAceFlags = $row[2]; Source = $source; AuditOutcome = 'Success'
|
||||
Scope = $(if ($IncludeChildren -and $row[2] -eq 66) { 'Selected namespace and inheriting descendants' } else { 'Selected namespace only' }) }
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-WelaWmiInterop {
|
||||
if ($env:OS -ne 'Windows_NT') { throw 'WMI namespace security requires Windows.' }
|
||||
Add-Type -AssemblyName System.Management -ErrorAction Stop
|
||||
if ('Wela.WmiSecurityPrivilege' -as [type]) { return }
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace Wela {
|
||||
public sealed class WmiSecurityPrivilege : IDisposable {
|
||||
[StructLayout(LayoutKind.Sequential)] struct Luid { public uint Low; public int High; }
|
||||
[StructLayout(LayoutKind.Sequential)] struct TokenPrivileges { public uint Count; public Luid Luid; public uint Attributes; }
|
||||
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
||||
[DllImport("kernel32.dll", SetLastError=true)] static extern bool CloseHandle(IntPtr handle);
|
||||
[DllImport("advapi32.dll", SetLastError=true)] static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token);
|
||||
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool LookupPrivilegeValue(string system, string name, out Luid luid);
|
||||
[DllImport("advapi32.dll", SetLastError=true)] static extern bool AdjustTokenPrivileges(IntPtr token, bool disable, ref TokenPrivileges current, uint size, out TokenPrivileges previous, out uint required);
|
||||
IntPtr token; TokenPrivileges previous; bool changed;
|
||||
public WmiSecurityPrivilege() {
|
||||
if (!OpenProcessToken(GetCurrentProcess(), 0x28, out token)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try {
|
||||
Luid luid;
|
||||
if (!LookupPrivilegeValue(null, "SeSecurityPrivilege", out luid)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
TokenPrivileges requested = new TokenPrivileges { Count=1, Luid=luid, Attributes=2 };
|
||||
uint required;
|
||||
bool ok = AdjustTokenPrivileges(token, false, ref requested, (uint)Marshal.SizeOf(typeof(TokenPrivileges)), out previous, out required);
|
||||
int error = Marshal.GetLastWin32Error();
|
||||
if (!ok || error != 0) throw new Win32Exception(error, "SeSecurityPrivilege must be assigned and enabled; refusing a potentially incomplete SACL read.");
|
||||
changed=true;
|
||||
} catch { CloseHandle(token); token=IntPtr.Zero; throw; }
|
||||
}
|
||||
public void Dispose() {
|
||||
if (token==IntPtr.Zero) return;
|
||||
try {
|
||||
if (changed) {
|
||||
TokenPrivileges ignored; uint required;
|
||||
if (!AdjustTokenPrivileges(token, false, ref previous, (uint)Marshal.SizeOf(typeof(TokenPrivileges)), out ignored, out required)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
} finally { CloseHandle(token); token=IntPtr.Zero; }
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ -ErrorAction Stop
|
||||
}
|
||||
|
||||
function Assert-WelaWmiReturnCode {
|
||||
param($Response, [string]$Method)
|
||||
if ($null -eq $Response -or $null -eq $Response.ReturnValue -or
|
||||
$Response.ReturnValue -is [bool] -or [string]$Response.ReturnValue -notmatch '^\d+$' -or
|
||||
[uint64]$Response.ReturnValue -ne 0) {
|
||||
throw "$Method failed (ReturnValue=$($Response.ReturnValue)); success requires an explicit numeric zero."
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertTo-WelaWmiData {
|
||||
param($Value)
|
||||
if ($null -eq $Value) { return $null }
|
||||
if ($Value -is [System.Management.ManagementBaseObject]) {
|
||||
$properties = [ordered]@{}
|
||||
foreach ($property in @($Value.Properties | Sort-Object Name)) { $properties[$property.Name] = ConvertTo-WelaWmiData $property.Value }
|
||||
return [pscustomobject]$properties
|
||||
}
|
||||
if ($Value -is [array]) {
|
||||
$items = @(); foreach ($item in $Value) { $items += ,(ConvertTo-WelaWmiData $item) }
|
||||
return ,$items
|
||||
}
|
||||
return $Value
|
||||
}
|
||||
|
||||
function ConvertTo-WelaWmiJson { param($Value) ConvertTo-Json -InputObject $Value -Depth 40 -Compress }
|
||||
|
||||
function Get-WelaWmiSid {
|
||||
param($Trustee)
|
||||
if ($Trustee.SIDString) { return [string]$Trustee.SIDString }
|
||||
$bytes = [byte[]]$Trustee.SID
|
||||
if (-not $bytes -or $bytes.Length -lt 8 -or $bytes.Length -ne (8 + 4 * $bytes[1])) { return '' }
|
||||
[uint64]$authority = 0
|
||||
for ($i = 2; $i -lt 8; $i++) { $authority = ($authority * 256) + $bytes[$i] }
|
||||
$sid = "S-$($bytes[0])-$authority"
|
||||
for ($i = 0; $i -lt $bytes[1]; $i++) { $sid += '-' + [BitConverter]::ToUInt32($bytes, 8 + 4 * $i) }
|
||||
return $sid
|
||||
}
|
||||
|
||||
function Test-WelaWmiAceMatch {
|
||||
param($Ace, $Definition)
|
||||
# Only an exact, explicit, ordinary success ACE satisfies a requested entry.
|
||||
# Unknown/object/inherited ACEs are retained without interpreting them.
|
||||
return $null -ne $Ace -and $Ace.AceType -eq 2 -and $Ace.AceFlags -eq $Definition.AceFlags -and
|
||||
$Ace.AccessMask -eq $Definition.AccessMask -and -not $Ace.GuidObjectType -and -not $Ace.GuidInheritedObjectType -and
|
||||
(Get-WelaWmiSid $Ace.Trustee) -eq $Definition.Sid
|
||||
}
|
||||
|
||||
function Get-WelaWmiMissingAces {
|
||||
param($Descriptor, [array]$Definitions)
|
||||
foreach ($definition in $Definitions) {
|
||||
$matches = @($Descriptor.SACL | Where-Object { Test-WelaWmiAceMatch $_ $definition })
|
||||
if ($matches.Count -eq 0) { $definition }
|
||||
}
|
||||
}
|
||||
|
||||
function New-WelaWmiConnection {
|
||||
param([string]$Namespace)
|
||||
$options = New-Object System.Management.ConnectionOptions
|
||||
$options.EnablePrivileges = $true
|
||||
$options.Impersonation = [System.Management.ImpersonationLevel]::Impersonate
|
||||
$scope = New-Object System.Management.ManagementScope -ArgumentList "\\.\$Namespace", $options
|
||||
$scope.Connect()
|
||||
$path = New-Object System.Management.ManagementPath -ArgumentList '__SystemSecurity=@'
|
||||
return New-Object System.Management.ManagementObject -ArgumentList $scope, $path, $null
|
||||
}
|
||||
|
||||
function Get-WelaWmiNativeDescriptor {
|
||||
param($Connection)
|
||||
$result = $Connection.InvokeMethod('GetSecurityDescriptor', $null, $null)
|
||||
Assert-WelaWmiReturnCode $result 'GetSecurityDescriptor'
|
||||
if ($null -eq $result.Descriptor -or $null -eq $result.Descriptor.ControlFlags) { throw 'GetSecurityDescriptor returned no complete descriptor.' }
|
||||
return $result.Descriptor
|
||||
}
|
||||
|
||||
function Get-WelaWmiNamespaceSnapshot {
|
||||
param([string]$Namespace)
|
||||
Initialize-WelaWmiInterop
|
||||
$privilege = New-Object Wela.WmiSecurityPrivilege
|
||||
$connection = $null
|
||||
try {
|
||||
$connection = New-WelaWmiConnection $Namespace
|
||||
$descriptor = Get-WelaWmiNativeDescriptor $connection
|
||||
$data = ConvertTo-WelaWmiData $descriptor
|
||||
# Strings prevent JSON journal depth truncation of nested, unfamiliar ACEs.
|
||||
[pscustomobject]@{ Namespace = $Namespace; DescriptorJson = ConvertTo-WelaWmiJson $data
|
||||
DescriptorMof = $descriptor.GetText([System.Management.TextFormat]::Mof); SaclReadPrivilege = 'SeSecurityPrivilege enabled' }
|
||||
} finally { if ($connection) { $connection.Dispose() }; $privilege.Dispose() }
|
||||
}
|
||||
|
||||
function Set-WelaWmiNamespaceDescriptor {
|
||||
param([string]$Namespace, [string]$ExpectedJson, [array]$Definitions)
|
||||
Initialize-WelaWmiInterop
|
||||
$privilege = New-Object Wela.WmiSecurityPrivilege
|
||||
$connection = $null
|
||||
try {
|
||||
$connection = New-WelaWmiConnection $Namespace
|
||||
$descriptor = Get-WelaWmiNativeDescriptor $connection
|
||||
$data = ConvertTo-WelaWmiData $descriptor
|
||||
if ((ConvertTo-WelaWmiJson $data) -cne $ExpectedJson) { throw 'Namespace descriptor changed after its recovery snapshot; no SACL was written. Review and retry.' }
|
||||
$missing = @(Get-WelaWmiMissingAces $data $Definitions)
|
||||
if (-not $missing.Count) { return 'Requested audit ACEs already present at the immediate pre-write read.' }
|
||||
# Clone the full native descriptor; existing native ACE objects are not
|
||||
# reconstructed from selected fields, merged, reordered, or removed.
|
||||
$updated = $descriptor.Clone()
|
||||
$aces = @($descriptor.SACL | Where-Object { $null -ne $_ })
|
||||
foreach ($definition in $missing) {
|
||||
$aceClass = New-Object System.Management.ManagementClass -ArgumentList '\\.\root\cimv2:Win32_ACE'
|
||||
$trusteeClass = New-Object System.Management.ManagementClass -ArgumentList '\\.\root\cimv2:Win32_Trustee'
|
||||
try {
|
||||
$ace = $aceClass.CreateInstance(); $trustee = $trusteeClass.CreateInstance()
|
||||
$sid = New-Object System.Security.Principal.SecurityIdentifier -ArgumentList $definition.Sid
|
||||
$sidBytes = New-Object byte[] $sid.BinaryLength; $sid.GetBinaryForm($sidBytes, 0)
|
||||
$trustee.SID = $sidBytes
|
||||
$ace.Trustee = $trustee; $ace.AccessMask = [uint32]$definition.AccessMask
|
||||
$ace.AceFlags = [uint32]$definition.AceFlags; $ace.AceType = [uint32]2
|
||||
$aces += $ace
|
||||
} finally { $aceClass.Dispose(); $trusteeClass.Dispose() }
|
||||
}
|
||||
$updated.SACL = [System.Management.ManagementBaseObject[]]$aces
|
||||
# Only SE_SACL_PRESENT is added when absent. Every other control bit stays.
|
||||
$updated.ControlFlags = [uint32]$descriptor.ControlFlags -bor [uint32]16
|
||||
$parameters = $connection.GetMethodParameters('SetSecurityDescriptor')
|
||||
$parameters.Descriptor = $updated
|
||||
$response = $connection.InvokeMethod('SetSecurityDescriptor', $parameters, $null)
|
||||
Assert-WelaWmiReturnCode $response 'SetSecurityDescriptor'
|
||||
'SACL update accepted; full descriptor preservation and audit entries require read-back verification. Event generation is unverified.'
|
||||
} finally { if ($connection) { $connection.Dispose() }; $privilege.Dispose() }
|
||||
}
|
||||
|
||||
function Test-WelaWmiDescriptorPreserved {
|
||||
param($Before, $After)
|
||||
foreach ($property in $Before.PSObject.Properties) {
|
||||
if ($property.Name -eq 'SACL') { continue }
|
||||
if ($property.Name -eq 'ControlFlags') {
|
||||
if ([uint32]$After.ControlFlags -ne ([uint32]$Before.ControlFlags -bor 16)) { return $false }
|
||||
} elseif ((ConvertTo-WelaWmiJson $property.Value) -cne (ConvertTo-WelaWmiJson $After.($property.Name))) { return $false }
|
||||
}
|
||||
# Compare a multiset: providers can reorder a SACL, but cannot remove/change
|
||||
# any original entry, including unknown types, trustee details or extra fields.
|
||||
$remaining = New-Object 'System.Collections.Generic.List[string]'
|
||||
foreach ($ace in @($After.SACL)) { if ($null -ne $ace) { $remaining.Add((ConvertTo-WelaWmiJson $ace)) } }
|
||||
foreach ($ace in @($Before.SACL)) {
|
||||
if ($null -eq $ace) { continue }
|
||||
if (-not $remaining.Remove((ConvertTo-WelaWmiJson $ace))) { return $false }
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-WelaWmiNamespaceInventory {
|
||||
$namespaces = @(Get-WelaWmiAuditDefinitions | Select-Object -ExpandProperty Namespace -Unique)
|
||||
try {
|
||||
$children = @(Get-CimInstance -Namespace root -ClassName __Namespace -ErrorAction Stop | ForEach-Object { 'root\' + $_.Name })
|
||||
foreach ($namespace in $namespaces) { [pscustomobject]@{ Namespace = $namespace; State = $(if ($namespace -in $children) { 'Present' } else { 'NotInstalled' }) } }
|
||||
} catch {
|
||||
foreach ($namespace in $namespaces) { [pscustomobject]@{ Namespace = $namespace; State = 'Unknown'; Diagnostic = $_.Exception.Message } }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-WelaWmiAuditPrerequisite {
|
||||
try {
|
||||
$mask = Get-WelaNativeAuditPolicy -Guid '0CCE9227-69AE-11D9-BED3-505054503030'
|
||||
[pscustomobject]@{ Policy = 'Other Object Access Events'; Mask = $mask; SuccessEnabled = (($mask -band 1) -eq 1); State = 'Observed' }
|
||||
} catch { [pscustomobject]@{ Policy = 'Other Object Access Events'; Mask = $null; SuccessEnabled = $null; State = 'Unknown'; Diagnostic = $_.Exception.Message } }
|
||||
}
|
||||
|
||||
function Get-WelaWmiAuditPlan {
|
||||
param([string[]]$Namespace, [switch]$IncludeChildren)
|
||||
if (-not $Namespace.Count) { throw 'Select at least one exact namespace with -WmiNamespace; there is no implicit all-namespaces configuration.' }
|
||||
$definitions = @(Get-WelaWmiAuditDefinitions -Namespace $Namespace -IncludeChildren:$IncludeChildren)
|
||||
foreach ($name in @($definitions | Select-Object -ExpandProperty Namespace -Unique)) {
|
||||
$selected = @($definitions | Where-Object Namespace -eq $name)
|
||||
try {
|
||||
$snapshot = Get-WelaWmiNamespaceSnapshot $name
|
||||
$descriptor = $snapshot.DescriptorJson | ConvertFrom-Json
|
||||
$missing = @(Get-WelaWmiMissingAces $descriptor $selected)
|
||||
[pscustomobject]@{ Namespace = $name; Status = $(if ($missing.Count) { 'ChangeRequired' } else { 'AlreadyCompliant' }); Before = $snapshot; Definitions = $selected; Missing = $missing; Diagnostic = '' }
|
||||
} catch { [pscustomobject]@{ Namespace = $name; Status = 'Unknown'; Before = $null; Definitions = $selected; Missing = @(); Diagnostic = $_.Exception.Message } }
|
||||
}
|
||||
}
|
||||
|
||||
function Set-WelaWmiAuditControls {
|
||||
param($Context, [array]$Plan)
|
||||
foreach ($entry in $Plan) {
|
||||
$callback = @{ Namespace = $entry.Namespace; Definitions = $entry.Definitions; Original = $null; ExpectedJson = $null; Applied = $false; VerifiedJson = $null }
|
||||
$read = {
|
||||
param($state)
|
||||
$snapshot = Get-WelaWmiNamespaceSnapshot $state.Namespace
|
||||
if ($null -eq $state.Original) { $state.Original = $snapshot.DescriptorJson | ConvertFrom-Json; $state.ExpectedJson = $snapshot.DescriptorJson }
|
||||
return $snapshot
|
||||
}
|
||||
$test = {
|
||||
param($snapshot, $state)
|
||||
$descriptor = $snapshot.DescriptorJson | ConvertFrom-Json
|
||||
if (@(Get-WelaWmiMissingAces $descriptor $state.Definitions).Count) { return $false }
|
||||
if ($state.Applied) {
|
||||
if (-not (Test-WelaWmiDescriptorPreserved $state.Original $descriptor)) { return $false }
|
||||
if ($null -eq $state.VerifiedJson) { $state.VerifiedJson = $snapshot.DescriptorJson }
|
||||
return $snapshot.DescriptorJson -ceq $state.VerifiedJson
|
||||
}
|
||||
# An already compliant descriptor still gets a full final drift check.
|
||||
return $snapshot.DescriptorJson -ceq $state.ExpectedJson
|
||||
}
|
||||
$apply = {
|
||||
param($state)
|
||||
Set-WelaWmiNamespaceDescriptor -Namespace $state.Namespace -ExpectedJson $state.ExpectedJson -Definitions $state.Definitions
|
||||
$state.Applied = $true
|
||||
}
|
||||
Invoke-WelaConfigurationControl -Context $Context -Id "WmiNamespace/$($entry.Namespace)/SACL" -Kind WmiNamespaceSacl `
|
||||
-Target @{ Namespace = $entry.Namespace; Computer = 'Local'; Operation = 'Append audit ACEs only' } -Desired $entry.Definitions `
|
||||
-Read $read -Compliant $test -Apply $apply -CallbackState $callback `
|
||||
-Description ('Append missing success audit ACEs. Scope: ' + (($entry.Definitions.Scope | Select-Object -Unique) -join ', '))
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-WelaWmiAuditCommand {
|
||||
param([ValidateSet('List', 'Audit', 'Plan', 'Configure')][string]$Action = 'List', [string[]]$Namespace,
|
||||
[switch]$IncludeChildren, [switch]$Auto, [switch]$DryRun, [string]$BackupPath, [string]$ResultsPath)
|
||||
if ($env:OS -ne 'Windows_NT') { throw 'WMI namespace auditing requires Windows.' }
|
||||
if ($DryRun -and $Action -ne 'Configure') { throw '-DryRun requires -WmiAction Configure.' }
|
||||
if ($Action -eq 'List') {
|
||||
if ($Namespace -or $IncludeChildren) { throw 'List does not accept namespace or inheritance selections. Use Audit, Plan or Configure.' }
|
||||
$inventory = @(Get-WelaWmiNamespaceInventory)
|
||||
$report = [pscustomobject]@{ Scope = 'wmi-namespace-sacl-only'; Action = $Action; Namespaces = $inventory; ExitCode = $(if (@($inventory | Where-Object State -eq Unknown).Count) { 1 } else { 0 }) }
|
||||
} else {
|
||||
$plan = @(Get-WelaWmiAuditPlan -Namespace $Namespace -IncludeChildren:$IncludeChildren)
|
||||
$prerequisite = Get-WelaWmiAuditPrerequisite
|
||||
if ($Action -eq 'Configure') {
|
||||
$context = New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
|
||||
Set-WelaWmiAuditControls -Context $context -Plan $plan
|
||||
$report = Complete-WelaConfiguration -Context $context -Scope 'wmi-namespace-sacl-only' `
|
||||
-SuccessMessage 'Selected WMI namespace SACLs verified; namespace access events and collection remain unverified.'
|
||||
$report | Add-Member NoteProperty Prerequisite $prerequisite
|
||||
} else { $report = [pscustomobject]@{ Scope = 'wmi-namespace-sacl-only'; Action = $Action; Controls = $plan; Prerequisite = $prerequisite; ExitCode = $(if (@($plan | Where-Object Status -eq Unknown).Count) { 1 } else { 0 }) } }
|
||||
$report | Add-Member NoteProperty EventValidation 'Not performed. Namespace access auditing (Security 4662) is distinct from provider-operation success and local/remote WMI-Activity telemetry. Audit-policy readiness is observed separately; no usable-rule credit.'
|
||||
}
|
||||
if ($ResultsPath) {
|
||||
try { $report | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
|
||||
catch { $report.ExitCode = 1; Write-Host "[Failed] Writing WMI results: $_" -ForegroundColor Red }
|
||||
}
|
||||
return $report
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# In-memory descriptors only. All native readers/writers are replaced before control execution.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = Split-Path $PSScriptRoot -Parent
|
||||
$script:ScriptRoot = $repo
|
||||
. (Join-Path $repo 'scripts/Configuration.ps1')
|
||||
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
|
||||
$script:assertions = 0
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-' + [guid]::NewGuid().ToString('N'))
|
||||
$null = New-Item -ItemType Directory -Path $root
|
||||
$fixture = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'fixtures/wmi-namespace-descriptor.json') -Raw
|
||||
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
|
||||
function Throws([scriptblock]$Action, [string]$Message) { $caught = $false; try { & $Action } catch { $caught = $true }; Assert $caught $Message }
|
||||
function Reset-Mocks {
|
||||
$script:descriptor = $fixture | ConvertFrom-Json
|
||||
$script:writes = 0; $script:reads = 0; $script:readFail = $false; $script:writeCode = 0
|
||||
$script:race = $false; $script:alterOwner = $false; $script:dropUnknown = $false; $script:ineffective = $false
|
||||
$script:decline = $false; $script:promptCallback = $null
|
||||
}
|
||||
function Get-WelaWmiNamespaceSnapshot {
|
||||
param($Namespace)
|
||||
$script:reads++
|
||||
if ($script:readFail) { throw 'Access denied or namespace missing; no complete SACL available.' }
|
||||
[pscustomobject]@{ Namespace = $Namespace; DescriptorJson = ConvertTo-WelaWmiJson $script:descriptor; DescriptorMof = 'mock full descriptor'; SaclReadPrivilege = 'mock assigned/enabled' }
|
||||
}
|
||||
function Set-WelaWmiNamespaceDescriptor {
|
||||
param($Namespace, $ExpectedJson, $Definitions)
|
||||
# Model the production immediate re-read and verify the generic runner journal.
|
||||
$entry = @(Get-Content -LiteralPath (Join-Path $script:context.BackupPath 'before.jsonl') | ConvertFrom-Json)[-1]
|
||||
Assert ($entry.Before.DescriptorJson -ceq $ExpectedJson -and $entry.Target.Namespace -eq $Namespace) 'Full recovery descriptor persisted before any setter'
|
||||
Assert ($entry.Before.DescriptorMof -eq 'mock full descriptor') 'Journal includes native descriptor representation'
|
||||
if ($script:race) { $script:descriptor.Owner.SIDString = 'S-1-5-18' }
|
||||
if ((ConvertTo-WelaWmiJson $script:descriptor) -cne $ExpectedJson) { throw 'Namespace descriptor changed after its recovery snapshot; no SACL was written.' }
|
||||
$script:writes++
|
||||
Assert-WelaWmiReturnCode ([pscustomobject]@{ ReturnValue = $script:writeCode }) 'SetSecurityDescriptor'
|
||||
if ($script:ineffective) { return }
|
||||
foreach ($definition in @(Get-WelaWmiMissingAces $script:descriptor $Definitions)) {
|
||||
$script:descriptor.SACL += [pscustomobject]@{ AccessMask = $definition.AccessMask; AceFlags = $definition.AceFlags; AceType = 2; Trustee = [pscustomobject]@{ SIDString = $definition.Sid } }
|
||||
}
|
||||
$script:descriptor.ControlFlags = [uint32]$script:descriptor.ControlFlags -bor 16
|
||||
if ($script:alterOwner) { $script:descriptor.Owner.SIDString = 'S-1-5-18' }
|
||||
if ($script:dropUnknown) { $script:descriptor.SACL = @($script:descriptor.SACL | Where-Object AceType -ne 19) }
|
||||
}
|
||||
function Read-Host { param($Prompt) if ($script:promptCallback) { & $script:promptCallback }; if ($script:decline) { 'n' } else { 'Y' } }
|
||||
function New-TestContext([switch]$DryRun, [switch]$Prompt) {
|
||||
$script:context = New-WelaConfigurationContext -Auto:(-not $Prompt) -DryRun:$DryRun -BackupPath (Join-Path $root ([guid]::NewGuid().ToString('N')))
|
||||
return $script:context
|
||||
}
|
||||
function Run-Controls { param($Context, [string[]]$Namespace = @('root\cimv2'), [switch]$IncludeChildren)
|
||||
Set-WelaWmiAuditControls -Context $Context -Plan @(Get-WelaWmiAuditPlan -Namespace $Namespace -IncludeChildren:$IncludeChildren)
|
||||
}
|
||||
try {
|
||||
$source = @(Get-WelaWmiAuditDefinitions -IncludeChildren)
|
||||
Assert ($source.Count -eq 8) 'All eight pinned ASD entries are represented'
|
||||
Assert (@($source.Namespace | Select-Object -Unique).Count -eq 5) 'Exactly five supported namespaces'
|
||||
$expected = @('root\cimv2|262146|64|S-1-1-0', 'root\cimv2|1|64|S-1-5-4', 'root\cimv2|1|64|S-1-5-2', 'root\cimv2|1|64|S-1-5-3', 'root\SecurityCenter|262145|66|S-1-1-0', 'root\SecurityCenter2|262145|66|S-1-1-0', 'root\subscription|262174|66|S-1-1-0', 'root\default|262175|66|S-1-1-0')
|
||||
foreach ($i in 0..7) { Assert (("$($source[$i].Namespace)|$($source[$i].AccessMask)|$($source[$i].AceFlags)|$($source[$i].Sid)") -eq $expected[$i]) 'Masks, principals and inheritance match pinned ASD source' }
|
||||
Assert (@(Get-WelaWmiAuditDefinitions | Where-Object AceFlags -ne 64).Count -eq 0) 'Default does not extend auditing into unselected descendants'
|
||||
Assert (@(Get-WelaWmiAuditDefinitions -Namespace @('ROOT\CIMV2','root\cimv2')).Count -eq 4) 'Case-insensitive duplicate selections do not duplicate ACE definitions'
|
||||
foreach ($invalid in @('root\*','\\server\root\cimv2','root/cimv2','root\cimv2\child','root\default ')) { Throws { Get-WelaWmiAuditDefinitions -Namespace $invalid } 'Wildcards, remote paths and unreviewed namespace targets rejected' }
|
||||
Throws { Get-WelaWmiAuditPlan } 'Empty selection refuses implicit configuration'
|
||||
foreach ($value in @(2,8,9,21,4294967295,$null,$false,'unknown')) {
|
||||
Throws { Assert-WelaWmiReturnCode ([pscustomobject]@{ReturnValue=$value}) 'GetSecurityDescriptor' } 'Every nonzero/missing/invalid return fails'
|
||||
Throws { Assert-WelaWmiReturnCode ([pscustomobject]@{ReturnValue=$value}) 'SetSecurityDescriptor' } 'Every setter error is checked'
|
||||
}
|
||||
Assert-WelaWmiReturnCode ([pscustomobject]@{ReturnValue=[uint32]0}) 'GetSecurityDescriptor'
|
||||
$getter = [pscustomobject]@{ Code = 2; Descriptor = [pscustomobject]@{ControlFlags=4} }
|
||||
$getter | Add-Member ScriptMethod InvokeMethod { param($Name,$Parameters,$Options) [pscustomobject]@{ReturnValue=$this.Code;Descriptor=$this.Descriptor} }
|
||||
Throws { Get-WelaWmiNativeDescriptor $getter } 'Production getter checks provider return code even when descriptor is populated'
|
||||
$getter.Code=0; $getter.Descriptor=$null
|
||||
Throws { Get-WelaWmiNativeDescriptor $getter } 'Production getter refuses missing descriptor even with success code'
|
||||
$getter.Descriptor=[pscustomobject]@{ControlFlags=4;SACL=$null}
|
||||
Assert ((Get-WelaWmiNativeDescriptor $getter).ControlFlags -eq 4) 'Production getter accepts explicit success and descriptor'
|
||||
Assert ((Get-WelaWmiSid ([pscustomobject]@{ SID = [byte[]]@(1,1,0,0,0,0,0,1,0,0,0,0) })) -eq 'S-1-1-0') 'Binary SID identity supported without localized names'
|
||||
Reset-Mocks
|
||||
$before = $script:descriptor | ConvertTo-Json -Depth 30 | ConvertFrom-Json
|
||||
$context = New-TestContext -DryRun
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 0 -and -not (Test-Path $context.BackupPath) -and $context.Results[0].Status -eq 'Skipped') 'Dry-run has no setter or journal mutation'
|
||||
$context = New-TestContext -Prompt; $script:decline = $true
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 0 -and $context.Results[0].Diagnostic -match 'Declined') 'Operator decline has no setter'
|
||||
Reset-Mocks
|
||||
$context = New-TestContext
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 1 -and $context.Results[0].Status -eq 'Applied') 'One namespace update appends four missing CIMV2 ACEs'
|
||||
Assert ((Complete-WelaConfiguration $context -Scope wmi-namespace-sacl-only).ExitCode -eq 0) 'Applied namespace passes final verification'
|
||||
Assert (Test-WelaWmiDescriptorPreserved $before $script:descriptor) 'Owner/group/DACL/control flags and duplicate unknown ACEs preserved'
|
||||
Assert ($script:descriptor.ControlFlags -eq (36868 -bor 16)) 'Only SACL_PRESENT is added to descriptor control flags'
|
||||
Assert ($script:descriptor.SACL.Count -eq 7 -and @($script:descriptor.SACL | Where-Object AceType -eq 19).Count -eq 2) 'Unknown ACE multiplicity preserved'
|
||||
$context = New-TestContext
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 1 -and $context.Results[0].Status -eq 'AlreadyCompliant') 'Second run does not duplicate entries'
|
||||
$script:descriptor.DACL[0].AccessMask = 1
|
||||
Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1 -and $context.Results[0].Status -eq 'Overridden') 'Final already-compliant DACL drift is detected'
|
||||
Reset-Mocks; $context = New-TestContext; $script:race = $true
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Changed owner between journal and setter refuses update'
|
||||
Reset-Mocks; $context = New-TestContext -Prompt
|
||||
$script:promptCallback = { $script:descriptor.SACL[1].OpaqueFutureField = @(99) }
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Unknown ACE changes during operator prompt are preserved by refusing write'
|
||||
foreach ($failure in @('alterOwner','dropUnknown','ineffective')) {
|
||||
Reset-Mocks; Set-Variable -Scope Script -Name $failure -Value $true; $context = New-TestContext
|
||||
Run-Controls $context
|
||||
Assert ($context.Results[0].Status -eq 'Failed' -and (Complete-WelaConfiguration $context).ExitCode -eq 1) 'Read-back rejects permission damage, dropped unknown ACE or ineffective update'
|
||||
}
|
||||
Reset-Mocks; $script:writeCode = 9; $context = New-TestContext
|
||||
Run-Controls $context
|
||||
Assert ($context.Results[0].Status -eq 'Failed' -and $script:descriptor.SACL.Count -eq 3) 'Provider return-code error is a failed control'
|
||||
Reset-Mocks; $script:readFail = $true; $context = New-TestContext
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Unreadable/missing selected namespace fails without partial descriptor writes'
|
||||
Reset-Mocks; $context = New-TestContext
|
||||
Remove-Item -LiteralPath $context.BackupPath -Recurse
|
||||
Run-Controls $context
|
||||
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Journal failure prevents setter invocation'
|
||||
Reset-Mocks; $context = New-TestContext
|
||||
Run-Controls $context
|
||||
$script:descriptor.SACL += [pscustomobject]@{ AceType=2; AceFlags=128; AccessMask=1; Trustee=[pscustomobject]@{SIDString='S-1-5-18'} }
|
||||
Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1 -and $context.Results[0].Status -eq 'Overridden') 'Extra SACL drift after successful write is detected at final check'
|
||||
Reset-Mocks; $context = New-TestContext
|
||||
Run-Controls $context -Namespace 'root\subscription' -IncludeChildren
|
||||
Assert ($script:descriptor.SACL[-1].AceFlags -eq 66 -and $script:descriptor.SACL[-1].AccessMask -eq 262174) 'Explicit child option enables exactly ASD inheritance for subscription'
|
||||
$definition = @(Get-WelaWmiAuditDefinitions -Namespace 'root\cimv2')[0]
|
||||
$ace = [pscustomobject]@{AceType=2;AceFlags=64;AccessMask=262146;Trustee=[pscustomobject]@{SIDString='S-1-1-0'}}
|
||||
Assert (Test-WelaWmiAceMatch $ace $definition) 'Exact ordinary success ACE satisfies requirement'
|
||||
foreach ($flags in @(80,192,66,72)) { $ace.AceFlags=$flags; Assert (-not (Test-WelaWmiAceMatch $ace $definition)) 'Inherited/broader/different-scope ACE does not hide a missing exact request' }
|
||||
$tokens=$null;$parseErrors=$null
|
||||
$ast=[System.Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'),[ref]$tokens,[ref]$parseErrors)
|
||||
Assert ($parseErrors.Count -eq 0) 'Combined CLI parses'
|
||||
# Execute only the actual top-level DryRun guard, with no command dispatch.
|
||||
$guard=$ast.EndBlock.Statements | Where-Object { $_ -is [System.Management.Automation.Language.IfStatementAst] -and $_.Extent.Text.StartsWith('if ($DryRun') } | Select-Object -First 1
|
||||
$Cmd='wmi-auditing';$DryRun=$true;$WmiAction='Configure';$FirewallAction='Audit';$SmbAction='Audit'
|
||||
& ([scriptblock]::Create($guard.Extent.Text));$WmiAction='Audit'
|
||||
Throws { & ([scriptblock]::Create($guard.Extent.Text)) } 'WMI Audit rejects DryRun before dispatch'
|
||||
Write-Host "PASS: $script:assertions WMI namespace assertions (mocked, no live namespace changes)."
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
@@ -0,0 +1,60 @@
|
||||
# Actual reads and in-memory typed provider responses only. Never sends a native SetSecurityDescriptor.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:OS -ne 'Windows_NT') { Write-Host 'SKIP: Windows only'; return }
|
||||
$repo = Split-Path $PSScriptRoot -Parent
|
||||
. (Join-Path $repo 'scripts/Configuration.ps1')
|
||||
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
|
||||
$script:assertions = 0
|
||||
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
|
||||
$before = Get-WelaWmiNamespaceSnapshot 'root\cimv2'
|
||||
Assert ($before.DescriptorJson -and $before.DescriptorMof -and $before.SaclReadPrivilege -eq 'SeSecurityPrivilege enabled') 'Actual privileged native descriptor read and full export'
|
||||
$inventory = @(Get-WelaWmiNamespaceInventory)
|
||||
Assert ($inventory.Count -eq 5 -and @($inventory | Where-Object { $_.Namespace -eq 'root\cimv2' -and $_.State -eq 'Present' }).Count -eq 1) 'Supported namespace inventory reads actual local namespaces'
|
||||
$plan = @(Get-WelaWmiAuditPlan -Namespace 'root\cimv2')
|
||||
Assert ($plan[0].Status -in @('AlreadyCompliant','ChangeRequired')) 'Actual CIMV2 plan reads successfully'
|
||||
$path = Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-readonly-' + [guid]::NewGuid().ToString('N'))
|
||||
$context = New-WelaConfigurationContext -Auto -DryRun -BackupPath $path
|
||||
Set-WelaWmiAuditControls -Context $context -Plan $plan
|
||||
Assert (-not (Test-Path $path) -and $context.Results[0].Status -in @('AlreadyCompliant','Skipped')) 'Real dry-run neither writes SACL nor creates journal'
|
||||
$after = Get-WelaWmiNamespaceSnapshot 'root\cimv2'
|
||||
Assert ($before.DescriptorJson -ceq $after.DescriptorJson) 'Full descriptor unchanged by read-only planning/dry-run'
|
||||
# Read actual native objects once; from here the native connection factory is
|
||||
# replaced in the same script scope before invoking ANY setter code.
|
||||
Initialize-WelaWmiInterop
|
||||
$privilege = New-Object Wela.WmiSecurityPrivilege
|
||||
$connection = $null
|
||||
try {
|
||||
$connection = New-WelaWmiConnection 'root\cimv2'
|
||||
$script:fixtureDescriptor = (Get-WelaWmiNativeDescriptor $connection).Clone()
|
||||
$script:fixtureParameters = $connection.GetMethodParameters('SetSecurityDescriptor')
|
||||
} finally { if ($connection) { $connection.Dispose() }; $privilege.Dispose() }
|
||||
# An empty in-memory SACL forces all requested additions without changing Windows.
|
||||
$script:fixtureDescriptor.SACL = $null
|
||||
$expected = ConvertTo-WelaWmiJson (ConvertTo-WelaWmiData $script:fixtureDescriptor)
|
||||
$script:setCalls = 0; $script:captured = $null; $script:returnCode = [uint32]0
|
||||
$script:fake = [pscustomobject]@{}
|
||||
$script:fake | Add-Member ScriptMethod InvokeMethod {
|
||||
param($Name, $Parameters, $Options)
|
||||
if ($Name -eq 'GetSecurityDescriptor') { return [pscustomobject]@{ ReturnValue = [uint32]0; Descriptor = $script:fixtureDescriptor } }
|
||||
if ($Name -ne 'SetSecurityDescriptor') { throw "Unexpected method: $Name" }
|
||||
$script:setCalls++; $script:captured = $Parameters.Descriptor.Clone()
|
||||
return [pscustomobject]@{ ReturnValue = $script:returnCode }
|
||||
}
|
||||
$script:fake | Add-Member ScriptMethod GetMethodParameters { param($Name) if ($Name -ne 'SetSecurityDescriptor') { throw 'Unexpected method parameters' }; return $script:fixtureParameters.Clone() }
|
||||
$script:fake | Add-Member ScriptMethod Dispose { }
|
||||
function New-WelaWmiConnection { param($Namespace) if ($Namespace -ne 'root\cimv2') { throw 'Unexpected fake target' }; return $script:fake }
|
||||
$definitions = @(Get-WelaWmiAuditDefinitions -Namespace 'root\cimv2')
|
||||
Set-WelaWmiNamespaceDescriptor -Namespace 'root\cimv2' -ExpectedJson $expected -Definitions $definitions
|
||||
Assert ($script:setCalls -eq 1 -and $script:captured -is [System.Management.ManagementBaseObject]) 'Production writer builds typed descriptor against fake provider only'
|
||||
$original = $expected | ConvertFrom-Json
|
||||
$captured = ConvertTo-WelaWmiData $script:captured
|
||||
Assert (Test-WelaWmiDescriptorPreserved $original $captured) 'Typed descriptor clone preserves DACL owner group and control flags'
|
||||
Assert (@(Get-WelaWmiMissingAces $captured $definitions).Count -eq 0 -and @($captured.SACL).Count -eq 4) 'Actual Win32_ACE/Trustee objects carry all four exact masks and binary SIDs'
|
||||
$script:returnCode = [uint32]9
|
||||
$failed = $false
|
||||
try { Set-WelaWmiNamespaceDescriptor -Namespace 'root\cimv2' -ExpectedJson $expected -Definitions $definitions } catch { $failed = $_.Exception.Message -match 'ReturnValue=9' }
|
||||
Assert $failed 'Production SetSecurityDescriptor wrapper rejects native nonzero return code'
|
||||
$prior = $script:setCalls; $failed = $false
|
||||
try { Set-WelaWmiNamespaceDescriptor -Namespace 'root\cimv2' -ExpectedJson '{}' -Definitions $definitions } catch { $failed = $_.Exception.Message -match 'changed after' }
|
||||
Assert ($failed -and $script:setCalls -eq $prior) 'Production writer detects changed snapshot before fake setter'
|
||||
Write-Host "PASS: $script:assertions Windows namespace read-only / in-memory native adapter assertions. No live SACL changes or event-generation claims."
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"ControlFlags": 36868,
|
||||
"DACL": [
|
||||
{"AccessMask": 393279, "AceFlags": 2, "AceType": 0, "GuidObjectType": null, "GuidInheritedObjectType": null, "Trustee": {"SIDString": "S-1-5-32-544", "Name": "Administrators", "Domain": "BUILTIN"}},
|
||||
{"AccessMask": 32, "AceFlags": 0, "AceType": 1, "Trustee": {"SIDString": "S-1-5-21-1-2-3-1001"}}
|
||||
],
|
||||
"Group": {"SIDString": "S-1-5-18"},
|
||||
"Owner": {"SIDString": "S-1-5-32-544"},
|
||||
"SACL": [
|
||||
{"AccessMask": 2, "AceFlags": 128, "AceType": 2, "Trustee": {"SIDString": "S-1-1-0"}},
|
||||
{"AccessMask": 8, "AceFlags": 16, "AceType": 19, "GuidObjectType": "unfamiliar-object", "OpaqueFutureField": [1, 7, 255], "Trustee": {"SIDString": "S-1-5-18"}},
|
||||
{"AccessMask": 8, "AceFlags": 16, "AceType": 19, "GuidObjectType": "unfamiliar-object", "OpaqueFutureField": [1, 7, 255], "Trustee": {"SIDString": "S-1-5-18"}}
|
||||
],
|
||||
"ProviderExtension": {"Keep": "unchanged"}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
**改善:**
|
||||
|
||||
- ASDのガイドに基づく任意実行のWMI名前空間SACL監査・計画・設定を追加した。ローカル名前空間の明示的な選択と、子名前空間への継承の個別指定に対応する。完全なセキュリティ記述子の記録、特権・戻り値の確認、書き込み前の変更検出と読み戻し検証により、既存のアクセス権と未知の監査エントリを保持する。イベント生成と転送の検証は別途必要となる。 (#372) (@Shirofune-Security)
|
||||
- ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (#394) (@Shirofune-Security)
|
||||
- イベントログのサイズ監査と設定に共通のバイト単位プロファイルを導入し、AppLocker・ファイアウォールログの256 MiB、Setupの32 MiB、ASD推奨のSecurityログ2048 MiBに対応した。`-LogProfile`と`configure-eventlogs`で送信元と収集サーバーのサイズ・保存方式を選択できる。明示的に指定しない限り、既存の大きいバッファと保存方式は維持する。結果には検証した設定を記録し、未測定の保存日数は不明と表示する。 (#396) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
**Improvements:**
|
||||
|
||||
- Added opt-in WMI namespace SACL audit, plan and configure actions based on ASD guidance, with explicit local namespace selection and separate descendant-inheritance consent. Full descriptor journals, privilege/return-code checks, race guards and read-back verification preserve existing permissions and unknown audit entries; event generation and forwarding remain separate lab validation. (#372) (@Shirofune-Security)
|
||||
- Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (#394) (@Shirofune-Security)
|
||||
- Unified event-log size auditing and configuration with shared byte-based profiles, including 256 MiB AppLocker/firewall logs, 32 MiB Setup and ASD 2048 MiB Security. Added separate source and collector size/mode choices through `-LogProfile` and `configure-eventlogs`; larger buffers and existing retention modes are preserved unless explicitly changed. Results include verified state and unknown retention duration. (#396) (@Shirofune-Security)
|
||||
|
||||
|
||||
Reference in new issue
Block a user