Merge remote-tracking branch 'origin/dev' into feat/381-applocker-readiness

# Conflicts:
#	WELA.ps1
This commit is contained in:
Shirofune-Security committed 2026-09-19 06:52:50 +09:00
commit aff422bf99
14 files changed
+877 -3

No files matched your search

@@ -0,0 +1,63 @@
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: In-memory privilege restoration failure paths (Windows PowerShell 5.1)
shell: powershell
run: ./tests/WmiNamespaceAuditing.Privilege.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
- name: In-memory privilege restoration failure paths (PowerShell 7)
shell: pwsh
run: ./tests/WmiNamespaceAuditing.Privilege.Tests.ps1
disposable-namespace:
# Mutations are restricted to newly created namespaces on hosted throwaway VMs.
strategy:
fail-fast: false
matrix:
os: [windows-2022, windows-2025]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Real temporary-namespace SACL write/readback (Windows PowerShell 5.1)
shell: powershell
run: ./tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 -AllowDisposableNamespaceWrite -EvidencePath wmi-native-ps51.json
- name: Real temporary-namespace SACL write/readback (PowerShell 7)
shell: pwsh
run: ./tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 -AllowDisposableNamespaceWrite -EvidencePath wmi-native-ps7.json
- name: Record native descriptor evidence
if: always()
shell: pwsh
run: |
foreach ($path in @('wmi-native-ps51.json', 'wmi-native-ps7.json')) {
if (Test-Path -LiteralPath $path) {
Write-Host "Evidence: $path"
Get-Content -LiteralPath $path -Raw
}
}
+1
View File
@@ -4,6 +4,7 @@
**改善:**
- ASDのガイドに基づく任意実行のWMI名前空間SACL監査・計画・設定を追加した。ローカル名前空間の明示的な選択と、子名前空間への継承の個別指定に対応する。完全なセキュリティ記述子の記録、SACLだけを更新するネイティブ要求、特権の復元確認、書き込み前の変更検出と読み戻し検証により、既存のアクセス権と未知の監査エントリを保持する。イベント生成と転送の検証は別途必要となる。 使い捨てのServer 2022/2025名前空間でPowerShell 5.1/7の制御フラグ読み戻しと冪等性を検証した。 (#399) (@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)
+1
View File
@@ -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, SACL-only native requests, checked privilege restoration, race guards and read-back verification preserve existing permissions and unknown audit entries; event generation and forwarding remain separate lab validation. Verified native control-flag readback and idempotence on disposable Server 2022/2025 namespaces under PowerShell 5.1/7. (#399) (@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)
+27 -2
View File
@@ -25,6 +25,9 @@
[ValidateSet('Audit', 'Plan', 'Configure')][string]$SmbAction = 'Audit',
[ValidateSet('Audit', 'Plan', 'Import')][string]$AppLockerAction = 'Audit',
[string]$AppLockerPolicyPath,
[ValidateSet('List', 'Audit', 'Plan', 'Configure')][string]$WmiAction = 'List',
[string[]]$WmiNamespace,
[switch]$WmiIncludeChildren,
[switch]$Help
)
@@ -42,6 +45,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
. (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1")
. (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1")
. (Join-Path $ScriptRoot "scripts/AppLockerReadiness.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
@@ -1678,6 +1682,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
@@ -1733,8 +1741,12 @@ if ($PSBoundParameters.ContainsKey('SaclMode') -and
# Reject unsupported dry-run requests before reaching any command's mutation path.
if ($DryRun -and -not ($Cmd -eq 'applocker-readiness' -and $AppLockerAction -eq 'Import') -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, smb-auditing -SmbAction Configure, and applocker-readiness -AppLockerAction Import. 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, wmi-auditing -WmiAction Configure, and applocker-readiness -AppLockerAction Import. 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.'
@@ -1752,6 +1764,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]'
+77
View File
@@ -0,0 +1,77 @@
# 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. The native setter request clears `SE_DACL_PRESENT` and leaves DACL, owner and group null: the [documented provider contract](https://learn.microsoft.com/en-us/windows/win32/wmisdk/setsecuritydescriptor-method-in-class---systemsecurity) preserves those access fields rather than rewriting them. `SE_SACL_PRESENT` requests the SACL update. Full read-back still verifies DACL order, owner, group, other control flags and every original audit entry against the complete recovery snapshot. Unknown entries are never deliberately simplified or discarded.
Privilege restoration runs even if connection disposal fails. Both enabling and restoring the token privilege check the API return value and last-error code; [AdjustTokenPrivileges](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-adjusttokenprivileges) can return success while reporting an unassigned privilege. A restoration error is reported as a failed operation, not silently treated as restored state.
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. A separate integration job performs real SACL writes only on uniquely created temporary namespaces on disposable Server 2022/2025 runners, verifies read-back/idempotence and deletes its own namespaces. It never changes the SACL of an existing namespace. **Writes to the five production target namespaces, Windows 11 behavior, 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).
## Native control-flag readback evidence
The disposable-namespace test addresses [review comment 4052822447](https://github.com/Yamato-Security/WELA/pull/399#discussion_r4052822447) without weakening preservation checks. [The verified CI run](https://github.com/Yamato-Security/WELA/actions/runs/35436825928) used the real production SACL writer and configuration runner against eight fresh namespaces (two ACE flag modes, two PowerShell versions and two operating systems). Each started without a SACL, retained owner/group/DACL, passed first-write readback and an idempotent repeat, then was deleted. Full descriptor snapshots and cleanup results are in the job logs.
| Host build | PowerShell | ACE flags tested | ControlFlags before | ControlFlags after |
| --- | --- | --- | --- | --- |
| Server 2022 / 20348 | 5.1.20348.5622, 7.6.6 | 64 and 66 | 32772 / `0x8004` | 32788 / `0x8014` |
| Server 2025 / 26100 | 5.1.26100.33296, 7.6.5 | 64 and 66 | 32772 / `0x8004` | 32788 / `0x8014` |
No extra auto-inherited/defaulted bit appeared in these cases. The existing `Before.ControlFlags | 0x10` equality is retained: an unexpected flag change still fails preservation verification. These results establish this provider behavior for the listed clean namespace scenarios, not every existing namespace or Windows version.
To repeat on a **disposable Windows lab VM** (this is a mutating integration test):
```powershell
./tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 -AllowDisposableNamespaceWrite -EvidencePath wmi-native.json
```
The script accepts no target namespace. It uses generated `root\WelaSaclTest_<GUID>` names, creates them with CreateOnly, verifies the returned identity, runs the writer only there, and removes only instances it created. The normal read-only test remains separate. It does not enable audit policy, generate controlled Security 4662 evidence or test forwarding. Namespace lifecycle follows Microsoft's [__Namespace contract](https://learn.microsoft.com/en-us/windows/win32/wmisdk/--namespace); SACL-only updates follow the [SetSecurityDescriptor contract](https://learn.microsoft.com/en-us/windows/win32/wmisdk/setsecuritydescriptor-method-in-class---systemsecurity).
+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")]
[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
+324
View File
@@ -0,0 +1,324 @@
# 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;
bool ok = AdjustTokenPrivileges(token, false, ref previous, (uint)Marshal.SizeOf(typeof(TokenPrivileges)), out ignored, out required);
int error = Marshal.GetLastWin32Error();
if (!ok || error != 0) throw new Win32Exception(error, "Restoring SeSecurityPrivilege failed; the previous token state could not be verified.");
}
} 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 {
try { if ($connection) { $connection.Dispose() } }
finally { $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
# SetSecurityDescriptor treats SE_DACL_PRESENT and non-null Owner/Group
# as requests to rewrite access permissions. Omit those fields explicitly
# so the provider preserves them, even if another writer races this call.
# Complete original fields remain in the journal and read-back comparison.
$updated.DACL = $null; $updated.Owner = $null; $updated.Group = $null
$updated.ControlFlags = ([uint32]$descriptor.ControlFlags -band [uint32]4294967291) -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 {
try { if ($connection) { $connection.Dispose() } }
finally { $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,85 @@
# Actual SACL writes, confined to fresh temporary namespaces on a disposable VM.
# Existing namespaces are read only as the parent/factory; never passed to a setter.
param([switch]$AllowDisposableNamespaceWrite, [string]$EvidencePath)
$ErrorActionPreference = 'Stop'
if (-not $AllowDisposableNamespaceWrite) { throw 'This integration test requires -AllowDisposableNamespaceWrite on a disposable Windows VM.' }
if ($env:OS -ne 'Windows_NT') { throw 'Disposable-namespace integration requires Windows.' }
$repo = Split-Path $PSScriptRoot -Parent
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
Initialize-WelaWmiInterop
$script:assertions = 0
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
$evidence = [pscustomobject]@{
SchemaVersion = 1; Computer = $env:COMPUTERNAME; OperatingSystem = [Environment]::OSVersion.VersionString
PowerShell = $PSVersionTable.PSVersion.ToString(); StartedUtc = [DateTime]::UtcNow.ToString('o')
Scope = 'Real SACL write/readback on uniquely created root child namespaces only'
EventGeneration = 'Not tested'; Forwarding = 'Not tested'; Cases = @(); Complete = $false
}
$backup = Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-integration-' + [guid]::NewGuid().ToString('N'))
try {
foreach ($flags in @(64, 66)) {
$name = 'WelaSaclTest_' + [guid]::NewGuid().ToString('N')
$namespace = 'root\' + $name
Assert ($namespace -match '^root\\WelaSaclTest_[0-9a-f]{32}$') 'Only the generated test namespace can receive writes'
$created = $false; $factory = $null; $instance = $null
$case = [pscustomobject]@{ Namespace=$namespace; AceFlags=$flags; Before=$null; After=$null; Result=$null; RepeatResult=$null; BeforeControlFlags=$null; ExpectedControlFlags=$null; AfterControlFlags=$null; Removed=$false }
$evidence.Cases += $case
try {
# CreateOnly is essential: never adopt or delete an existing namespace.
$factory = New-Object System.Management.ManagementClass -ArgumentList '\\.\root:__Namespace'
$instance = $factory.CreateInstance(); $instance.Name = $name
$options = New-Object System.Management.PutOptions
$options.Type = [System.Management.PutType]::CreateOnly
$createdPath = $instance.Put($options)
$created = $true
Assert ($createdPath.RelativePath -eq ('__NAMESPACE.Name="' + $name + '"')) 'Created namespace identity matches the generated name'
$before = Get-WelaWmiNamespaceSnapshot $namespace
$case.Before = $before
$beforeData = $before.DescriptorJson | ConvertFrom-Json
$case.BeforeControlFlags = [uint32]$beforeData.ControlFlags
$case.ExpectedControlFlags = [uint32]$beforeData.ControlFlags -bor 16
Assert (@($beforeData.SACL | Where-Object { $null -ne $_ }).Count -eq 0) 'Fixture exercises first SACL creation on a namespace with no existing audit ACEs'
# Reuse the real ASD root-default mask/SID, with the test target and
# explicit inheritance mode. Production profile scope is unchanged.
$definitions = @(Get-WelaWmiAuditDefinitions -Namespace 'root\default' -IncludeChildren)
$definitions[0].Namespace = $namespace; $definitions[0].AceFlags = [uint32]$flags
$entry = [pscustomobject]@{ Namespace=$namespace; Definitions=$definitions }
$context = New-WelaConfigurationContext -Auto -BackupPath (Join-Path $backup ('first-' + $flags))
Set-WelaWmiAuditControls -Context $context -Plan @($entry)
$case.Result = Complete-WelaConfiguration -Context $context -Scope 'wmi-namespace-sacl-only'
$after = Get-WelaWmiNamespaceSnapshot $namespace
$case.After = $after
$afterData = $after.DescriptorJson | ConvertFrom-Json
$case.AfterControlFlags = [uint32]$afterData.ControlFlags
Write-Host "Native flags: mode=$flags before=$($case.BeforeControlFlags) expected=$($case.ExpectedControlFlags) after=$($case.AfterControlFlags)"
Assert ($case.Result.ExitCode -eq 0 -and $case.Result.Results[0].Status -eq 'Applied') 'Actual production runner accepts the provider readback after first SACL creation'
Assert ($case.AfterControlFlags -eq $case.ExpectedControlFlags) 'Provider control flags match the exact preservation contract for this tested host/mode'
Assert (Test-WelaWmiDescriptorPreserved $beforeData $afterData) 'Original access fields and existing ACEs survive the real SACL-only write'
Assert (@(Get-WelaWmiMissingAces $afterData $definitions).Count -eq 0) 'Native provider stores the requested SID/mask/outcome/inheritance'
$repeat = New-WelaConfigurationContext -Auto -BackupPath (Join-Path $backup ('repeat-' + $flags))
Set-WelaWmiAuditControls -Context $repeat -Plan @($entry)
$case.RepeatResult = Complete-WelaConfiguration -Context $repeat -Scope 'wmi-namespace-sacl-only'
Assert ($case.RepeatResult.ExitCode -eq 0 -and $case.RepeatResult.Results[0].Status -eq 'AlreadyCompliant') 'Repeated real configuration is idempotent'
Assert ((Get-WelaWmiNamespaceSnapshot $namespace).DescriptorJson -ceq $after.DescriptorJson) 'Repeat leaves the full descriptor unchanged'
} finally {
try {
if ($created) {
# The only deletion target is the instance this run created.
$instance.Delete()
$remaining = @(Get-CimInstance -Namespace root -ClassName __Namespace -Filter ("Name='$name'") -ErrorAction Stop)
Assert ($remaining.Count -eq 0) 'Owned temporary namespace was removed'
$case.Removed = $true
}
} finally {
if ($instance) { $instance.Dispose() }
if ($factory) { $factory.Dispose() }
}
}
}
$evidence.Complete = $true
Write-Host "PASS: $script:assertions disposable-namespace native SACL assertions. Event generation and forwarding were not tested."
} finally {
if ($EvidencePath) { $evidence | ConvertTo-Json -Depth 25 | Set-Content -LiteralPath $EvidencePath -Encoding UTF8 -ErrorAction Stop }
if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction Stop }
}
@@ -0,0 +1,76 @@
# Compile the production privilege lifecycle with in-memory native API substitutes.
# No process token or live WMI namespace is modified by this test.
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
$source = Get-Content -LiteralPath (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1') -Raw
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
$script:assertions = 0
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
$match = [regex]::Match($source, "(?s)Add-Type -TypeDefinition @'\r?\n(.*?)\r?\n'@ -ErrorAction Stop")
Assert $match.Success 'Production privilege helper located'
$csharp = $match.Groups[1].Value.Replace('namespace Wela {', 'namespace WelaPrivilegeFixture {')
# Replace only external API declarations/error reads, retaining constructor and
# Dispose control flow from the shipped helper rather than mirroring that logic.
$csharp = [regex]::Replace($csharp, '(?m)^ \[DllImport[^\r\n]+\r?\n', '')
$csharp = $csharp.Replace('Marshal.GetLastWin32Error()', 'TestError')
$native = @'
public static int TestError, EnableError, RestoreError, AdjustCalls, CloseCalls;
public static bool RestoreSuccess = true;
public static void Reset() { TestError=EnableError=RestoreError=AdjustCalls=CloseCalls=0; RestoreSuccess=true; }
static IntPtr GetCurrentProcess() { return (IntPtr)1; }
static bool CloseHandle(IntPtr handle) { CloseCalls++; return true; }
static bool OpenProcessToken(IntPtr process, uint access, out IntPtr token) { token=(IntPtr)2; return true; }
static bool LookupPrivilegeValue(string system, string name, out Luid luid) { luid=new Luid(); return true; }
static bool AdjustTokenPrivileges(IntPtr token, bool disable, ref TokenPrivileges current, uint size, out TokenPrivileges previous, out uint required) {
previous=current; previous.Attributes=0; required=16;
AdjustCalls++; TestError=AdjustCalls==1 ? EnableError : RestoreError;
return AdjustCalls==1 || RestoreSuccess;
}
'@
$csharp = $csharp.Replace(' IntPtr token;', $native + "`n IntPtr token;")
Assert ($csharp -notmatch '\[DllImport') 'All token API imports are replaced before compilation'
Add-Type -TypeDefinition $csharp -ErrorAction Stop
$type = [WelaPrivilegeFixture.WmiSecurityPrivilege]
$type::Reset()
$instance = [WelaPrivilegeFixture.WmiSecurityPrivilege]::new()
$instance.Dispose(); $instance.Dispose()
Assert ($type::AdjustCalls -eq 2 -and $type::CloseCalls -eq 1) 'Normal restoration executes once and closes the token once'
foreach ($restoreError in @(1300, 5)) {
$type::Reset(); $type::RestoreError = $restoreError
$instance = [WelaPrivilegeFixture.WmiSecurityPrivilege]::new()
$failed = $false
try { $instance.Dispose() } catch { $failed = $_.Exception.InnerException.NativeErrorCode -eq $restoreError }
Assert $failed 'A true AdjustTokenPrivileges return with nonzero last error is a restoration failure'
Assert ($type::CloseCalls -eq 1) 'Failed privilege restoration still closes the token handle'
}
$type::Reset(); $type::RestoreError = 5; $type::RestoreSuccess = $false
$instance = [WelaPrivilegeFixture.WmiSecurityPrivilege]::new()
$failed = $false; try { $instance.Dispose() } catch { $failed = $true }
Assert ($failed -and $type::CloseCalls -eq 1) 'False API restoration result is reported and handle is closed'
$type::Reset(); $type::EnableError = 1300
$failed = $false; try { [WelaPrivilegeFixture.WmiSecurityPrivilege]::new() } catch { $failed = $true }
Assert ($failed -and $type::AdjustCalls -eq 1 -and $type::CloseCalls -eq 1) 'Unavailable SeSecurityPrivilege refuses the operation and closes its handle'
# Exercise the production PowerShell cleanup paths with a throwing connection.
function Initialize-WelaWmiInterop { }
$script:disposed = 0
$script:privilegeFixture = [pscustomobject]@{}
$script:privilegeFixture | Add-Member ScriptMethod Dispose { $script:disposed++ }
function New-Object {
param([string]$TypeName, [object[]]$ArgumentList)
if ($TypeName -eq 'Wela.WmiSecurityPrivilege') { return $script:privilegeFixture }
throw "Unexpected construction in failure fixture: $TypeName"
}
$script:connectionFixture = [pscustomobject]@{}
$script:connectionFixture | Add-Member ScriptMethod Dispose { throw 'fixture COM cleanup failure' }
function New-WelaWmiConnection { param($Namespace) return $script:connectionFixture }
function Get-WelaWmiNativeDescriptor { param($Connection) throw 'fixture descriptor read failure' }
foreach ($operation in @('Get', 'Set')) {
$before = $script:disposed; $failed = $false
try {
if ($operation -eq 'Get') { Get-WelaWmiNamespaceSnapshot 'root\cimv2' }
else { Set-WelaWmiNamespaceDescriptor 'root\cimv2' '{}' @() }
} catch { $failed = $true }
Assert ($failed -and $script:disposed -eq $before + 1) "$operation restores privilege even when connection cleanup throws"
}
Write-Host "PASS: $script:assertions WMI privilege/cleanup assertions with in-memory APIs only."
+137
View File
@@ -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,68 @@
# 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 ($null -eq $captured.DACL -and $null -eq $captured.Owner -and $null -eq $captured.Group) 'Native request omits access-permission fields instead of requesting that they be rewritten'
Assert (([uint32]$captured.ControlFlags -band 4) -eq 0 -and ([uint32]$captured.ControlFlags -band 16) -eq 16) 'Native request uses only SACL-present mutation semantics, with DACL-present cleared'
Assert ((ConvertTo-WelaWmiJson (ConvertTo-WelaWmiData $script:fixtureDescriptor)) -ceq $expected) 'Building the SACL-only request leaves the complete original descriptor unchanged'
# Simulate the documented provider contract in memory: absent access fields and
# SE_DACL_PRESENT preserve the current access permissions.
$effective = $expected | ConvertFrom-Json
$effective.SACL = $captured.SACL
$effective.ControlFlags = [uint32]$effective.ControlFlags -bor 16
Assert (Test-WelaWmiDescriptorPreserved $original $effective) 'SACL-only provider semantics retain every original non-SACL field'
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
View File
@@ -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"}
}
+1
View File
@@ -7,6 +7,7 @@
**改善:**
- ASDのガイドに基づく任意実行のWMI名前空間SACL監査・計画・設定を追加した。ローカル名前空間の明示的な選択と、子名前空間への継承の個別指定に対応する。完全なセキュリティ記述子の記録、SACLだけを更新するネイティブ要求、特権の復元確認、書き込み前の変更検出と読み戻し検証により、既存のアクセス権と未知の監査エントリを保持する。イベント生成と転送の検証は別途必要となる。 使い捨てのServer 2022/2025名前空間でPowerShell 5.1/7の制御フラグ読み戻しと冪等性を検証した。 (#399) (@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)
+1
View File
@@ -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, SACL-only native requests, checked privilege restoration, race guards and read-back verification preserve existing permissions and unknown audit entries; event generation and forwarding remain separate lab validation. Verified native control-flag readback and idempotence on disposable Server 2022/2025 namespaces under PowerShell 5.1/7. (#399) (@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)