mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-04 13:34:46 +02:00
Add reviewable GPO audit-policy deployment packages (#415)
* Add reviewable GPO audit-policy deployment components * Link GPO audit package changelog to PR 415 * Check GPO verification exit code from a real CLI process
This commit is contained in:
1 parent
ec6a6df68a
commit
3a80ef5e67
12 files changed
+570
-2
No files matched your search
@@ -0,0 +1,45 @@
|
||||
name: GPO audit-policy package regressions
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
paths:
|
||||
- 'WELA.ps1'
|
||||
- 'scripts/GpoAuditPackages.ps1'
|
||||
- 'scripts/Configuration.ps1'
|
||||
- 'modules/AuditProfiles.psm1'
|
||||
- 'config/audit_profiles.json'
|
||||
- 'docs/gpo-*'
|
||||
- 'tests/GpoAuditPackages*'
|
||||
- '.github/workflows/gpo-audit-packages.yml'
|
||||
- '.github/workflows/release.yml'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
gpo-packages:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2022, windows-2025]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Offline semantic and file-safety fixtures in Windows PowerShell 5.1
|
||||
shell: powershell
|
||||
run: ./tests/GpoAuditPackages.Tests.ps1
|
||||
- name: Native template validation and unchanged-policy checks in Windows PowerShell 5.1
|
||||
shell: powershell
|
||||
run: ./tests/GpoAuditPackages.Windows.Tests.ps1 -OutputPath "$env:RUNNER_TEMP/gpo-components-51"
|
||||
- name: Offline semantic and file-safety fixtures in PowerShell 7
|
||||
shell: pwsh
|
||||
run: ./tests/GpoAuditPackages.Tests.ps1
|
||||
- name: Native read-only checks and Windows PowerShell package verification in PowerShell 7
|
||||
shell: pwsh
|
||||
run: ./tests/GpoAuditPackages.Windows.Tests.ps1 -OutputPath "$env:RUNNER_TEMP/gpo-components-7" -VerifyOtherPath "$env:RUNNER_TEMP/gpo-components-51"
|
||||
- name: Verify PowerShell 7 package through public CLI in Windows PowerShell 5.1
|
||||
shell: powershell
|
||||
run: |
|
||||
# A script invoked in-process does not set LASTEXITCODE on normal return.
|
||||
& "$env:SystemRoot/System32/WindowsPowerShell/v1.0/powershell.exe" -NoLogo -NoProfile -NonInteractive -File ./WELA.ps1 gpo-package -GpoAction Verify -GpoOutputPath "$env:RUNNER_TEMP/gpo-components-7"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Public package verification failed.' }
|
||||
@@ -40,6 +40,8 @@ jobs:
|
||||
Copy-Item -Recurse -Path ./config -Destination release-binaries/
|
||||
Copy-Item -Recurse -Path ./scripts -Destination release-binaries/
|
||||
Copy-Item -Recurse -Path ./modules -Destination release-binaries/
|
||||
New-Item -ItemType Directory -Path release-binaries/docs -Force | Out-Null
|
||||
Copy-Item -Path ./docs/gpo-audit-packages.md, ./docs/gpo-package-deployment.md -Destination release-binaries/docs/
|
||||
|
||||
- name: Set Artifact Name
|
||||
if: contains(matrix.info.os, 'windows') == true
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
**改善:**
|
||||
|
||||
- 共有する詳細監査プロファイルと優先設定のセキュリティテンプレートについて、オフラインで計画・出力・検証する`gpo-package`を追加しました。省略項目を保持し、成功または失敗だけの最低要件を両方へ拡張する場合は明示指定を求め、未検証のゼロ値による配備は拒否します。出典・申告対象・全項目レビュー・検証済みハッシュを含む配備用ファイルであり、GPOバックアップではありません。正規のGPMC/LGPO準備と未リンクGPO作成のレビュー手順を文書化しました。ドメイン配備と実イベントの検証は別途ラボで必要となり、Sigma検知範囲には加算しません。 (#415) (@Shirofune-Security)
|
||||
- 検証対象のWindows 11クライアント向けに、共通の標準監査プロファイルからオフラインで出力する`intune-export`を追加しました。Microsoft DDFに基づく59件の明示的な対応表、整数型のOMA-URI CSV/Graphデータ、監査サブカテゴリの優先設定、出典と省略理由の一覧を保存します。最小監査マスクは既定で拒否し、`PromoteToBoth`の明示指定時だけ成功・失敗の両方へ拡張します。新規ローカル出力には検証済みハッシュを付け、アップロード・割り当て・ポリシー削除・Windows設定変更は行いません。Intune配備・競合・復旧・イベントの確認は別途必要です。 (#414) (@Shirofune-Security)
|
||||
- `-ProfileFile`で管理者のJSON詳細監査プロファイルを一覧・計画・監査・設定に使用できるようにしました。標準GUID、役割、設定モード、出典ハッシュを厳密に検証し、組み込みプロファイルを保持します。厳密なJSON字句検証で重複キー検出の回避を防ぎ、レポートは新規ファイルに限ることで別名リンク経由でも入力と既存の証拠を保持します。共通の優先設定、復旧記録、変更直前のファイル確認と最終検証を使用し、イベント生成やSigma検知可能性は別途検証とします。 (#416) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
**Improvements:**
|
||||
|
||||
- Added offline `gpo-package` plan, export and verification for shared advanced audit profiles and the precedence security template. Packages preserve omissions, require explicit expansion of one-sided minimum masks, reject unvalidated zero-mask deployment, and include source/target context, full reviews and verified file hashes. These are deployment components, not GPO backups; genuine GPMC/LGPO preparation and reviewed create-unlinked procedures are documented. Native domain application and event evidence remain separate lab acceptance with no Sigma credit. (#415) (@Shirofune-Security)
|
||||
- Added offline `intune-export` for shared native audit profiles on reviewed Windows 11 client targets, with 59 explicit Microsoft DDF mappings, typed OMA-URI CSV/Graph artifacts, the audit precedence prerequisite and complete source/omission manifests. Static minimum masks are rejected unless explicitly expanded with `PromoteToBoth`; fresh local bundles include verified fingerprints and never upload, assign, delete policies or change Windows. Intune deployment, conflicts, recovery and event evidence remain separate validation. (#414) (@Shirofune-Security)
|
||||
- Added `-ProfileFile` for strictly validated custom advanced audit profiles in listing, planning, auditing and configuration. Canonical GUIDs, roles, modes and source hashes remain explicit; built-in profiles are preserved. Strict JSON tokens prevent duplicate-key bypasses, and new report files preserve inputs and prior evidence even through file aliases. Shared precedence, recovery journals, pre-write file checks and final verification protect configuration, without claiming event or Sigma readiness. (#416) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -63,6 +63,10 @@
|
||||
[ValidateSet('OneSettings','SecurityWarning')][string[]]$NotificationControl,
|
||||
[ValidateRange(1,90)][int]$WarningPercent = 90,
|
||||
[switch]$EnablePrivacyChannel,
|
||||
[ValidateSet('Plan','Export','Verify')][string]$GpoAction = 'Plan',
|
||||
[string]$GpoProfile,
|
||||
[string]$GpoOutputPath,
|
||||
[ValidateSet('Reject','PromoteToBoth')][string]$GpoMinimumMode = 'Reject',
|
||||
[string]$IntuneProfile,
|
||||
[int]$IntuneBuild,
|
||||
[string]$IntuneEdition,
|
||||
@@ -109,6 +113,7 @@ Import-Module (Join-Path $ScriptRoot "modules/WefSubscriptions.psm1") -ErrorActi
|
||||
. (Join-Path $ScriptRoot "scripts/WefDeployment.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/RetentionHealth.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/TargetedSaclPlanning.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/GpoAuditPackages.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/IntuneAuditExport.ps1")
|
||||
|
||||
# 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。
|
||||
@@ -1804,6 +1809,10 @@ function Get-WelaUserProfiles {
|
||||
|
||||
$usage = @"
|
||||
Usage:
|
||||
./WELA.ps1 gpo-package -GpoAction Plan -GpoProfile wela-2.2.0 -Role Client -Build 26100
|
||||
./WELA.ps1 gpo-package -GpoAction Export -GpoProfile wela-2.2.0 -Role Client -Build 26100 -GpoOutputPath .\audit-components
|
||||
./WELA.ps1 gpo-package -GpoAction Verify -GpoOutputPath .\audit-components
|
||||
|
||||
./WELA.ps1 audit-integrity -IntegrityAction Audit -ResultsPath integrity.json
|
||||
./WELA.ps1 audit-integrity -IntegrityAction Plan -IntegrityProfile cis-server2022-v4-dc
|
||||
./WELA.ps1 audit-integrity -IntegrityAction Configure -IntegrityProfile cis-win11-v4-l1 -DryRun
|
||||
@@ -1877,6 +1886,10 @@ Write-Host ""
|
||||
Write-Host "WELA v$WELAVersion - $WELAReleaseName"
|
||||
Write-Host ""
|
||||
|
||||
if ($Cmd -ne 'gpo-package' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('GpoAction','GpoProfile','GpoOutputPath','GpoMinimumMode') }).Count) {
|
||||
throw 'GPO package options require gpo-package. No command was run.'
|
||||
}
|
||||
|
||||
if ($Cmd -ne 'intune-export' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('IntuneProfile','IntuneBuild','IntuneEdition','IntuneOutputPath','IntuneMinimumMode') }).Count) {
|
||||
throw 'Intune options require the offline intune-export command. No command was run.'
|
||||
}
|
||||
@@ -1952,7 +1965,7 @@ if ($Cmd -ne 'ad-object-sacl' -and @($PSBoundParameters.Keys | Where-Object {
|
||||
}).Count) {
|
||||
throw 'AD object SACL options require the dedicated ad-object-sacl command. No command was run.'
|
||||
}
|
||||
if ($DryRun -and -not ($Cmd -eq 'audit-integrity' -and $IntegrityAction -eq 'Configure') -and -not ($Cmd -eq 'audit-notifications' -and $NotificationAction -eq 'Configure') -and -not ($Cmd -eq 'ldap-diagnostics' -and $LdapAction -eq 'Configure') -and -not ($Cmd -eq 'applocker-readiness' -and $AppLockerAction -eq 'Import') -and $Cmd -notin @('configure', 'configure-eventlogs') -and
|
||||
if ($DryRun -and -not ($Cmd -eq 'gpo-package' -and $GpoAction -eq 'Export') -and -not ($Cmd -eq 'audit-integrity' -and $IntegrityAction -eq 'Configure') -and -not ($Cmd -eq 'audit-notifications' -and $NotificationAction -eq 'Configure') -and -not ($Cmd -eq 'ldap-diagnostics' -and $LdapAction -eq 'Configure') -and -not ($Cmd -eq 'applocker-readiness' -and $AppLockerAction -eq 'Import') -and $Cmd -notin @('configure', 'configure-eventlogs') -and
|
||||
-not ($Cmd -eq 'provider-packs' -and $ProviderAction -eq 'Configure') -and
|
||||
-not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure') -and
|
||||
-not ($Cmd -eq 'smb-auditing' -and $SmbAction -eq 'Configure') -and
|
||||
@@ -1961,7 +1974,7 @@ if ($DryRun -and -not ($Cmd -eq 'audit-integrity' -and $IntegrityAction -eq 'Con
|
||||
-not ($Cmd -in @('wef-source','wec-collector') -and $WefAction -eq 'Configure') -and
|
||||
-not ($Cmd -eq 'ad-object-sacl' -and $AdSaclAction -in @('Configure', 'Rollback')) -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, powershell-transcription -TranscriptionAction Configure, wmi-auditing -WmiAction Configure, channel-settings -ChannelAction Configure, wef-source/wec-collector -WefAction Configure, applocker-readiness -AppLockerAction Import, ad-object-sacl -AdSaclAction Configure|Rollback, ldap-diagnostics -LdapAction Configure, provider-packs -ProviderAction Configure, audit-integrity -IntegrityAction Configure, and audit-notifications -NotificationAction Configure. No command was run."
|
||||
throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs, firewall-logging -FirewallAction Configure, smb-auditing -SmbAction Configure, powershell-transcription -TranscriptionAction Configure, wmi-auditing -WmiAction Configure, channel-settings -ChannelAction Configure, wef-source/wec-collector -WefAction Configure, applocker-readiness -AppLockerAction Import, ad-object-sacl -AdSaclAction Configure|Rollback, ldap-diagnostics -LdapAction Configure, provider-packs -ProviderAction Configure, audit-integrity -IntegrityAction Configure, and audit-notifications -NotificationAction Configure; gpo-package -GpoAction Export writes component files only. 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.'
|
||||
@@ -1991,6 +2004,14 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi
|
||||
}
|
||||
|
||||
switch ($Cmd.ToLower()) {
|
||||
'gpo-package' {
|
||||
if ($Help) { Write-Host 'Usage: ./WELA.ps1 gpo-package [-GpoAction Plan|Export|Verify] [-GpoProfile profile-id -Role Client|MemberServer|DomainController|ADCS -Build number] [-GpoMinimumMode Reject|PromoteToBoth] [-IncludeOptional] [-GpoOutputPath directory] [-DryRun]. Export requires a fresh directory. These are offline components, not an importable GPO backup. See docs/gpo-audit-packages.md.'; return }
|
||||
if ($Profile -or $Baseline -or $HtmlPath -or $Auto -or $BackupPath -or $PlanPath -or $ResultsPath) { throw 'gpo-package uses GpoProfile and GpoOutputPath. Export contains its JSON manifest/review; other profile, result, backup and configuration options are unsupported.' }
|
||||
if ($GpoAction -eq 'Verify' -and @($PSBoundParameters.Keys|Where-Object {$_ -in @('GpoProfile','Role','Build','GpoMinimumMode','IncludeOptional')}).Count) {throw 'Verify reads package context; do not supply profile, role/build or expansion overrides.'}
|
||||
$report=Invoke-WelaGpoPackageCommand -Action $GpoAction -Profile $GpoProfile -Role $Role -Build $Build -MinimumMode $GpoMinimumMode -IncludeOptional:$IncludeOptional -Path $GpoOutputPath -DryRun:$DryRun
|
||||
$report
|
||||
if ($report.ExitCode) {exit $report.ExitCode}
|
||||
}
|
||||
'intune-export' {
|
||||
if ($Help) { Write-Host 'Usage: ./WELA.ps1 intune-export -IntuneProfile shared-profile-id -IntuneBuild 26100|26200 -IntuneEdition Pro|Enterprise|Education|IoTEnterprise -IntuneOutputPath new-local-directory [-IntuneMinimumMode Reject|PromoteToBoth] [-IncludeOptional]. Offline native audit artifacts only; no tenant or Windows changes. See docs/intune-audit-export.md.'; return }
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# GPO audit-policy deployment packages
|
||||
|
||||
`gpo-package` builds reviewable **offline components** from WELA's shared advanced audit-policy profiles. It does not generate a GPO backup, create/import a domain GPO, configure local policy or run LGPO. `Plan` is the default. The target role/build is explicitly declared by the operator; the exporter never assumes the machine running WELA is the deployment target.
|
||||
|
||||
```powershell
|
||||
./WELA.ps1 gpo-package -GpoProfile wela-2.2.0 -Role Client -Build 26100
|
||||
./WELA.ps1 gpo-package -GpoAction Export -GpoProfile wela-2.2.0 `
|
||||
-Role Client -Build 26100 -GpoOutputPath .\audit-components -DryRun
|
||||
./WELA.ps1 gpo-package -GpoAction Export -GpoProfile wela-2.2.0 `
|
||||
-Role Client -Build 26100 -GpoOutputPath .\audit-components
|
||||
./WELA.ps1 gpo-package -GpoAction Verify -GpoOutputPath .\audit-components
|
||||
|
||||
# A source minimum may need an explicit expansion to fit exact GPO semantics.
|
||||
./WELA.ps1 gpo-package -GpoAction Export `
|
||||
-GpoProfile microsoft-wef-reviewed-2026-09 -Role MemberServer -Build 20348 `
|
||||
-GpoMinimumMode PromoteToBoth -GpoOutputPath .\wef-audit-components
|
||||
```
|
||||
|
||||
Plan/Export require `-GpoProfile`, `-Role` and `-Build`, validated against the bundled profile's applicability. `-IncludeOptional` includes optional entries. Verify takes only its existing package path and reads context from the manifest; no context overrides are accepted. Use the returned PowerShell object's `Plan.Controls`/`Plan.Blockers` to inspect a plan. Export includes JSON and Markdown, so `-ResultsPath` and unrelated configuration/backup/profile options are rejected. `-DryRun` is supported only for Export and creates no files or directories. GPO-only parameters are rejected before unrelated profile command dispatch.
|
||||
|
||||
## Components and semantics
|
||||
|
||||
| File | Content |
|
||||
| --- | --- |
|
||||
| `audit.csv` | UTF-8 without BOM, CRLF, documented seven-column header; System subcategory GUIDs with exact positive values 1/2/3 only |
|
||||
| `GptTmpl.inf` | UTF-16LE with BOM; only `SCENoApplyLegacyAuditPolicy=1` as a DWORD security-template registry value |
|
||||
| `manifest.json` | Profile/version/hash, declared role/build, source provenance, all control dispositions, hashes/lengths/encodings and scope limits |
|
||||
| `review.md` | Full control list, exported and omitted settings, minimum expansions, prerequisites and source references |
|
||||
| `deployment.md` | Supported genuine-GPO preparation, reviewed create-unlinked procedure, validation and recovery boundaries |
|
||||
|
||||
The CSV follows [MS-GPAC message syntax](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/6494a0f2-8a16-40e2-b87d-328be7d732e0). It contains no per-user exclusions, global object SACLs or audit options. The precedence template follows the mechanism recommended in [MS-GPAC security considerations](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/e8edc8e2-4b91-433f-b1a2-672d4647e12f). Precedence is a prerequisite, not evidence of the winning domain policy or persistence.
|
||||
|
||||
| Shared profile mode | Export treatment |
|
||||
| --- | --- |
|
||||
| Exact 1, 2, 3 | Same exact mask; may turn off an opposite audit bit on a target |
|
||||
| Minimum 1 or 2 | Blocked by default; explicit `PromoteToBoth` emits exact 3 and records each expansion |
|
||||
| Minimum 3 | Exact 3, equivalent requirement |
|
||||
| Minimum 0 | Omitted; no required bits |
|
||||
| Optional | Omitted unless explicitly selected; selected positive mask is exact |
|
||||
| Unchanged / Not Configured / Not applicable | Omitted; neither zero nor a delete instruction is emitted |
|
||||
| Exact 0 / selected optional 0 | Blocked pending native validation of explicit-disable CSV semantics |
|
||||
| Reference-only Windows defaults | Deployment export blocked |
|
||||
|
||||
A GPO CSV has no dynamic operation equivalent to WELA's local minimum-mask OR. Promoting a minimum to Both avoids dropping an unknown existing bit but can increase volume. No host snapshot is invented to resolve this. The normative [system-audit value specification](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/21ad2e80-3011-48ef-be13-cc11ff7bfeb1) distinguishes unchanged 0 from None 4, while Microsoft's [combined example](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/d77939fe-8fdc-4d06-b08a-13670cc8cbe7) also labels a 0 row No Auditing. This implementation rejects explicit-disable exports instead of choosing unverified behavior. Current non-reference built-in profiles use positive explicit masks; defaults remain documentary only.
|
||||
|
||||
## File safety and verification scope
|
||||
|
||||
The output's parent directory must exist and the final output directory must be new. Existing files/directories are never reused, and observed symlink/junction components are refused. Export writes an owned sibling staging directory, verifies all expected bytes against the installed profile, then publishes it with a directory rename that refuses a concurrent destination. Verification runs again at the final path. A failure can retain staging/output evidence for review; remove only the owned failed artifact after checking it. No Windows-policy recovery is needed because this workflow changes only package files.
|
||||
|
||||
Verify requires exactly the five expected regular files. It checks hashes, lengths and encodings and regenerates the expected policy/review from the installed shared profile schema. Changing a CSV and updating its manifest hash is insufficient: the intended settings and source provenance must still match the installed profile. A profile/generator/guide change can require regeneration and review; retain the WELA version used to create older packages. Hashes provide consistency checks, not a digital signature or trusted origin. Protect both WELA's source and the package and reverify immediately before review/use; concurrent filesystem changes after a check are not prevented.
|
||||
|
||||
`ExitCode=0` means the plan has no blockers or component verification succeeded, including a dry-run that produced no package. It does not mean a GPO was created, imported, linked or applied. Reports always retain `ImportableGpoBackup=false`, `DeploymentVerified=false` and `SigmaEvtxCredit=0`.
|
||||
|
||||
Only built-in advanced Security audit profiles and the precedence template are supported. Other registry controls, event-log sizing/retention/ACLs, SACLs, AD CS filters, WEF, PowerShell, firewall/SMB/provider settings, privileges and diagnostics are explicitly listed as unsupported. Sysmon is excluded. Follow [the deployment guide](gpo-package-deployment.md) for a genuine GPMC/LGPO preparation path and remaining domain acceptance work. Do not rename this folder or manufacture backup XML to make it look importable.
|
||||
|
||||
## Tests and remaining evidence
|
||||
|
||||
The offline suite covers shared source/role/build selection, every mode/mask translation, exact-zero/default refusal, optional/role omissions, source fingerprint drift, CSV/template contents, tampering even with updated hashes, fresh-directory collisions, dry-run, filesystem guards and public CLI option boundaries. The Windows workflow targets Server 2022/2025 under PowerShell 5.1 and 7, verifies cross-edition package compatibility, calls native `secedit /validate` on the generated template and checks unchanged effective audit masks/precedence. It does not apply audit CSV or create a domain GPO.
|
||||
|
||||
Accepted file syntax and package round-trips do not establish domain import, exact-disable behavior, GPO propagation or event generation. Genuine backup preparation, create-unlinked/import/readback, client/member/DC/AD CS lab application and benign event/collector evidence remain pending. Issue #2 therefore retains deployment acceptance work beyond this package feature.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Preparing and reviewing a genuine audit-policy GPO
|
||||
|
||||
This WELA folder contains **deployment components, not a GPO backup**. Do not pass it to `Import-GPO`, copy it into SYSVOL, fabricate `Backup.xml`, or edit a genuine archived backup to insert its files. Microsoft directs administrators to manage archived backups through GPMC. No domain creation, import, linking, filtering, delegation or policy refresh is performed by WELA's package commands. [Microsoft backup/import guidance](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/group-policy/group-policy-backup-restore).
|
||||
|
||||
## Prepare the source policy in an isolated domain
|
||||
|
||||
1. Verify the component package with the same reviewed WELA version. Read `review.md`, including every omitted/expanded row and object/service prerequisites. Confirm the intended target role, build, scope and source version; these are declared package inputs, not observations of a domain's computers.
|
||||
2. On a disposable, snapshotted lab, use GPMC to create a **new unlinked source GPO**. Leave existing GPOs, default domain policies and links untouched. In the Group Policy Management Editor, enter only the exported rows under Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy Configuration. `ExportMask` 1 is Success, 2 is Failure, 3 is Success and Failure. Leave omitted rows Not Configured in this new GPO; do not interpret omission as disabling auditing.
|
||||
3. In the same GPO, enable Security Options > **Audit: Force audit policy subcategory settings (Windows Vista or later) to override audit policy category settings**. The companion `GptTmpl.inf` shows the exact DWORD requirement. Do not add legacy category audit policy, audit failure options, privileges, SACLs or unrelated registry settings.
|
||||
4. Review GPMC's Settings report against the package. Require an exact match for the selected audit GUIDs/masks and precedence=1, with no unexpected Computer or User policy settings. Confirm no links or WMI filter. Review the GPO's security filtering/delegation independently. Back up this source GPO through GPMC or `Backup-GPO` into a fresh protected directory, retaining its **backup-instance ID**, source GPO ID, report and hashes.
|
||||
|
||||
This manual source-GPO preparation is the supported path for a narrow, genuine domain backup. Microsoft documents creating an [unlinked GPO](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/group-policy/group-policy-management-console) and generating backups through [Backup-GPO](https://learn.microsoft.com/en-us/powershell/module/grouppolicy/backup-gpo?view=windowsserver2025-ps).
|
||||
|
||||
## Optional LGPO lab route
|
||||
|
||||
Obtain Microsoft's signed LGPO utility and its documentation from the [Security Compliance Toolkit](https://www.microsoft.com/en-us/download/details.aspx?id=55319); WELA does not bundle or execute it. LGPO v3 documents `/s GptTmpl.inf` for a security template, `/a audit.csv` for advanced auditing and `/b directory /n display-name` for a genuine local-policy backup. These **apply modes change the lab host** and are outside the offline package workflow. Snapshot and record the lab's policy first; inspect all generated settings and compare effective policy before/after. Do not run them on a shared or production administrator workstation.
|
||||
|
||||
`/a` and `/ac` differ: `/ac` clears existing advanced auditing before application and copies the CSV into local policy. WELA does not provide a clearing command. `/a` must not be presented as proof of persistent GPO configuration: local policy editor state, effective AuditPol state and the audit client-side extension are separate observations. LGPO `/e audit` enables that extension for local processing, but neither its use nor successful import proves correct later policy application.
|
||||
|
||||
LGPO `/b` backs up local policy, including security settings, current advanced audit state, registry policy and configured extensions. **Its output can include settings absent from this package**, even on a lab machine. Review the complete backup in GPMC. If it contains extras, edit a newly created isolated source GPO through GPMC and make a new genuine backup; do not remove files or patch XML inside the archive. A generic local-policy backup is not automatically a narrow WELA audit-only backup. Microsoft explains the difference between [AuditPol state and local policy](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/auditpol-local-security-policy-results-differ).
|
||||
|
||||
## Reviewed create-new-unlinked procedure
|
||||
|
||||
The following is an **operator procedure for an already prepared, genuine, fully reviewed backup**. It is not generated executable content and WELA does not run it. Review its backup by instance ID through GPMC's Manage Backups/Settings report (or the native `GPMBackupDir.GetBackup` and `GPMBackup.GenerateReport` APIs). A CSV folder is insufficient. Require only the intended computer audit settings and precedence, with no unknown extensions, scripts, preferences, per-user audit entries or unrelated settings.
|
||||
|
||||
Use explicit domain and DC parameters for every operation. Check connectivity/authority before creation and do not treat an access error as an absent GPO. Choose a fresh unique name. For example, in an authorized lab using Windows PowerShell and the GroupPolicy module:
|
||||
|
||||
```powershell
|
||||
# Replace these with the explicitly reviewed lab domain, DC, genuine backup and name.
|
||||
$targetDomain = 'lab.example.test'
|
||||
$targetDc = 'dc01.lab.example.test'
|
||||
$genuineBackupRoot = 'C:\ReviewedGpoBackups'
|
||||
$reviewedBackupId = [guid]'11111111-1111-1111-1111-111111111111'
|
||||
$newName = 'WELA Audit - reviewed lab candidate'
|
||||
$receiptPath = 'C:\Review\new-gpo-receipt.json' # Must not already exist.
|
||||
|
||||
if (Test-Path -LiteralPath $receiptPath) { throw 'Use a fresh receipt path.' }
|
||||
$existing = @(Get-GPO -All -Domain $targetDomain -Server $targetDc -ErrorAction Stop |
|
||||
Where-Object DisplayName -eq $newName)
|
||||
if ($existing.Count) { throw 'The target name already exists; stop without importing.' }
|
||||
|
||||
# Do not specify a Starter GPO or pipe to New-GPLink.
|
||||
$created = New-GPO -Name $newName -Domain $targetDomain -Server $targetDc -ErrorAction Stop
|
||||
[pscustomobject]@{
|
||||
Domain = $targetDomain; Server = $targetDc; GpoGuid = $created.Id
|
||||
Name = $created.DisplayName; BackupId = $reviewedBackupId
|
||||
State = 'Created; import and verification pending'
|
||||
} | ConvertTo-Json | Out-File -LiteralPath $receiptPath -Encoding UTF8 -NoClobber -ErrorAction Stop
|
||||
|
||||
# Recheck that this exact new GPO is empty and unlinked before importing.
|
||||
Get-GPOReport -Guid $created.Id -Domain $targetDomain -Server $targetDc -ReportType Xml
|
||||
```
|
||||
|
||||
Stop and review the returned GUID, persisted receipt and actual report. Only after confirming that the exact new GPO remains empty and unlinked, execute this separate import step in the same reviewed session:
|
||||
|
||||
```powershell
|
||||
Import-GPO -BackupId $reviewedBackupId -Path $genuineBackupRoot `
|
||||
-TargetGuid $created.Id -Domain $targetDomain -Server $targetDc -ErrorAction Stop
|
||||
Get-GPOReport -Guid $created.Id -Domain $targetDomain -Server $targetDc -ReportType Xml
|
||||
```
|
||||
|
||||
`New-GPO` creates an unlinked object and refuses a duplicate name. `Import-GPO` imports into the returned **new GUID**, without `-CreateIfNeeded`, name-based targeting or `Restore-GPO`. Importing settings preserves the destination's existing security filtering and links; it does not supply an approved scope of application. Review the actual result, including exact settings and continued absence of links, before considering any later link. Do not assume the backup's security filtering or role metadata protects the new GPO. [New-GPO](https://learn.microsoft.com/en-us/powershell/module/grouppolicy/new-gpo?view=windowsserver2025-ps), [Import-GPO](https://learn.microsoft.com/en-us/powershell/module/grouppolicy/import-gpo?view=windowsserver2025-ps).
|
||||
|
||||
If receipt persistence, import or readback fails, stop and retain the created GUID and evidence for manual review. Do not retry against an arbitrary existing GPO or automatically delete an object that another administrator may have changed or linked. This procedure is not a transaction or a lock against concurrent administrators; check the same DC immediately before each operation. Inspect both AD/SYSVOL versions and replication before proceeding beyond the unlinked candidate.
|
||||
|
||||
## Deployment acceptance and recovery
|
||||
|
||||
After separate approval of scope, stage any linking on an isolated test OU containing only representative disposable clients/servers. Plan role/build targeting and filtering explicitly; WELA's declared Role/Build is documentation, not a generated WMI or security filter. Check precedence, link order, enforcement, inheritance and resultant policy. No deployment link/force-refresh command is included here.
|
||||
|
||||
Record the genuine backup, target GUID, actual GPMC settings, RSoP/GPO source evidence, effective audit masks after ordinary policy processing, relevant SACL/service prerequisites, benign generated XML and collector arrival. Unlinked creation/import does not prove any client applied the settings. More specific or enforced policy can change the result. [Group Policy processing](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/group-policy/group-policy-processing).
|
||||
|
||||
Rollback must be designed before linking. Preserve existing production GPOs and links throughout preparation. If testing fails, the policy owner should inspect and selectively reverse only the test changes, considering current links, authoritative settings and replication. Unlinking or deleting a GPO is not proof that all effective settings reverted. Retain evidence rather than blindly restoring an old whole-host policy snapshot. No audit-exhaustion test is required or provided.
|
||||
|
||||
WELA's automated tests validate package contents and unchanged host settings only. Genuine GPO creation/import, absence of unintended policy settings, AD/SYSVOL replication, client/DC/AD CS application and event/collection evidence remain pending lab acceptance for issue #2. Other WELA controls and Sysmon are outside this package.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Offline deployment components only. This script never calls GPMC, LGPO or Windows policy writers.
|
||||
function ConvertTo-WelaGpoPackagePlan {
|
||||
param($ProfilePlan,[ValidateSet('Reject','PromoteToBoth')][string]$MinimumMode='Reject')
|
||||
$rows=@();$blockers=@()
|
||||
if ($ProfilePlan.referenceOnly) { $blockers+='Reference-only Windows defaults cannot be exported as a deployment policy.' }
|
||||
foreach ($policy in $ProfilePlan.policies) {
|
||||
$mask=$null;$disposition='Omitted';$reason=$policy.mode
|
||||
if ($policy.mode -eq 'optional' -and -not $ProfilePlan.includeOptional) { $reason='Optional control not selected.' }
|
||||
elseif ($policy.mode -in @('exact','minimum','optional')) {
|
||||
if ($policy.mode -eq 'minimum' -and $policy.requiredMask -eq 0) { $reason='Minimum zero imposes no requirement; preserve by omission.' }
|
||||
elseif ($policy.mode -eq 'minimum' -and $policy.requiredMask -in @(1,2) -and $MinimumMode -eq 'Reject') {
|
||||
$disposition='Blocked';$reason='GPO stores an exact mask. Explicit PromoteToBoth is required to avoid turning off an existing opposite audit bit.'
|
||||
} elseif ($policy.requiredMask -eq 0) {
|
||||
$disposition='Blocked';$reason='Exact No Auditing export is unsupported: normative MS-GPAC uses value 4 for None, but a Microsoft example uses 0. Native deployment semantics remain unvalidated.'
|
||||
} else {
|
||||
$disposition='Exported'
|
||||
$mask=if ($policy.mode -eq 'minimum') {3} else {[int]$policy.requiredMask}
|
||||
$reason=if ($policy.mode -eq 'minimum' -and $policy.requiredMask -ne 3) {'Explicit expansion: minimum Success or Failure becomes exact Success and Failure; additional event volume is possible.'}
|
||||
elseif ($policy.mode -eq 'minimum') {'Minimum Both is equivalent to exact Both.'}
|
||||
else {'Exact source mask; an opposite audit bit may be disabled when deployed. Target effective state is not assessed.'}
|
||||
}
|
||||
}
|
||||
if ($disposition -eq 'Blocked') { $blockers+="$($policy.id): $reason" }
|
||||
$rows+=[pscustomobject][ordered]@{
|
||||
Name=$policy.id;Guid=$policy.guid.ToUpperInvariant();Category=$policy.category;SourceMode=$policy.mode;RequiredMask=$policy.requiredMask
|
||||
Disposition=$disposition;ExportMask=$mask;Reason=$reason;SourceIds=@($policy.sourceIds);Prerequisites=@($policy.prerequisites);SourceNote=$policy.note
|
||||
}
|
||||
}
|
||||
if (-not @($rows|Where-Object Disposition -eq 'Exported').Count) { $blockers+='No applicable system audit subcategories were selected for export.' }
|
||||
[pscustomobject][ordered]@{
|
||||
SchemaVersion=1;Kind='WelaGpoAuditDeploymentComponents';Scope='advanced-audit-policy-and-precedence-components-only'
|
||||
Profile=$ProfilePlan.profile;ProfileVersion=$ProfilePlan.version;Role=$ProfilePlan.role;Build=$ProfilePlan.build
|
||||
ContextBasis='Operator-declared target role/build; no host or domain observations.'
|
||||
IncludeOptional=[bool]$ProfilePlan.includeOptional;MinimumMode=$MinimumMode;ProfileSchemaSha256=$ProfilePlan.schemaSha256.ToLowerInvariant()
|
||||
Sources=@($ProfilePlan.provenance);Controls=$rows;Blockers=$blockers
|
||||
Precedence=[pscustomobject][ordered]@{Path='MACHINE\System\CurrentControlSet\Control\Lsa\SCENoApplyLegacyAuditPolicy';Type='REG_DWORD';Value=1;Source='https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/e8edc8e2-4b91-433f-b1a2-672d4647e12f'}
|
||||
ImportableGpoBackup=$false;DeploymentVerified=$false;SigmaEvtxCredit=0
|
||||
UnsupportedControls=@('Event channel enablement, size, retention, permissions and WEF/collector configuration','Object, directory, registry, AD and WMI SACLs; AD CS AuditFilter','PowerShell module/script-block/transcription and process-command-line policies','Audit privileges, CrashOnAuditFail, notifications and diagnostic controls','Firewall text logs, SMB auditing, AppLocker and other provider-specific settings','Domain GPO creation, import, linking, filtering, delegation and client refresh')
|
||||
}
|
||||
}
|
||||
function Get-WelaGpoPackagePlan {
|
||||
param([Parameter(Mandatory)][string]$Profile,[Parameter(Mandatory)][ValidateSet('Client','MemberServer','DomainController','ADCS')][string]$Role,
|
||||
[Parameter(Mandatory)][ValidateRange(1,999999)][int]$Build,[ValidateSet('Reject','PromoteToBoth')][string]$MinimumMode='Reject',[switch]$IncludeOptional)
|
||||
$path=Join-Path $PSScriptRoot '../config/audit_profiles.json'
|
||||
$before=(Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash
|
||||
$profilePlan=Get-WelaAuditProfilePlan -Profile $Profile -Role $Role -Build $Build -IncludeOptional:$IncludeOptional
|
||||
$after=(Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash
|
||||
if ($before -ne $after -or $after -ne $profilePlan.schemaSha256) { throw 'Shared audit profile source changed during planning; retry with a consistent source.' }
|
||||
ConvertTo-WelaGpoPackagePlan -ProfilePlan $profilePlan -MinimumMode $MinimumMode
|
||||
}
|
||||
function Get-WelaGpoComponentContent {
|
||||
param($Plan)
|
||||
if ($Plan.Blockers.Count) { throw ('Package export blocked: '+($Plan.Blockers -join ' ')) }
|
||||
$csv=@('Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value')
|
||||
foreach ($row in $Plan.Controls) {
|
||||
if ($row.Disposition -ne 'Exported') { continue }
|
||||
# Built-in catalog labels are non-executable readable names; GUID/value govern policy.
|
||||
$name=$row.Name
|
||||
if ($name -match '[,"\r\n]' -or $row.ExportMask -notin @(1,2,3)) { throw 'Unsupported audit CSV label or exact mask.' }
|
||||
$csv+=",System,$name,{$($row.Guid)},$(Format-WelaAuditMask $row.ExportMask),,$($row.ExportMask)"
|
||||
}
|
||||
$template=@('[Unicode]','Unicode=yes','[Version]','signature="$CHICAGO$"','Revision=1','[Registry Values]',($Plan.Precedence.Path+'=4,1'))
|
||||
$review=@('# WELA GPO audit-policy deployment components','',
|
||||
'**This component folder is not a GPO backup and cannot be passed to Import-GPO. No host or domain policy has been changed.**','',
|
||||
"Profile: $($Plan.Profile) ($($Plan.ProfileVersion)); declared target: $($Plan.Role), build $($Plan.Build).",
|
||||
"Minimum policy: $($Plan.MinimumMode); optional controls selected: $($Plan.IncludeOptional).",
|
||||
"Shared profile SHA-256: $($Plan.ProfileSchemaSha256)",'',
|
||||
'The security template specifies only SCENoApplyLegacyAuditPolicy=1 (DWORD). Review precedence and effective policy separately after deployment.',
|
||||
'Omitted rows are absent from audit.csv. They do not clear settings supplied by another GPO, and they do not mean No Auditing.','',
|
||||
'| Subcategory / GUID | Source mode / mask | Export disposition / mask | Reason and prerequisites |','| --- | --- | --- | --- |')
|
||||
foreach ($row in $Plan.Controls) {
|
||||
$reason=($row.Reason+' '+($row.Prerequisites -join '; ')+' '+$row.SourceNote).Replace('|','\|').Replace("`r",' ').Replace("`n",' ')
|
||||
$review+="| $($row.Name) / $($row.Guid) | $($row.SourceMode) / $($row.RequiredMask) | $($row.Disposition) / $($row.ExportMask) | $reason |"
|
||||
}
|
||||
$review+=@('','## Unsupported scope','')+@($Plan.UnsupportedControls|ForEach-Object {'- '+$_})
|
||||
$review+=@('','## Profile source provenance','')
|
||||
foreach ($source in $Plan.Sources) { $review+="- $($source.id): $($source.source.title); $($source.source.version). $($source.source.url)" }
|
||||
$review+=@('',
|
||||
'Component validation does not prove GPO import, replication, client application, event generation or ingestion. Sigma EVTX credit remains zero. See deployment.md for preparation, staged review and recovery.')
|
||||
[ordered]@{
|
||||
'audit.csv'=[pscustomobject]@{Text=($csv -join "`r`n")+"`r`n";Encoding='utf-8'}
|
||||
'GptTmpl.inf'=[pscustomobject]@{Text=($template -join "`r`n")+"`r`n";Encoding='utf-16le-bom'}
|
||||
'review.md'=[pscustomobject]@{Text=($review -join "`r`n")+"`r`n";Encoding='utf-8'}
|
||||
'deployment.md'=[pscustomobject]@{Text=(Get-Content -LiteralPath (Join-Path $PSScriptRoot '../docs/gpo-package-deployment.md') -Raw -Encoding UTF8 -ErrorAction Stop).Replace("`r`n","`n").Replace("`n","`r`n");Encoding='utf-8'}
|
||||
}
|
||||
}
|
||||
function Get-WelaGpoContentBytes {
|
||||
param($Component)
|
||||
if ($Component.Encoding -eq 'utf-8') { $encoding=New-Object Text.UTF8Encoding($false) }
|
||||
elseif ($Component.Encoding -eq 'utf-16le-bom') { $encoding=New-Object Text.UnicodeEncoding($false,$true) }
|
||||
else { throw 'Unknown component encoding.' }
|
||||
return ,([byte[]](@($encoding.GetPreamble())+@($encoding.GetBytes($Component.Text))))
|
||||
}
|
||||
function Get-WelaGpoBytesHash {
|
||||
param([byte[]]$Bytes)
|
||||
$sha=[Security.Cryptography.SHA256]::Create()
|
||||
try { ([BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-','').ToLowerInvariant() } finally {$sha.Dispose()}
|
||||
}
|
||||
function Resolve-WelaGpoPackagePath {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
$provider=$null;$drive=$null
|
||||
$full=$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path,[ref]$provider,[ref]$drive)
|
||||
if ($provider.Name -ne 'FileSystem') { throw 'Package paths must use the filesystem.' }
|
||||
$full=[IO.Path]::GetFullPath($full)
|
||||
if ($full.StartsWith('\\?\') -or $full.StartsWith('\\.\')) {throw 'Device paths are unsupported.'}
|
||||
# Reject observed symbolic links/junctions, including ancestors. No traversal through them.
|
||||
$check=$full
|
||||
while ($check) {
|
||||
if (Test-Path -LiteralPath $check) {
|
||||
$item=Get-Item -LiteralPath $check -Force -ErrorAction Stop
|
||||
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {throw 'Reparse-point package paths are unsupported.'}
|
||||
}
|
||||
$parent=[IO.Directory]::GetParent($check)
|
||||
if (-not $parent) {break};$check=$parent.FullName
|
||||
}
|
||||
return $full
|
||||
}
|
||||
function Test-WelaGpoPackage {
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
$full=Resolve-WelaGpoPackagePath $Path
|
||||
if (-not (Test-Path -LiteralPath $full -PathType Container)) {throw 'Package directory does not exist.'}
|
||||
$names=@('audit.csv','GptTmpl.inf','review.md','deployment.md','manifest.json')
|
||||
$files=@(Get-ChildItem -LiteralPath $full -Force -ErrorAction Stop)
|
||||
if ($files.Count -ne $names.Count -or @($files|Where-Object {$_.PSIsContainer -or $_.Name -cnotin $names -or ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0}).Count) {throw 'Package must contain exactly the five expected regular files, with no reparse points.'}
|
||||
foreach ($file in $files) {if ($file.Length -gt 4194304) {throw 'Component exceeds the supported size limit.'}}
|
||||
$manifest=Get-Content -LiteralPath (Join-Path $full 'manifest.json') -Raw -Encoding UTF8 -ErrorAction Stop|ConvertFrom-Json -ErrorAction Stop
|
||||
if (@($manifest.PSObject.Properties.Name).Count -ne 5 -or @($manifest.PSObject.Properties.Name|Where-Object {$_ -cnotin @('SchemaVersion','Kind','CreatedUtc','Plan','Files')}).Count) {throw 'Unexpected manifest metadata.'}
|
||||
$created=[DateTimeOffset]::MinValue
|
||||
if (-not [DateTimeOffset]::TryParse([string]$manifest.CreatedUtc,[ref]$created)) {throw 'Invalid manifest creation timestamp.'}
|
||||
if ($manifest.SchemaVersion -ne 1 -or $manifest.Kind -cne 'WelaGpoAuditDeploymentComponents' -or $manifest.Plan.IncludeOptional -isnot [bool] -or $manifest.Files.Count -ne 4) {throw 'Invalid deployment component manifest.'}
|
||||
$expected=Get-WelaGpoPackagePlan -Profile $manifest.Plan.Profile -Role $manifest.Plan.Role -Build $manifest.Plan.Build -MinimumMode $manifest.Plan.MinimumMode -IncludeOptional:$manifest.Plan.IncludeOptional
|
||||
if ((ConvertTo-Json -InputObject $manifest.Plan -Depth 18 -Compress) -cne (ConvertTo-Json -InputObject $expected -Depth 18 -Compress)) {throw 'Manifest intent/provenance does not match the installed shared profile. Regenerate and review the package.'}
|
||||
$content=Get-WelaGpoComponentContent $expected
|
||||
$seen=@{}
|
||||
foreach ($entry in $manifest.Files) {
|
||||
if (@($entry.PSObject.Properties.Name).Count -ne 4 -or @($entry.PSObject.Properties.Name|Where-Object {$_ -cnotin @('Name','Encoding','Length','Sha256')}).Count) {throw 'Unexpected component metadata.'}
|
||||
if ($entry.Name -cnotin @($content.Keys) -or $seen.ContainsKey($entry.Name)) {throw 'Unexpected or duplicate manifest component.'}
|
||||
$seen[$entry.Name]=$true
|
||||
$bytes=[IO.File]::ReadAllBytes((Join-Path $full $entry.Name))
|
||||
$hash=Get-WelaGpoBytesHash $bytes
|
||||
$expectedHash=Get-WelaGpoBytesHash (Get-WelaGpoContentBytes $content[$entry.Name])
|
||||
if ($entry.Encoding -cne $content[$entry.Name].Encoding -or $entry.Sha256 -cne $hash -or $entry.Length -ne $bytes.Length -or $hash -cne $expectedHash) {throw "Component content/hash differs from the reviewed installed profile: $($entry.Name)"}
|
||||
}
|
||||
[pscustomobject]@{Scope=$expected.Scope;Action='Verify';ExitCode=0;Path=$full;ComponentValidation='Matches installed shared profile and component generator';ImportableGpoBackup=$false;DeploymentVerified=$false;SigmaEvtxCredit=0;Plan=$expected}
|
||||
}
|
||||
function Export-WelaGpoPackage {
|
||||
param($Plan,[Parameter(Mandatory)][string]$Path,[switch]$DryRun)
|
||||
if ($Plan.Blockers.Count) {throw ('Package export blocked: '+($Plan.Blockers -join ' '))}
|
||||
$full=Resolve-WelaGpoPackagePath $Path
|
||||
if (Test-Path -LiteralPath $full) {throw 'Export requires a fresh output directory; existing files and directories are never reused.'}
|
||||
$parent=[IO.Directory]::GetParent($full)
|
||||
if (-not $parent -or -not (Test-Path -LiteralPath $parent.FullName -PathType Container)) {throw 'The output parent directory must already exist.'}
|
||||
$content=Get-WelaGpoComponentContent $Plan
|
||||
if ($DryRun) {return [pscustomobject]@{Scope=$Plan.Scope;Action='Export';ExitCode=0;DryRun=$true;Path=$full;ComponentValidation='Not exported';ImportableGpoBackup=$false;DeploymentVerified=$false;SigmaEvtxCredit=0;Plan=$Plan}}
|
||||
$stage=Join-Path $parent.FullName ('.wela-gpo-stage-'+[guid]::NewGuid().ToString('N'))
|
||||
$null=New-Item -ItemType Directory -Path $stage -ErrorAction Stop
|
||||
try {
|
||||
$entries=@()
|
||||
foreach ($name in $content.Keys) {
|
||||
$bytes=Get-WelaGpoContentBytes $content[$name]
|
||||
$stream=[IO.File]::Open((Join-Path $stage $name),[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
|
||||
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush()} finally {$stream.Dispose()}
|
||||
$entries+=[pscustomobject][ordered]@{Name=$name;Encoding=$content[$name].Encoding;Length=$bytes.Length;Sha256=(Get-WelaGpoBytesHash $bytes)}
|
||||
}
|
||||
$manifest=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaGpoAuditDeploymentComponents';CreatedUtc=[DateTime]::UtcNow.ToString('o');Plan=$Plan;Files=$entries}
|
||||
[IO.File]::WriteAllText((Join-Path $stage 'manifest.json'),(ConvertTo-Json -InputObject $manifest -Depth 18),(New-Object Text.UTF8Encoding($false)))
|
||||
$null=Test-WelaGpoPackage $stage
|
||||
$null=Resolve-WelaGpoPackagePath $full
|
||||
# Directory.Move refuses a raced destination; no overwrite or recursive merge.
|
||||
[IO.Directory]::Move($stage,$full)
|
||||
$result=Test-WelaGpoPackage $full
|
||||
$result.Action='Export';$result|Add-Member NoteProperty DryRun $false
|
||||
return $result
|
||||
} catch {throw "Component export did not complete: $($_.Exception.Message) Review any retained staging directory '$stage' or output '$full'; no Windows policy was changed."}
|
||||
}
|
||||
function Invoke-WelaGpoPackageCommand {
|
||||
param([ValidateSet('Plan','Export','Verify')][string]$Action='Plan',[string]$Profile,[string]$Role,[int]$Build,
|
||||
[ValidateSet('Reject','PromoteToBoth')][string]$MinimumMode='Reject',[switch]$IncludeOptional,[string]$Path,[switch]$DryRun)
|
||||
if ($DryRun -and $Action -ne 'Export') {throw '-DryRun requires GpoAction Export; Plan and Verify are read-only.'}
|
||||
if ($Action -eq 'Verify') {
|
||||
if ($Profile -or $Role -or $Build -or $IncludeOptional -or $MinimumMode -ne 'Reject') {throw 'Verify reads the package context; profile, role, build and expansion overrides are unsupported.'}
|
||||
if (-not $Path) {throw 'Verify requires -GpoOutputPath.'}
|
||||
return Test-WelaGpoPackage $Path
|
||||
}
|
||||
if (-not $Profile -or -not $Role -or -not $Build) {throw 'Plan and Export require explicit -GpoProfile, -Role and -Build for the declared deployment target.'}
|
||||
$plan=Get-WelaGpoPackagePlan -Profile $Profile -Role $Role -Build $Build -MinimumMode $MinimumMode -IncludeOptional:$IncludeOptional
|
||||
if ($Action -eq 'Export') {
|
||||
if (-not $Path) {throw 'Export requires -GpoOutputPath.'}
|
||||
return Export-WelaGpoPackage -Plan $plan -Path $Path -DryRun:$DryRun
|
||||
}
|
||||
if ($Path) {throw 'GpoOutputPath is supported only with Export or Verify.'}
|
||||
[pscustomobject]@{Scope=$plan.Scope;Action='Plan';ExitCode=$(if ($plan.Blockers.Count){1}else{0});ImportableGpoBackup=$false;DeploymentVerified=$false;SigmaEvtxCredit=0;Plan=$plan}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
# Offline fixtures only; no LGPO, auditpol mutation, GPMC or domain operations.
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
|
||||
. (Join-Path $repo 'scripts/GpoAuditPackages.ps1')
|
||||
$script:checks=0
|
||||
function Assert($Value,[string]$Message){if(-not $Value){throw "FAIL: $Message"};$script:checks++}
|
||||
function Reject([scriptblock]$Code,[string]$Pattern){$message='';try{& $Code|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, got $message"}
|
||||
function New-GPO {throw 'Forbidden domain mutation'}
|
||||
function Import-GPO {throw 'Forbidden domain mutation'}
|
||||
function Set-WelaEffectiveAuditPolicy {throw 'Forbidden policy mutation'}
|
||||
function Invoke-WelaNative {throw 'Forbidden native process'}
|
||||
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-gpo-tests-'+[guid]::NewGuid().ToString('N'))
|
||||
$null=New-Item -ItemType Directory -Path $temp
|
||||
function FreshPath {Join-Path $temp ([guid]::NewGuid().ToString('N'))}
|
||||
function Clone($Value){ConvertTo-Json -InputObject $Value -Depth 18|ConvertFrom-Json}
|
||||
function SaveManifest($Path,$Manifest){$Manifest|ConvertTo-Json -Depth 18|Set-Content -LiteralPath (Join-Path $Path 'manifest.json') -Encoding UTF8}
|
||||
try {
|
||||
$plan=Get-WelaGpoPackagePlan -Profile wela-2.2.0 -Role Client -Build 26100
|
||||
Assert ($plan.Controls.Count -eq 59 -and $plan.Blockers.Count -eq 0) 'Shared catalog produces the full review, including omitted controls'
|
||||
Assert ($plan.ContextBasis -match 'Operator-declared' -and $plan.SigmaEvtxCredit -eq 0 -and -not $plan.ImportableGpoBackup -and -not $plan.DeploymentVerified) 'Declared deployment context is never host, GPO or detection evidence'
|
||||
Assert ($plan.ProfileSchemaSha256 -match '^[a-f0-9]{64}$' -and $plan.Sources.Count -gt 0 -and $plan.UnsupportedControls.Count -gt 0) 'Source fingerprint, provenance and unsupported scopes stay visible'
|
||||
Assert (@($plan.Controls|Where-Object {$_.Name -eq 'Directory Service Changes' -and $_.Disposition -eq 'Omitted'}).Count -eq 1) 'DC-only directory auditing is omitted on a client'
|
||||
$dc=Get-WelaGpoPackagePlan -Profile wela-2.2.0 -Role DomainController -Build 20348
|
||||
Assert (@($dc.Controls|Where-Object {$_.Name -eq 'Directory Service Changes' -and $_.Disposition -eq 'Exported'}).Count -eq 1) 'The same source respects explicit DC scope'
|
||||
$ca=Get-WelaGpoPackagePlan -Profile wela-2.2.0 -Role ADCS -Build 20348
|
||||
Assert (@($ca.Controls|Where-Object {$_.Name -eq 'Certification Services' -and $_.Disposition -eq 'Exported'}).Count -eq 1) 'AD CS target audit policy is retained without claiming AuditFilter or SACL configuration'
|
||||
Reject {Get-WelaGpoPackagePlan -Profile cis-server2022-v4-l1 -Role Client -Build 26100} 'does not support'
|
||||
Reject {Get-WelaGpoPackagePlan -Profile unknown -Role Client -Build 26100} 'Unknown audit profile'
|
||||
$reference=Get-WelaGpoPackagePlan -Profile windows-defaults-reviewed-2026-09 -Role Client -Build 26100
|
||||
Assert ($reference.Blockers -match 'Reference-only') 'Documentary defaults cannot become deployment policy'
|
||||
$minimum=Get-WelaGpoPackagePlan -Profile microsoft-wef-reviewed-2026-09 -Role MemberServer -Build 20348
|
||||
Assert ($minimum.Blockers.Count -gt 0 -and @($minimum.Controls|Where-Object {$_.SourceMode -eq 'minimum' -and $_.RequiredMask -in @(1,2) -and $_.Disposition -eq 'Exported'}).Count -eq 0) 'Minimum masks cannot silently become restrictive exact masks'
|
||||
$both=Get-WelaGpoPackagePlan -Profile microsoft-wef-reviewed-2026-09 -Role MemberServer -Build 20348 -MinimumMode PromoteToBoth
|
||||
Assert ($both.Blockers.Count -eq 0 -and @($both.Controls|Where-Object {$_.SourceMode -eq 'minimum' -and $_.RequiredMask -in @(1,2) -and $_.ExportMask -eq 3 -and $_.Reason -match 'expansion'}).Count -gt 0) 'Explicit promotion records each expanded exact Both mask'
|
||||
# Exercise all semantic states independently of which happen to exist in today's sources.
|
||||
$raw=Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role Client -Build 26100
|
||||
$fixture=Clone $raw;$base=$fixture.policies[0];$fixture.policies=@()
|
||||
foreach($setting in @(@('exact',1),@('exact',2),@('exact',3),@('minimum',0),@('minimum',1),@('minimum',2),@('minimum',3),@('not-configured',$null),@('unchanged',$null),@('not-applicable',$null),@('optional',3))) {
|
||||
$row=Clone $base;$row.mode=$setting[0];$row.requiredMask=$setting[1];$fixture.policies+=@($row)
|
||||
}
|
||||
$translated=ConvertTo-WelaGpoPackagePlan $fixture -MinimumMode PromoteToBoth
|
||||
Assert (($translated.Controls[0..2].ExportMask -join ',') -eq '1,2,3') 'Exact positive masks preserve their precise meaning'
|
||||
Assert ($translated.Controls[3].Disposition -eq 'Omitted' -and ($translated.Controls[4..6].ExportMask -join ',') -eq '3,3,3') 'Minimum zero omits and positive minima resolve only to Both'
|
||||
Assert (@($translated.Controls[7..10]|Where-Object Disposition -ne 'Omitted').Count -eq 0) 'Not Configured, unchanged, inapplicable and unselected optional rows remain omitted'
|
||||
$fixture.includeOptional=$true
|
||||
Assert ((ConvertTo-WelaGpoPackagePlan $fixture -MinimumMode PromoteToBoth).Controls[10].ExportMask -eq 3) 'Selected optional positive mask becomes exact'
|
||||
$fixture.policies[0].requiredMask=0
|
||||
$zero=ConvertTo-WelaGpoPackagePlan $fixture -MinimumMode PromoteToBoth
|
||||
Assert ($zero.Controls[0].Disposition -eq 'Blocked' -and $zero.Controls[0].Reason -match 'value 4') 'No Auditing versus unchanged CSV conflict is explicit and blocks export'
|
||||
$zeroPath=FreshPath;Reject {Export-WelaGpoPackage $zero $zeroPath} 'blocked';Assert (-not(Test-Path $zeroPath)) 'Blocked plans create no output'
|
||||
$fixture.policies=@($fixture.policies[7]);$empty=ConvertTo-WelaGpoPackagePlan $fixture
|
||||
Assert ($empty.Blockers -match 'No applicable') 'An omission-only profile cannot silently export precedence alone'
|
||||
$plain=Get-WelaGpoComponentContent $plan
|
||||
$csv=@($plain['audit.csv'].Text|ConvertFrom-Csv)
|
||||
Assert ($csv.Count -eq @($plan.Controls|Where-Object Disposition -eq 'Exported').Count) 'CSV contains exactly the selected rows'
|
||||
Assert (@($csv|Where-Object {$_.'Policy Target' -cne 'System' -or $_.'Machine Name' -ne '' -or $_.'Exclusion Setting' -ne '' -or $_.'Setting Value' -notin @('1','2','3')}).Count -eq 0) 'CSV has no per-user/exclusion/audit-option or zero rows'
|
||||
Assert ($csv[0].PSObject.Properties.Name.Count -eq 7 -and $plain['audit.csv'].Text -notmatch '(?<!\r)\n') 'Documented CSV header has seven columns and CRLF endings'
|
||||
$rpc=@($csv|Where-Object Subcategory -eq 'RPC Events')
|
||||
$token=@($plan.Controls|Where-Object Name -eq 'Token Right Adjusted Events')
|
||||
Assert ($rpc.Count -eq 1 -and $rpc[0].'Subcategory GUID' -eq '{0CCE922E-69AE-11D9-BED3-505054503030}' -and $token[0].Guid -eq '0CCE924A-69AE-11D9-BED3-505054503030' -and $token[0].Disposition -eq 'Omitted') 'RPC exports independently while unchanged token auditing retains its canonical review identity'
|
||||
$regLines=@($plain['GptTmpl.inf'].Text -split "`r`n"|Where-Object {$_ -like 'MACHINE*'})
|
||||
Assert ($regLines.Count -eq 1 -and $regLines[0] -ceq 'MACHINE\System\CurrentControlSet\Control\Lsa\SCENoApplyLegacyAuditPolicy=4,1') 'Template contains only the typed precedence value'
|
||||
$infBytes=Get-WelaGpoContentBytes $plain['GptTmpl.inf'];$csvBytes=Get-WelaGpoContentBytes $plain['audit.csv']
|
||||
Assert ($infBytes[0] -eq 255 -and $infBytes[1] -eq 254 -and $csvBytes[0] -eq [byte][char]'M') 'Security template has UTF-16LE BOM; audit CSV is UTF-8 without BOM'
|
||||
$path=FreshPath;$dry=Export-WelaGpoPackage $plan $path -DryRun
|
||||
Assert ($dry.DryRun -and -not(Test-Path $path) -and @(Get-ChildItem $temp -Force).Count -eq 0) 'Dry-run creates neither output nor staging directory'
|
||||
$result=Export-WelaGpoPackage $plan $path
|
||||
Assert ($result.ExitCode -eq 0 -and $result.Action -eq 'Export' -and -not $result.DeploymentVerified -and @(Get-ChildItem $path -Force).Count -eq 5) 'Export publishes exactly the verified component files without deployment claims'
|
||||
$verified=Invoke-WelaGpoPackageCommand -Action Verify -Path $path
|
||||
Assert ($verified.ExitCode -eq 0 -and $verified.Action -eq 'Verify' -and $verified.Plan.Profile -eq $plan.Profile) 'Read-only verification reconstructs source intent'
|
||||
Reject {Export-WelaGpoPackage $plan $path} 'fresh output'
|
||||
$filePath=FreshPath;'keep'|Set-Content $filePath;Reject {Export-WelaGpoPackage $plan $filePath} 'fresh output';Assert ((Get-Content $filePath) -eq 'keep') 'Existing files are not overwritten'
|
||||
Reject {Export-WelaGpoPackage $plan (Join-Path (FreshPath) 'missing-parent')} 'parent directory'
|
||||
foreach($case in @('payload','updated-hash','intent','source-hash','extra-file','extra-manifest','extra-filemetadata','duplicate-file','missing-file','encoding','guide')) {
|
||||
$altered=FreshPath;$null=Copy-Item -LiteralPath $path -Destination $altered -Recurse
|
||||
$manifest=Get-Content (Join-Path $altered 'manifest.json') -Raw|ConvertFrom-Json
|
||||
switch($case) {
|
||||
'payload' {Add-Content (Join-Path $altered 'audit.csv') 'unreviewed'}
|
||||
'updated-hash' {Add-Content (Join-Path $altered 'audit.csv') 'unreviewed';$bytes=[IO.File]::ReadAllBytes((Join-Path $altered 'audit.csv'));$manifest.Files[0].Length=$bytes.Length;$manifest.Files[0].Sha256=Get-WelaGpoBytesHash $bytes}
|
||||
'intent' {$manifest.Plan.Controls[0].ExportMask=2}
|
||||
'source-hash' {$manifest.Plan.ProfileSchemaSha256='0'*64}
|
||||
'extra-file' {'unreviewed'|Set-Content (Join-Path $altered 'Backup.xml')}
|
||||
'extra-manifest' {$manifest|Add-Member NoteProperty ImportableGpoBackup $true}
|
||||
'extra-filemetadata' {$manifest.Files[0]|Add-Member NoteProperty Executable $true}
|
||||
'duplicate-file' {$manifest.Files[1]=$manifest.Files[0]}
|
||||
'missing-file' {Remove-Item (Join-Path $altered 'audit.csv')}
|
||||
'encoding' {$manifest.Files[0].Encoding='utf-16'}
|
||||
'guide' {Add-Content (Join-Path $altered 'deployment.md') 'unreviewed deployment'}
|
||||
}
|
||||
SaveManifest $altered $manifest
|
||||
Reject {Test-WelaGpoPackage $altered} 'differs|match|exactly|Unexpected|duplicate'
|
||||
}
|
||||
$link=FreshPath
|
||||
$null=New-Item -ItemType SymbolicLink -Path $link -Target $path
|
||||
Reject {Test-WelaGpoPackage $link} 'Reparse'
|
||||
Reject {Export-WelaGpoPackage $plan (Join-Path $link 'child')} 'Reparse'
|
||||
$memberLink=FreshPath;$null=Copy-Item $path $memberLink -Recurse
|
||||
Remove-Item (Join-Path $memberLink 'audit.csv');$null=New-Item -ItemType SymbolicLink -Path (Join-Path $memberLink 'audit.csv') -Target (Join-Path $path 'audit.csv')
|
||||
Reject {Test-WelaGpoPackage $memberLink} 'reparse'
|
||||
# A raced destination must survive; the staged payload must never merge into it.
|
||||
$racedPath=FreshPath;$originalVerifier=${function:Test-WelaGpoPackage}
|
||||
& {
|
||||
function Test-WelaGpoPackage {param($Path) if($Path -like '*stage*' -and -not(Test-Path $racedPath)){'concurrent owner'|Set-Content $racedPath};& $originalVerifier $Path}
|
||||
Reject {Export-WelaGpoPackage $plan $racedPath} 'did not complete'
|
||||
}
|
||||
Assert ((Get-Content $racedPath) -eq 'concurrent owner') 'Atomic publication preserves a concurrent destination'
|
||||
& {
|
||||
$script:hashReads=0
|
||||
function Get-FileHash {param($LiteralPath,$Algorithm,$ErrorAction) $script:hashReads++;if($script:hashReads -ge 2){[pscustomobject]@{Hash=('0'*64)}}else{Microsoft.PowerShell.Utility\Get-FileHash -LiteralPath $LiteralPath -Algorithm $Algorithm}}
|
||||
Reject {Get-WelaGpoPackagePlan -Profile wela-2.2.0 -Role Client -Build 26100} 'changed during planning'
|
||||
}
|
||||
Reject {Invoke-WelaGpoPackageCommand} 'explicit'
|
||||
Reject {Invoke-WelaGpoPackageCommand -Action Verify -Path $path -Profile wela-2.2.0} 'overrides'
|
||||
Reject {Invoke-WelaGpoPackageCommand -Action Verify -Path $path -DryRun} 'DryRun'
|
||||
Reject {Invoke-WelaGpoPackageCommand -Profile wela-2.2.0 -Role Client -Build 26100 -Path $path} 'only with Export'
|
||||
# The actual CLI's early guard runs before an unrelated configuration profile dispatch.
|
||||
$errors=$null;$ast=[Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'),[ref]$null,[ref]$errors)
|
||||
Assert ($errors.Count -eq 0) 'Public CLI parses'
|
||||
$nodes=@($ast.EndBlock.Statements|Where-Object {$_ -is [Management.Automation.Language.IfStatementAst] -and ($_.Extent.Text -match 'GPO package options require' -or $_.Extent.Text -match 'Invoke-WelaProfileCommand -Command')})
|
||||
Assert ($nodes.Count -eq 2) 'Both dedicated guard and existing profile dispatcher remain present'
|
||||
$dispatch=[scriptblock]::Create('param($Cmd,$Profile,$GpoAction,$GpoProfile,$GpoOutputPath,$GpoMinimumMode)'+[Environment]::NewLine+(($nodes|ForEach-Object {$_.Extent.Text})-join [Environment]::NewLine))
|
||||
function Invoke-WelaProfileCommand {throw 'UNSAFE unrelated dispatcher'}
|
||||
foreach($option in @('GpoAction','GpoProfile','GpoOutputPath','GpoMinimumMode')) {$arguments=@{Cmd='configure';Profile='wela'};$arguments[$option]='value';Reject {& $dispatch @arguments} 'GPO package options require'}
|
||||
Write-Host "PASS: $script:checks GPO component assertions. No native policy or domain changes."
|
||||
} finally {Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Windows-only read-only native observations. The output directory is an owned test artifact.
|
||||
param([Parameter(Mandatory)][string]$OutputPath,[string]$VerifyOtherPath)
|
||||
$ErrorActionPreference='Stop'
|
||||
if ($env:OS -ne 'Windows_NT') {throw 'This smoke test requires Windows.'}
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
|
||||
. (Join-Path $repo 'scripts/Configuration.ps1')
|
||||
. (Join-Path $repo 'scripts/GpoAuditPackages.ps1')
|
||||
function New-GPO {throw 'Domain creation is forbidden in this read-only test.'}
|
||||
function Import-GPO {throw 'Domain import is forbidden in this read-only test.'}
|
||||
function Set-WelaEffectiveAuditPolicy {throw 'Native audit mutation is forbidden in this read-only test.'}
|
||||
function Set-ItemProperty {throw 'Registry mutation is forbidden in this read-only test.'}
|
||||
$script:checks=0
|
||||
function Assert($Value,[string]$Message){if(-not $Value){throw "FAIL: $Message"};$script:checks++}
|
||||
function PolicyFingerprint($State) {(@($State.Keys|Sort-Object|ForEach-Object {$_+'='+$State[$_]})-join ';')}
|
||||
$before=Get-WelaEffectiveAuditPolicy
|
||||
$precedenceBefore=Get-WelaRegistryState -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name SCENoApplyLegacyAuditPolicy
|
||||
Assert ($before.Count -eq 59) 'Native API reads the actual 59 subcategory masks'
|
||||
try {
|
||||
# Explicit target is a package input, regardless of the Server runner's actual role/build.
|
||||
$plan=Get-WelaGpoPackagePlan -Profile wela-2.2.0 -Role Client -Build 26100
|
||||
$export=Export-WelaGpoPackage -Plan $plan -Path $OutputPath
|
||||
Assert ($export.ExitCode -eq 0 -and $export.Plan.Role -eq 'Client' -and -not $export.DeploymentVerified) 'Package target remains declared without host/application claims'
|
||||
$null=Invoke-WelaNative -FilePath (Join-Path $env:SystemRoot 'System32/secedit.exe') -Arguments @('/validate',(Join-Path $OutputPath 'GptTmpl.inf'))
|
||||
Assert $true 'Native secedit validates security-template syntax only'
|
||||
$verified=Test-WelaGpoPackage -Path $OutputPath
|
||||
Assert ($verified.ExitCode -eq 0) 'Template validation leaves the generated package intact'
|
||||
if ($VerifyOtherPath) {
|
||||
$other=Test-WelaGpoPackage -Path $VerifyOtherPath
|
||||
Assert ($other.ExitCode -eq 0) 'Package created by the other PowerShell edition verifies against this generator'
|
||||
foreach ($name in @('audit.csv','GptTmpl.inf','review.md','deployment.md')) {
|
||||
$left=Get-FileHash -LiteralPath (Join-Path $OutputPath $name) -Algorithm SHA256
|
||||
$right=Get-FileHash -LiteralPath (Join-Path $VerifyOtherPath $name) -Algorithm SHA256
|
||||
Assert ($left.Hash -eq $right.Hash) "Both editions generate identical $name bytes"
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$after=Get-WelaEffectiveAuditPolicy
|
||||
$precedenceAfter=Get-WelaRegistryState -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name SCENoApplyLegacyAuditPolicy
|
||||
Assert ((PolicyFingerprint $before) -ceq (PolicyFingerprint $after)) 'All effective native audit masks remain unchanged'
|
||||
Assert ((ConvertTo-Json $precedenceBefore -Compress) -ceq (ConvertTo-Json $precedenceAfter -Compress)) 'Precedence registry presence/type/value remain unchanged'
|
||||
}
|
||||
Write-Host "PASS: $script:checks read-only GPO component checks. No local/domain policy application; retain package for cross-edition verification."
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
**改善:**
|
||||
|
||||
- 共有する詳細監査プロファイルと優先設定のセキュリティテンプレートについて、オフラインで計画・出力・検証する`gpo-package`を追加しました。省略項目を保持し、成功または失敗だけの最低要件を両方へ拡張する場合は明示指定を求め、未検証のゼロ値による配備は拒否します。出典・申告対象・全項目レビュー・検証済みハッシュを含む配備用ファイルであり、GPOバックアップではありません。正規のGPMC/LGPO準備と未リンクGPO作成のレビュー手順を文書化しました。ドメイン配備と実イベントの検証は別途ラボで必要となり、Sigma検知範囲には加算しません。 (#415) (@Shirofune-Security)
|
||||
- 検証対象のWindows 11クライアント向けに、共通の標準監査プロファイルからオフラインで出力する`intune-export`を追加しました。Microsoft DDFに基づく59件の明示的な対応表、整数型のOMA-URI CSV/Graphデータ、監査サブカテゴリの優先設定、出典と省略理由の一覧を保存します。最小監査マスクは既定で拒否し、`PromoteToBoth`の明示指定時だけ成功・失敗の両方へ拡張します。新規ローカル出力には検証済みハッシュを付け、アップロード・割り当て・ポリシー削除・Windows設定変更は行いません。Intune配備・競合・復旧・イベントの確認は別途必要です。 (#414) (@Shirofune-Security)
|
||||
- `-ProfileFile`で管理者のJSON詳細監査プロファイルを一覧・計画・監査・設定に使用できるようにしました。標準GUID、役割、設定モード、出典ハッシュを厳密に検証し、組み込みプロファイルを保持します。厳密なJSON字句検証で重複キー検出の回避を防ぎ、レポートは新規ファイルに限ることで別名リンク経由でも入力と既存の証拠を保持します。共通の優先設定、復旧記録、変更直前のファイル確認と最終検証を使用し、イベント生成やSigma検知可能性は別途検証とします。 (#416) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
**Improvements:**
|
||||
|
||||
- Added offline `gpo-package` plan, export and verification for shared advanced audit profiles and the precedence security template. Packages preserve omissions, require explicit expansion of one-sided minimum masks, reject unvalidated zero-mask deployment, and include source/target context, full reviews and verified file hashes. These are deployment components, not GPO backups; genuine GPMC/LGPO preparation and reviewed create-unlinked procedures are documented. Native domain application and event evidence remain separate lab acceptance with no Sigma credit. (#415) (@Shirofune-Security)
|
||||
- Added offline `intune-export` for shared native audit profiles on reviewed Windows 11 client targets, with 59 explicit Microsoft DDF mappings, typed OMA-URI CSV/Graph artifacts, the audit precedence prerequisite and complete source/omission manifests. Static minimum masks are rejected unless explicitly expanded with `PromoteToBoth`; fresh local bundles include verified fingerprints and never upload, assign, delete policies or change Windows. Intune deployment, conflicts, recovery and event evidence remain separate validation. (#414) (@Shirofune-Security)
|
||||
- Added `-ProfileFile` for strictly validated custom advanced audit profiles in listing, planning, auditing and configuration. Canonical GUIDs, roles, modes and source hashes remain explicit; built-in profiles are preserved. Strict JSON tokens prevent duplicate-key bypasses, and new report files preserve inputs and prior evidence even through file aliases. Shared precedence, recovery journals, pre-write file checks and final verification protect configuration, without claiming event or Sigma readiness. (#416) (@Shirofune-Security)
|
||||
|
||||
|
||||
Reference in new issue
Block a user