diff --git a/.github/workflows/audit-integrity.yml b/.github/workflows/audit-integrity.yml new file mode 100644 index 00000000..b392615c --- /dev/null +++ b/.github/workflows/audit-integrity.yml @@ -0,0 +1,36 @@ +name: Audit integrity regressions +on: + push: + branches: ['**'] + paths: + - 'WELA.ps1' + - 'scripts/Configuration.ps1' + - 'scripts/AuditIntegrity*' + - 'config/audit_integrity_profiles.json' + - 'tests/AuditIntegrity*' + - '.github/workflows/audit-integrity.yml' + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + audit-integrity: + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Mocked privilege and safety regressions in Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditIntegrity.Tests.ps1 + - name: Native read-only LSA and security-policy observations in Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditIntegrity.Windows.Tests.ps1 + - name: Mocked privilege and safety regressions in PowerShell 7 + shell: pwsh + run: ./tests/AuditIntegrity.Tests.ps1 + - name: Native read-only LSA and security-policy observations in PowerShell 7 + shell: pwsh + run: ./tests/AuditIntegrity.Windows.Tests.ps1 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index e836a384..71e269ef 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,7 @@ **改善:** +- ローカル監査権限と`CrashOnAuditFail`を監査・計画・出典別に設定する任意実行の`audit-integrity`を追加しました。実際のクライアント・メンバーサーバー・DCを区別し、Microsoft SCTで省略された設定は保持します。対象SIDの一覧、権限削除の明示指定、権限単位のLSA更新、完全な復旧記録と変更直前・変更後の検証により、無関係な権限を保持します。復旧状態では変更せず、ネイティブCIは読み取りだけを行います。トークン・GPO・サービス・イベントの検証は別途ラボで必要となり、Sigma検知範囲には加算しません。 (#412) (@Shirofune-Security) - 読み取り専用の`retention-health`を追加し、送信元/収集サーバーの標準ログバッファ、確認した先頭レコードの経過日数、申告されたアーカイブ方針、件数を制限したローカルEVTX/ACL一覧、各言語のWEF・時刻情報とログ消失・消去・満杯の兆候をJSONと単独で表示できるHTMLに分けて記録します。保持されたイベントの時刻に基づく件数率とUTF-8 XMLバイト数の試算には上限・前提を明示し、容量・完全な保持・実効読み取り権限・時刻同期・転送成功の証明とは扱いません。複数ホストでのロールオーバー・復旧・到着検証は別途必要です。 (#410) (@Shirofune-Security) - 読み取り専用の`control-applicability`を追加し、旧CISのApplication Guard監査要件を保持しつつ、Windows 11 24H2以降では削除済み・対象外と表示します。`default-evidence`で正確なビルド・パッチ・ドメイン参加・役割を含む現状を記録し、出典とレビュー情報が一致する参照環境と比較できます。旧ベースラインの既定値は履歴情報として保持し、未検証の既定値はUnknownと表示します。合成テストと読み取り専用CIからクリーンインストールやイベント生成の証明は行いません。 (#409) (@Shirofune-Security) - Windows標準のDNS Client/Server、CAPI2、WinRM、RDP Clientについて、明示的に選択するprovider-packsの監査・計画とチャネル設定を追加しました。固定した完全なルール定義と実際のプロバイダー・チャネル・イベントスキーマを確認し、DNSのチャネル名不一致や不明な前提条件を保持します。復旧記録付きの変更では大きいバッファ・保持方式・ACLを維持し、Analyticalと従来のDNSログは手動確認のみとします。イベント生成・バックエンド検証や検知範囲の向上は未確認です。 (#411) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c46bc66..bf2534cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Improvements:** +- Added opt-in `audit-integrity` audit, plan and source-profile configuration for local audit privileges and `CrashOnAuditFail`, with separate actual client/member/DC scope and preserved Microsoft SCT omissions. Exact affected SIDs, explicit privilege-removal consent, per-right LSA updates, complete recovery journals and fresh/readback checks preserve unrelated privileges. Recovery states are blocked; native CI reads policy only, while token, GPO, service and event validation remains a lab requirement without Sigma credit. (#412) (@Shirofune-Security) - Added read-only `retention-health` source/collector JSON and self-contained HTML reports separating native buffers, observed record-boundary ages, declared archive policy, bounded local EVTX/ACL inventory, localized WEF/time evidence and loss/clear/full indicators. Retained-event timestamp rates and UTF-8 XML-byte scenarios expose caps and assumptions; none establish archive capacity, complete retention, effective reader access, synchronized time or successful forwarding. Multi-host rollover/recovery/arrival validation remains pending. (#410) (@Shirofune-Security) - Added read-only `control-applicability` for the historical CIS Application Guard audit requirement, reporting removal on Windows 11 24H2+ without remediation. Added exact build/patch/join/role native snapshots and provenance-bound reference comparison through `default-evidence`. Legacy baseline default strings are retained as historical hints while public defaults remain Unknown without reviewed scenario evidence. Synthetic fixtures and native read-only CI do not claim clean-install or event-generation proof. (#409) (@Shirofune-Security) - Added explicit native DNS Client/Server, CAPI2, WinRM and RDP Client provider-pack inventory and selective channel configuration. Pinned full rule definitions and live provider/channel/event schemas retain DNS channel mismatches and unknown prerequisites; journaled opt-in changes preserve larger buffers, retention and ACLs. Analytical/classic DNS remain manual-only, and no event/backend readiness uplift is claimed. (#411) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index 31da5002..41837522 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -51,6 +51,9 @@ [ValidateRange(1,2147483647)][int]$LdapSearchTimeMs, [ValidateRange(1,2147483647)][int]$LdapExpensiveThreshold, [ValidateRange(1,2147483647)][int]$LdapInefficientThreshold, + [ValidateSet('Audit','Plan','Configure')][string]$IntegrityAction = 'Audit', + [string]$IntegrityProfile, + [switch]$AllowPrivilegeRemoval, [ValidateSet('Capture','Compare')][string]$DefaultEvidenceAction = 'Capture', [string]$DefaultEvidencePath, [ValidateSet('List','Audit','Plan','Configure')][string]$ProviderAction = 'List', @@ -73,6 +76,7 @@ $EidMappingPath = Join-Path $ScriptRoot "config/eid_subcategory_mapping.csv" $AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt" $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" . (Join-Path $ScriptRoot "scripts/Configuration.ps1") +. (Join-Path $ScriptRoot "scripts/AuditIntegrity.ps1") . (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1") . (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1") . (Join-Path $ScriptRoot "scripts/LdapDiagnostics.ps1") @@ -1752,6 +1756,9 @@ function Get-WelaUserProfiles { $usage = @" Usage: + ./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 ./WELA.ps1 ldap-diagnostics -LdapAction Audit ./WELA.ps1 ldap-diagnostics -LdapAction Plan -LdapMode Diagnostic -LdapSearchTimeMs 100 ./WELA.ps1 ldap-diagnostics -LdapAction Configure -LdapMode Diagnostic -LdapSearchTimeMs 100 -DryRun @@ -1818,6 +1825,10 @@ Write-Host "" Write-Host "WELA v$WELAVersion - $WELAReleaseName" Write-Host "" +if ($Cmd -ne 'audit-integrity' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('IntegrityAction','IntegrityProfile','AllowPrivilegeRemoval') }).Count) { + throw 'Integrity options require the dedicated audit-integrity command. No command was run.' +} + if ($Cmd -ne 'default-evidence' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('DefaultEvidenceAction','DefaultEvidencePath') }).Count) { throw 'Default evidence options require default-evidence. No command was run.' } @@ -1867,7 +1878,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-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 '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 @@ -1876,7 +1887,7 @@ if ($DryRun -and -not ($Cmd -eq 'audit-notifications' -and $NotificationAction - -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, 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. 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.' @@ -1906,6 +1917,14 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi } switch ($Cmd.ToLower()) { + 'audit-integrity' { + if ($Help) { Write-Host 'Usage: ./WELA.ps1 audit-integrity [-IntegrityAction Audit|Plan|Configure] [-IntegrityProfile source-id] [-AllowPrivilegeRemoval] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath report.json]. Audit is read-only; Plan/Configure require an exact source profile. See docs/audit-integrity.md.'; return } + if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath) { throw 'audit-integrity observes the actual local Windows host; use -IntegrityProfile and -ResultsPath, without Security profiles, role/build overrides or HTML.' } + if ($IntegrityAction -eq 'Configure' -and -not (TestAdministrator)) { throw 'Audit-integrity Configure requires Administrator privileges.' } + $report=Invoke-WelaIntegrityCommand -Action $IntegrityAction -Profile $IntegrityProfile -AllowPrivilegeRemoval:$AllowPrivilegeRemoval -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $report + if ($report.ExitCode) { exit $report.ExitCode } + } 'retention-health' { if ($Help) { Write-Host 'Usage: ./WELA.ps1 retention-health [-RetentionConfigPath operator.json] [-RetentionPreviousPath prior-local-report.json] [-ResultsPath report.json] [-HtmlPath report.html]' diff --git a/config/audit_integrity_profiles.json b/config/audit_integrity_profiles.json new file mode 100644 index 00000000..a6ed3af4 --- /dev/null +++ b/config/audit_integrity_profiles.json @@ -0,0 +1,172 @@ +{ + "SchemaVersion": 1, + "Scope": "Two direct LSA rights assignments and CrashOnAuditFail only; omitted values preserve observed state.", + "Profiles": [ + { + "Id": "cis-win11-v4-l1", + "Role": "Client", + "MinBuild": 22000, + "MaxBuild": 26100, + "Source": "CIS v4.0.0 (reviewed historical edition)", + "Url": "https://rayasec.com/wp-content/uploads/CIS-Benchmark/Microsoft-Windows-Desktop/CIS_Microsoft_Windows_11_Enterprise_Benchmark_v4.0.0.pdf", + "SourceSha256": "362fe88c80307a809e6cf42556b029839d1c32440048f13fff3e666653bb32ce", + "References": [ + "2.2.23", + "2.2.30", + "2.3.2.2" + ], + "SeAuditPrivilege": [ + "S-1-5-19", + "S-1-5-20" + ], + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": 0 + }, + { + "Id": "cis-server2022-v4-member", + "Role": "MemberServer", + "MinBuild": 20348, + "MaxBuild": 20348, + "Source": "CIS v4.0.0 (reviewed historical edition)", + "Url": "https://rayasec.com/wp-content/uploads/CIS-Benchmark/Microsoft-Windows-Server/CIS_Microsoft_Windows_Server_2022_Benchmark_v4.0.0.pdf", + "SourceSha256": "9be0532e08a9b2919e2591a6eb4499f1edd11bc1676876959513f2f4c566bec2", + "References": [ + "2.2.31", + "2.2.39", + "2.3.2.2" + ], + "SeAuditPrivilege": [ + "S-1-5-19", + "S-1-5-20" + ], + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": 0 + }, + { + "Id": "cis-server2022-v4-dc", + "Role": "DomainController", + "MinBuild": 20348, + "MaxBuild": 20348, + "Source": "CIS v4.0.0 (reviewed historical edition)", + "Url": "https://rayasec.com/wp-content/uploads/CIS-Benchmark/Microsoft-Windows-Server/CIS_Microsoft_Windows_Server_2022_Benchmark_v4.0.0.pdf", + "SourceSha256": "9be0532e08a9b2919e2591a6eb4499f1edd11bc1676876959513f2f4c566bec2", + "References": [ + "2.2.31", + "2.2.38", + "2.3.2.2" + ], + "SeAuditPrivilege": [ + "S-1-5-19", + "S-1-5-20" + ], + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": 0 + }, + { + "Id": "microsoft-sct-win11-24h2", + "Role": "Client", + "MinBuild": 26100, + "MaxBuild": 26100, + "Source": "MSFT Windows 11 24H2 - Computer", + "Url": "https://www.microsoft.com/en-us/download/details.aspx?id=55319", + "Package": "Windows 11 v24H2 Security Baseline.zip", + "PackageSha256": "b75439a231c64edaccaad16a16268d199f56ce78273104e117d893f82cf174a5", + "Template": "Windows 11 v24H2 Security Baseline/GPOs/{DAD42DA1-8499-42FE-A1CD-9E39196D8B98}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf", + "TemplateSha256": "2da1ec458c2daf9fcf9bd6a56c485a975261e7c04fd59f321000bcae532c75a1", + "SeAuditPrivilege": null, + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": null + }, + { + "Id": "microsoft-sct-win11-25h2", + "Role": "Client", + "MinBuild": 26200, + "MaxBuild": 26200, + "Source": "MSFT Windows 11 25H2 - Computer", + "Url": "https://www.microsoft.com/en-us/download/details.aspx?id=55319", + "Package": "Windows 11 v25H2 Security Baseline.zip", + "PackageSha256": "3517a53030a3e437c9fe00c04274d80965d3527a8eb0514520cba75023c376f7", + "Template": "Windows 11 v25H2 Security Baseline/GPOs/{02DB0E53-0925-4E5A-B775-E7A1A9370AB8}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf", + "TemplateSha256": "8c5509e4876e51c448e88add14088a6d7a96df9e1ba4786e6a90749f30ba8b98", + "SeAuditPrivilege": null, + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": null + }, + { + "Id": "microsoft-sct-server2022-member", + "Role": "MemberServer", + "MinBuild": 20348, + "MaxBuild": 20348, + "Source": "MSFT Windows Server 2022 - Member Server", + "Url": "https://www.microsoft.com/en-us/download/details.aspx?id=55319", + "Package": "Windows Server 2022 Security Baseline.zip", + "PackageSha256": "49590cc694626d171fc934fafea6494f13ecd3843086704b7a5b98355909b8e0", + "Template": "Windows Server-2022-Security-Baseline-FINAL/GPOs/{20FAD6FB-7C6D-496E-801C-0434769847FF}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf", + "TemplateSha256": "e9fa8208aec6b2fe1f048c61c2b6667cc2b01fbc29f48ed84e0d341e8d0a9c08", + "SeAuditPrivilege": null, + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": null + }, + { + "Id": "microsoft-sct-server2022-dc", + "Role": "DomainController", + "MinBuild": 20348, + "MaxBuild": 20348, + "Source": "MSFT Windows Server 2022 - Domain Controller", + "Url": "https://www.microsoft.com/en-us/download/details.aspx?id=55319", + "Package": "Windows Server 2022 Security Baseline.zip", + "PackageSha256": "49590cc694626d171fc934fafea6494f13ecd3843086704b7a5b98355909b8e0", + "Template": "Windows Server-2022-Security-Baseline-FINAL/GPOs/{E2B8214C-729F-4324-A876-F067E58B740B}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf", + "TemplateSha256": "e4a3f7a28f42e17069a2672ca0ae30fba2f54160b05fa847ca85164d028d7a05", + "SeAuditPrivilege": null, + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": null + }, + { + "Id": "microsoft-sct-server2025-v2602-member", + "Role": "MemberServer", + "MinBuild": 26100, + "MaxBuild": 26100, + "Source": "MSFT Windows Server 2025 v2602 - Member Server", + "Url": "https://www.microsoft.com/en-us/download/details.aspx?id=55319", + "Package": "Windows Server 2025 Security Baseline - 2602.zip", + "PackageSha256": "a66dffbe2622c3c4dd70e44a7f2080f362fb2d6f9208c50678671cbb046a286f", + "Template": "Windows Server 2025 Security Baseline - 2602/GPOs/{066B7FF5-BF2B-4B1B-8A92-2A83B8619444}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf", + "TemplateSha256": "4403795c0f1eb374b285592a0589a5addfa6af3c5b33490c35595037513ba8e8", + "SeAuditPrivilege": null, + "SeSecurityPrivilege": [ + "S-1-5-32-544" + ], + "CrashOnAuditFail": null + }, + { + "Id": "microsoft-sct-server2025-v2602-dc", + "Role": "DomainController", + "MinBuild": 26100, + "MaxBuild": 26100, + "Source": "MSFT Windows Server 2025 v2602 - Domain Controller", + "Url": "https://www.microsoft.com/en-us/download/details.aspx?id=55319", + "Package": "Windows Server 2025 Security Baseline - 2602.zip", + "PackageSha256": "a66dffbe2622c3c4dd70e44a7f2080f362fb2d6f9208c50678671cbb046a286f", + "Template": "Windows Server 2025 Security Baseline - 2602/GPOs/{88603F56-DC8F-4132-8A8C-8FE5EB0B4B1A}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf", + "TemplateSha256": "a0702b0010e54fd16afdad758fee35157c28f82cbbe28e66f69a3a8ea6034ef7", + "SeAuditPrivilege": null, + "SeSecurityPrivilege": null, + "CrashOnAuditFail": null + } + ] +} diff --git a/docs/audit-integrity.md b/docs/audit-integrity.md new file mode 100644 index 00000000..09544e45 --- /dev/null +++ b/docs/audit-integrity.md @@ -0,0 +1,70 @@ +# Audit integrity: local rights and audit failure policy + +`audit-integrity` reads the direct local LSA assignments for **Generate security audits** (`SeAuditPrivilege`) and **Manage auditing and security log** (`SeSecurityPrivilege`), together with the typed `CrashOnAuditFail` value. The default action is read-only. Configuration is a separate opt-in action with an explicit reviewed source profile; normal `configure` and Security audit-policy profiles do not invoke it. + +```powershell +./WELA.ps1 audit-integrity -ResultsPath integrity-audit.json +./WELA.ps1 audit-integrity -IntegrityAction Plan ` + -IntegrityProfile cis-server2022-v4-dc -ResultsPath integrity-plan.json +./WELA.ps1 audit-integrity -IntegrityAction Configure ` + -IntegrityProfile cis-win11-v4-l1 -DryRun -ResultsPath integrity-preview.json + +# After reviewing the actual host, affected SIDs and application dependencies: +./WELA.ps1 audit-integrity -IntegrityAction Configure ` + -IntegrityProfile cis-server2022-v4-member ` + -Auto -BackupPath .\new-integrity-backup -ResultsPath integrity-result.json +``` + +Use a 64-bit Windows PowerShell 5.1 or PowerShell 7 process. Reads need sufficient LSA/registry access; access denial is `Unknown`, never an empty compliant assignment. Configure requires elevation. CIM observations determine the actual client, member-server, standalone-server or domain-controller role and build; conflicting or unreadable role evidence blocks configuration. AD CS is a separate observed `CertSvc` role and does not turn a member server into a DC. No source profile is currently mapped to a standalone server. Host `-Role`/`-Build` overrides, `-Profile`, `-Baseline` and HTML are rejected. Integrity-only options are rejected before unrelated command dispatch. Audit and Plan never change Windows policy; Configure `-DryRun` also creates no recovery directory. + +## Reviewed sources and exact requested settings + +The catalog at [`config/audit_integrity_profiles.json`](../config/audit_integrity_profiles.json) pins source/PDF or package/template SHA-256 hashes and the exact SCT GPO template path. These are selected historical sources, not a claim of the latest CIS edition or full baseline compliance. Profile identity is recorded separately from observed host policy. `SourceSetting=OmittedBySource` means preserve the observed value, not clear it, assume a Windows default, or copy a recommendation from another profile. + +| Profile | Actual role/build scope | `SeAuditPrivilege` | `SeSecurityPrivilege` | `CrashOnAuditFail` | +| --- | --- | --- | --- | --- | +| `cis-win11-v4-l1` | Client, 22000–26100 | LOCAL SERVICE + NETWORK SERVICE | Administrators | DWORD 0 | +| `cis-server2022-v4-member` | Joined member server, 20348 | LOCAL SERVICE + NETWORK SERVICE | Administrators | DWORD 0 | +| `cis-server2022-v4-dc` | DC, 20348 | LOCAL SERVICE + NETWORK SERVICE | Administrators | DWORD 0 | +| `microsoft-sct-win11-24h2` | Client, 26100 | Omitted; preserve | Administrators | Omitted; preserve | +| `microsoft-sct-win11-25h2` | Client, 26200 | Omitted; preserve | Administrators | Omitted; preserve | +| `microsoft-sct-server2022-member` | Joined member server, 20348 | Omitted; preserve | Administrators | Omitted; preserve | +| `microsoft-sct-server2022-dc` | DC, 20348 | Omitted; preserve | Administrators | Omitted; preserve | +| `microsoft-sct-server2025-v2602-member` | Joined member server, 26100 | Omitted; preserve | Administrators | Omitted; preserve | +| `microsoft-sct-server2025-v2602-dc` | DC, 26100 | Omitted; preserve | **Omitted; preserve** | Omitted; preserve | + +Principal identity uses SIDs rather than localized account names: LOCAL SERVICE `S-1-5-19`, NETWORK SERVICE `S-1-5-20`, BUILTIN\Administrators `S-1-5-32-544`. The CIS entries are exact sets for the selected two rights; every missing and extra SID is listed in the plan, with resolved names where available. Unresolved names do not discard a SID. + +The reviewed CIS Windows 11 Enterprise v4.0.0 sections are **2.2.23**, **2.2.30**, **2.3.2.2**. Server 2022 v4.0.0 uses **2.2.31**, **2.2.38 (DC)** / **2.2.39 (member)**, and **2.3.2.2**. [Reviewed Windows 11 PDF](https://rayasec.com/wp-content/uploads/CIS-Benchmark/Microsoft-Windows-Desktop/CIS_Microsoft_Windows_11_Enterprise_Benchmark_v4.0.0.pdf), [reviewed Server 2022 PDF](https://rayasec.com/wp-content/uploads/CIS-Benchmark/Microsoft-Windows-Server/CIS_Microsoft_Windows_Server_2022_Benchmark_v4.0.0.pdf). + +The Microsoft entries come from the respective computer/member/DC `GptTmpl.inf` in the [Security Compliance Toolkit](https://www.microsoft.com/en-us/download/details.aspx?id=55319) packages listed in the catalog. In particular, the **Server 2025 v2602 DC template omits all three settings**; its member template's Administrators assignment is not imported into the DC profile. Omission is a statement about those selected templates, not a statement that Microsoft recommends removing a privilege or that a deployment's other GPOs omit it. WELA applies only these selected local settings; it does not import SCT GPOs, run the SCT installation scripts or change domain policy. + +Microsoft's general policy documentation is separate from SCT template contents. Its [Generate security audits guidance](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn221956(v=ws.11)) identifies LOCAL SERVICE and NETWORK SERVICE; [Manage auditing and security log guidance](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn221953(v=ws.11)) discusses Administrators and dependency review. General [audit failure guidance](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/audit-shut-down-system-immediately-if-unable-to-log-security-audits) discusses availability/completeness tradeoffs. WELA does not turn this general guidance into an omitted SCT value. + +## Privilege removal and service dependencies + +Plans always show proposed additions and removals. If either exact right has extra assigned principals, the entire Configure action is blocked unless the operator explicitly supplies `-AllowPrivilegeRemoval`. `-Auto` alone cannot authorize removal. The flag can also be supplied to Plan to preview an executable removal plan; it does not discover or certify application dependencies. + +Review each affected SID before using that option. IIS application pools can require `SeAuditPrivilege`. AD FS can require its service identity and `NT SERVICE\ADFSSrv` / `NT SERVICE\DRS`. Exchange deployments can require the Exchange Servers group to retain `SeSecurityPrivilege` on DCs. Custom applications may have additional requirements. The command reports these exceptions but does not invent service-specific principal sets, automatically classify an extra principal as unnecessary, or grant substitute rights. Keep required exceptions and record a source deviation instead of using removal consent to break a service. + +The native adapter uses local [`LsaEnumerateAccountsWithUserRight`](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaenumerateaccountswithuserright) and [`LsaEnumerateAccountRights`](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaenumerateaccountrights). These are direct assignments, not an expansion of nested groups or a test of every user's effective token. Updates use [`LsaAddAccountRights`](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaaddaccountrights) / [`LsaRemoveAccountRights`](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaremoveaccountrights) for **one named right and SID per operation**. Removal always uses `AllRights=false`. Other privileges, logon rights, account objects and local/domain group membership are not replaced or deleted. + +## CrashOnAuditFail and verification + +The exact value is `HKLM\SYSTEM\CurrentControlSet\Control\Lsa\CrashOnAuditFail`. DWORD `0` is reported disabled, DWORD `1` enabled, an absent value as absent (not assumed equivalent to an explicit source value), and DWORD `2` as `RecoveryRequired`. Unknown values, non-DWORD types, read errors and missing LSA keys block configuration. **A value of 2 is never reset by this workflow.** It does not clear the Security log, deliberately exhaust audit capacity, enable crash-on-audit-failure, restart Windows or automate recovery. CIS profiles may explicitly set DWORD 0 after review; SCT omissions preserve the observed state. + +Before any mutation, the shared configuration runner writes `before.jsonl` with the complete observed assignments, every affected account's full rights, the original registry type/value/absence and the selected plan. The native operation compares a fresh snapshot with the plan, rereads before each individual mutation, then verifies the expected result and all preserved account rights. A final read detects later drift. Journal failure prevents mutation; a write, readback or preservation failure stops remaining operations. This is not a transaction: a concurrent writer can act between checks, and an earlier successful operation can remain after a later failure. + +JSON keeps the reviewed plan's **before** observations separate from `Results[].After`. `Applied` / `AlreadyCompliant` establish only the observed local assignment/registry checks for the selected profile, including preservation of source omissions. `Skipped` includes dry-run or declined changes. Exit 0 means no failed/overridden controls; a skipped run is not configuration evidence. Unknown or mismatched source/host state is blocked. An already compliant source profile that omits all three controls requests no changes and does not certify a secure privilege set. + +Existing process tokens are not refreshed. A later logon/service restart can be needed for rights to affect a new token; WELA performs neither. GPO can overwrite local settings later, and these observations do not identify the winning GPO, establish persistence or modify the authoritative source. Verify resultant policy with the policy owner and test a fresh appropriate identity separately. + +## Recovery and remaining lab evidence + +Recovery is manual. Protect the backup directory as administrator policy evidence and retain the result JSON. Compare the journal's `Before`, requested operations, any recorded `After`, **current** LSA assignments and the authoritative policy source. A journal records intent/prior state; it is not proof every listed operation completed. Reverse only confirmed changes: add back a right removed by this run or remove a right added by this run for the same exact SID, leaving all other current rights untouched. Do not import an old whole-account privilege list or a whole `secedit` template over later changes. Restore only `CrashOnAuditFail`'s previous type/value or absence after determining that no newer policy or audit-failure recovery state supersedes it; never blindly overwrite a current value of 2. Re-read policy afterward. No automatic rollback is offered because concurrent administrative changes and recovery state require review. + +The focused suite mocks every mutation and covers exact source sets/omissions, role contradictions, removal consent, typed values, idempotence, journal failure, plan/prewrite races, partial failure, read errors, preservation of unrelated privileges and final drift. Windows CI is read-only on Server 2022/2025 under Windows PowerShell 5.1 and PowerShell 7: it compiles the native adapter, reads real LSA/CIM/registry state, compares assignments to a `secedit /export` in an owned temporary directory, and confirms unchanged observations. It does not grant/revoke any live rights or change the audit failure policy. + +Remaining acceptance evidence requires disposable, snapshotted client/member/DC labs, including member/DC AD CS and relevant IIS/AD FS/Exchange dependencies: review affected principals, apply the chosen source profile, verify new-token behavior, readback after ordinary GPO refresh, benign audit generation and authorized collection, and selective recovery with unrelated rights preserved. Do not create an audit-exhaustion test. DC/member mutation, service-token and event/ingestion evidence is still pending; read-only CI cannot close those requirements. + +This is audit-integrity hardening, not a new event family. Reports set `SigmaEvtxCredit=0`; no rule-eligibility or Sigma coverage increase is inferred. Sysmon is outside this native Windows workflow. diff --git a/scripts/AuditIntegrity.ps1 b/scripts/AuditIntegrity.ps1 new file mode 100644 index 00000000..b3bd58e5 --- /dev/null +++ b/scripts/AuditIntegrity.ps1 @@ -0,0 +1,192 @@ +# Explicit source-profile audit integrity. Direct assignments are not token membership. +function Initialize-WelaIntegrityNative { + if (-not ('Wela.AuditIntegrityNative' -as [type])) { + Add-Type -Path (Join-Path $PSScriptRoot 'AuditIntegrityNative.cs') -ErrorAction Stop + } +} +function Get-WelaIntegrityHolders { + param([ValidateSet('SeAuditPrivilege','SeSecurityPrivilege')][string]$Right) + Initialize-WelaIntegrityNative + return ,@([Wela.AuditIntegrityNative]::Holders($Right)) +} +function Get-WelaIntegrityAccountRights { + param([string]$Sid) + Initialize-WelaIntegrityNative + return ,@([Wela.AuditIntegrityNative]::Rights($Sid)) +} +function Set-WelaIntegrityAccountRight { + param([string]$Sid,[ValidateSet('SeAuditPrivilege','SeSecurityPrivilege')][string]$Right,[bool]$Grant) + Initialize-WelaIntegrityNative + [Wela.AuditIntegrityNative]::Change($Sid,$Right,$Grant) +} +function Get-WelaIntegrityHost { + if ($env:OS -ne 'Windows_NT') { throw 'Audit integrity requires Windows.' } + $os=Get-CimInstance Win32_OperatingSystem -ErrorAction Stop + $computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop + if ($os.ProductType -notin @(1,2,3) -or $computer.DomainRole -notin @(0,1,2,3,4,5) -or $null -eq $computer.PartOfDomain -or [string]$os.BuildNumber -notmatch '^\d+$') { throw 'Unknown Windows role/build.' } + $role=$null + if ($os.ProductType -eq 1 -and $computer.DomainRole -in @(0,1)) { $role='Client' } + elseif ($os.ProductType -eq 2 -and $computer.DomainRole -in @(4,5) -and $computer.PartOfDomain) { $role='DomainController' } + elseif ($os.ProductType -eq 3 -and $computer.DomainRole -eq 3 -and $computer.PartOfDomain) { $role='MemberServer' } + elseif ($os.ProductType -eq 3 -and $computer.DomainRole -eq 2 -and -not $computer.PartOfDomain) { $role='StandaloneServer' } + if (-not $role -or ($computer.DomainRole -eq 1 -and -not $computer.PartOfDomain) -or ($computer.DomainRole -eq 0 -and $computer.PartOfDomain)) { throw 'Conflicting Windows role observations.' } + if (-not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell for this workflow.' } + $ca='Unknown' + try { $ca=if (@(Get-CimInstance Win32_Service -Filter "Name='CertSvc'" -ErrorAction Stop).Count) { 'Installed' } else { 'NotInstalled' } } catch { } + [pscustomobject]@{Status='Known';ComputerName=[string]$computer.Name;Role=$role;Build=[int]$os.BuildNumber;DomainJoined=[bool]$computer.PartOfDomain;DomainRole=[int]$computer.DomainRole;CertificateAuthority=$ca} +} +function Get-WelaIntegritySnapshot { + param([string[]]$ObserveSids=@()) + $errors=@();$hostState=$null;$rights=@();$accounts=@();$crash=$null + try { $hostState=Get-WelaIntegrityHost } catch { $hostState=[pscustomobject]@{Status='Unknown';Diagnostic=$_.Exception.Message};$errors+=$_.Exception.Message } + if ($hostState.Status -eq 'Known') { + foreach ($right in @('SeAuditPrivilege','SeSecurityPrivilege')) { + try { $holders=@(Get-WelaIntegrityHolders $right | ForEach-Object {$_} | Sort-Object -Unique);$rights+=[pscustomobject]@{Name=$right;Holders=$holders;Status='Known'} } + catch { $rights+=[pscustomobject]@{Name=$right;Holders=@();Status='Unknown'};$errors+="$right : $($_.Exception.Message)" } + } + $sids=@(@('S-1-5-19','S-1-5-20','S-1-5-32-544') + @($rights | ForEach-Object {$_.Holders}) + @($ObserveSids) | Sort-Object -Unique) + foreach ($sid in $sids) { + try { $assigned=@(Get-WelaIntegrityAccountRights $sid | ForEach-Object {$_} | Sort-Object -Unique);$accounts+=[pscustomobject]@{Sid=$sid;Rights=$assigned;Status='Known'} } + catch { $accounts+=[pscustomobject]@{Sid=$sid;Rights=@();Status='Unknown'};$errors+="$sid : $($_.Exception.Message)" } + } + if (-not $errors.Count) { + foreach ($right in $rights) { + $fromAccounts=@($accounts | Where-Object { $_.Rights -contains $right.Name } | ForEach-Object {$_.Sid} | Sort-Object -Unique) + if (($right.Holders -join '|') -cne ($fromAccounts -join '|')) { $errors+='LSA holder and per-account observations differ; retry a consistent snapshot.' } + } + } + try { $crash=Get-WelaRegistryState -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name CrashOnAuditFail } + catch { $errors+="CrashOnAuditFail : $($_.Exception.Message)" } + } + [pscustomobject]@{Host=$hostState;Rights=$rights;Accounts=$accounts;CrashOnAuditFail=$crash;Errors=$errors} +} +function Get-WelaIntegrityProfiles { + $catalog=Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/audit_integrity_profiles.json') -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop + if ($catalog.SchemaVersion -ne 1) { throw 'Unsupported integrity profile catalog.' } + return @($catalog.Profiles) +} +function Get-WelaIntegrityStateKey { + param($Snapshot) + ConvertTo-Json -InputObject $Snapshot -Depth 14 -Compress +} +function Get-WelaIntegrityPrincipal { + param([string]$Sid) + $name=switch ($Sid) { 'S-1-5-19' {'LOCAL SERVICE'} 'S-1-5-20' {'NETWORK SERVICE'} 'S-1-5-32-544' {'BUILTIN\Administrators'} default {$null} } + if (-not $name -and $env:OS -eq 'Windows_NT') { try { $name=([Security.Principal.SecurityIdentifier]::new($Sid)).Translate([Security.Principal.NTAccount]).Value } catch { } } + [pscustomobject]@{Sid=$Sid;Name=$name;NameStatus=$(if ($name) {'Resolved'} else {'Unresolved; SID remains authoritative'})} +} +function Get-WelaIntegrityPlan { + param($Snapshot,[string]$Profile,[switch]$AllowPrivilegeRemoval) + $source=$null;$blockers=@($Snapshot.Errors);$operations=@();$rows=@() + if ($Profile) { + $matches=@(Get-WelaIntegrityProfiles | Where-Object Id -eq $Profile) + if ($matches.Count -ne 1) { throw "Unknown audit integrity profile: $Profile" } + $source=$matches[0] + if ($Snapshot.Host.Status -ne 'Known' -or $source.Role -ne $Snapshot.Host.Role -or $Snapshot.Host.Build -lt $source.MinBuild -or $Snapshot.Host.Build -gt $source.MaxBuild) { $blockers+='Selected source profile does not match the actual Windows role/build.' } + } + foreach ($right in @('SeAuditPrivilege','SeSecurityPrivilege')) { + $observed=@($Snapshot.Rights | Where-Object Name -eq $right) + $current=@(if ($observed.Count -eq 1) { $observed[0].Holders }) + $desired=$null; if ($source -and $null -ne $source.$right) { $desired=@($source.$right | Sort-Object -Unique) } + $adds=@();$removes=@();$mode='Preserve' + if ($null -ne $desired) { + $mode='Exact';$adds=@($desired | Where-Object { $current -notcontains $_ });$removes=@($current | Where-Object { $desired -notcontains $_ }) + if ($removes.Count -and -not $AllowPrivilegeRemoval) { $blockers+="$right has extra principals; inspect service/dependency exceptions and explicitly use -AllowPrivilegeRemoval before revoking them." } + foreach ($sid in $adds) { $operations+=[pscustomobject]@{Kind='Right';Right=$right;Sid=$sid;Grant=$true} } + foreach ($sid in $removes) { $operations+=[pscustomobject]@{Kind='Right';Right=$right;Sid=$sid;Grant=$false} } + } + $rows+=[pscustomobject]@{Name=$right;Mode=$mode;SourceSetting=$(if (-not $source) {'NoSourceSelected'} elseif ($null -eq $desired) {'OmittedBySource'} else {'ExplicitRequirement'});ObservedStatus=$(if ($observed.Count) {$observed[0].Status} else {'Unknown'});Current=$current;Desired=$desired;Add=@($adds|ForEach-Object {Get-WelaIntegrityPrincipal $_});Remove=@($removes|ForEach-Object {Get-WelaIntegrityPrincipal $_})} + } + $crash=$Snapshot.CrashOnAuditFail;$crashStatus='Unknown' + if ($crash -and $crash.KeyExists) { + if (-not $crash.ValueExists) { $crashStatus='Absent; no explicit value observed' } + elseif ($crash.Type -eq 'DWord' -and $crash.Value -in @(0,1)) { $crashStatus=if ($crash.Value -eq 0) {'Disabled'} else {'Enabled'} } + elseif ($crash.Type -eq 'DWord' -and $crash.Value -eq 2) { $crashStatus='RecoveryRequired';$blockers+='CrashOnAuditFail=2 is a recovery state. This workflow never resets it or clears the Security log.' } + else { $blockers+='Unknown CrashOnAuditFail type/value; preserve it for manual review.' } + } else { $blockers+='LSA registry key/state could not be verified.' } + $crashMode='Preserve';$crashDesired=$null + if ($source -and $null -ne $source.CrashOnAuditFail) { + if ($source.CrashOnAuditFail -ne 0) { throw 'Only reviewed disabled CrashOnAuditFail profiles are supported.' } + $crashMode='Exact';$crashDesired=0 + if ($crashStatus -ne 'Disabled') { $operations+=[pscustomobject]@{Kind='Registry';Name='CrashOnAuditFail';Desired=0} } + } + $rows+=[pscustomobject]@{Name='CrashOnAuditFail';Mode=$crashMode;SourceSetting=$(if (-not $source) {'NoSourceSelected'} elseif ($null -eq $crashDesired) {'OmittedBySource'} else {'ExplicitRequirement'});ObservedStatus=$crashStatus;Current=$crash;Desired=$crashDesired;Add=@();Remove=@()} + [pscustomobject]@{Profile=$source;Before=$Snapshot;Controls=$rows;Operations=$operations;Blockers=@($blockers|Select-Object -Unique);AllowPrivilegeRemoval=[bool]$AllowPrivilegeRemoval; + Exceptions=@('IIS application pools can require SeAuditPrivilege.','AD FS service identities can require SeAuditPrivilege.','Exchange Servers can require SeSecurityPrivilege on DCs.','Other application dependencies must be reviewed before any removal.'); + VerificationScope='Local LSA direct assignments and typed registry readback only. Existing tokens, GPO persistence, benign event generation, ingestion and effective user/group access are not verified.'} +} +function Copy-WelaIntegrityExpected { + param($Snapshot) + Get-WelaIntegrityStateKey $Snapshot | ConvertFrom-Json +} +function Update-WelaIntegrityExpected { + param($Snapshot,$Operation) + if ($Operation.Kind -eq 'Registry') { + $Snapshot.CrashOnAuditFail.ValueExists=$true;$Snapshot.CrashOnAuditFail.Value=0;$Snapshot.CrashOnAuditFail.Type='DWord' + } else { + $right=@($Snapshot.Rights|Where-Object Name -eq $Operation.Right)[0] + $account=@($Snapshot.Accounts|Where-Object Sid -eq $Operation.Sid)[0] + if (-not $right -or -not $account) { throw 'Missing affected principal in the recovery snapshot.' } + if ($Operation.Grant) { $right.Holders=@(@($right.Holders)+$Operation.Sid|Sort-Object -Unique);$account.Rights=@(@($account.Rights)+$Operation.Right|Sort-Object -Unique) } + else { $right.Holders=@($right.Holders|Where-Object {$_ -ne $Operation.Sid});$account.Rights=@($account.Rights|Where-Object {$_ -ne $Operation.Right}) } + } +} +function Set-WelaIntegrityControls { + param($Context,$Plan) + $expected=Copy-WelaIntegrityExpected $Plan.Before + foreach ($operation in $Plan.Operations) { Update-WelaIntegrityExpected $expected $operation } + $state=@{Plan=$Plan;Expected=$expected;ObserveSids=@($Plan.Before.Accounts.Sid)} + $read={param($s) Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids} + $test={param($snapshot,$s) -not $s.Plan.Blockers.Count -and (Get-WelaIntegrityStateKey $snapshot) -ceq (Get-WelaIntegrityStateKey $s.Expected)} + $apply={ + param($s) + if ($s.Plan.Blockers.Count) { throw ($s.Plan.Blockers -join ' ') } + $expected=Copy-WelaIntegrityExpected $s.Plan.Before + if ((Get-WelaIntegrityStateKey (Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids)) -cne (Get-WelaIntegrityStateKey $expected)) { throw 'Audit-integrity state changed since planning; no write was sent.' } + foreach ($operation in $s.Plan.Operations) { + $fresh=Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids + if ((Get-WelaIntegrityStateKey $fresh) -cne (Get-WelaIntegrityStateKey $expected)) { throw 'Audit-integrity state changed before the next write; remaining operations stopped.' } + if ($operation.Kind -eq 'Right') { Set-WelaIntegrityAccountRight -Sid $operation.Sid -Right $operation.Right -Grant $operation.Grant } + else { Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name CrashOnAuditFail -Value 0 -Type DWord -ErrorAction Stop } + Update-WelaIntegrityExpected $expected $operation + $after=Get-WelaIntegritySnapshot -ObserveSids $s.ObserveSids + if ((Get-WelaIntegrityStateKey $after) -cne (Get-WelaIntegrityStateKey $expected)) { throw 'Audit-integrity write/readback or unrelated-right preservation did not verify; remaining operations stopped. Review the recovery journal.' } + } + } + Invoke-WelaConfigurationControl -Context $Context -Id 'AuditIntegrity/LocalPolicy' -Kind 'AuditIntegritySet' -Target @('SeAuditPrivilege','SeSecurityPrivilege','CrashOnAuditFail') -Desired $Plan ` + -Read $read -Compliant $test -Apply $apply -CallbackState $state -Description 'Apply the exact listed local privilege additions/removals and selected CrashOnAuditFail policy. Review every affected SID and service exception first.' +} +function Invoke-WelaIntegrityCommand { + param([ValidateSet('Audit','Plan','Configure')][string]$Action='Audit',[string]$Profile,[switch]$AllowPrivilegeRemoval,[switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath) + if ($Action -ne 'Audit' -and -not $Profile) { throw 'Plan and Configure require an explicit -IntegrityProfile.' } + if ($AllowPrivilegeRemoval -and (-not $Profile -or $Action -eq 'Audit')) { throw '-AllowPrivilegeRemoval requires an explicit profile with Plan or Configure.' } + if ($DryRun -and $Action -ne 'Configure') { throw '-DryRun requires IntegrityAction Configure; Audit and Plan are read-only.' } + $snapshot=Get-WelaIntegritySnapshot + $plan=Get-WelaIntegrityPlan -Snapshot $snapshot -Profile $Profile -AllowPrivilegeRemoval:$AllowPrivilegeRemoval + $report=[pscustomobject]@{Scope='audit-integrity-local-policy-only';ExitCode=$(if ($plan.Blockers.Count) {1} else {0});Action=$Action;Plan=$plan} + # Show complete affected principals before the shared runner asks for consent. + Write-Host "Observed host: $($snapshot.Host.ComputerName); role/build: $($snapshot.Host.Role)/$($snapshot.Host.Build); source profile: $Profile" + Write-Host 'Plan observations (before any configuration):' + foreach ($row in $plan.Controls) { + Write-Host "$($row.Name): $($row.Mode); $($row.SourceSetting); $($row.ObservedStatus)" + Write-Host (' Current: ' + (ConvertTo-Json -InputObject $row.Current -Depth 4 -Compress)) + if ($row.Mode -eq 'Exact') { Write-Host (' Requested: ' + (ConvertTo-Json -InputObject $row.Desired -Compress)) } + foreach ($principal in $row.Add) { Write-Host " ADD: $($principal.Sid) ($($principal.Name))" } + foreach ($principal in $row.Remove) { Write-Host " REMOVE: $($principal.Sid) ($($principal.Name))" } + } + if (@($plan.Controls.Remove).Count) { Write-Host ($plan.Exceptions -join ' ') } + foreach ($blocker in $plan.Blockers) { Write-Host "Blocked: $blocker" -ForegroundColor Yellow } + Write-Host $plan.VerificationScope + if ($Action -eq 'Configure') { + # Refuse unresolved scope/recovery/removal decisions even before creating a journal directory. + if ($plan.Blockers.Count) { throw ($plan.Blockers -join ' ') } + $context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath + Set-WelaIntegrityControls -Context $context -Plan $plan + $report=Complete-WelaConfiguration -Context $context -Scope 'audit-integrity-local-policy-only' -SuccessMessage 'Audit-integrity configuration completed. Applied/AlreadyCompliant rows verify local settings; skipped rows do not. Token, GPO and event evidence remain separate checks.' + $report|Add-Member NoteProperty Action $Action + $report|Add-Member NoteProperty Plan $plan + } + $report|Add-Member NoteProperty SigmaEvtxCredit 0 + if ($ResultsPath) { $report|ConvertTo-Json -Depth 18|Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } + return $report +} diff --git a/scripts/AuditIntegrityNative.cs b/scripts/AuditIntegrityNative.cs new file mode 100644 index 00000000..a4d8fca1 --- /dev/null +++ b/scripts/AuditIntegrityNative.cs @@ -0,0 +1,59 @@ +// Local LSA policy only. No remote target, account deletion or all-rights replacement. +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Security.Principal; +namespace Wela { + public static class AuditIntegrityNative { + [StructLayout(LayoutKind.Sequential)] struct LSA_UNICODE_STRING { public ushort Length; public ushort MaximumLength; public IntPtr Buffer; } + [StructLayout(LayoutKind.Sequential)] struct LSA_OBJECT_ATTRIBUTES { public int Length; public IntPtr RootDirectory; public IntPtr ObjectName; public uint Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; } + [DllImport("advapi32.dll")] static extern uint LsaOpenPolicy(IntPtr system, ref LSA_OBJECT_ATTRIBUTES attributes, uint access, out IntPtr handle); + [DllImport("advapi32.dll")] static extern uint LsaClose(IntPtr handle); + [DllImport("advapi32.dll")] static extern uint LsaFreeMemory(IntPtr memory); + [DllImport("advapi32.dll")] static extern uint LsaNtStatusToWinError(uint status); + [DllImport("advapi32.dll")] static extern uint LsaEnumerateAccountsWithUserRight(IntPtr handle, ref LSA_UNICODE_STRING right, out IntPtr buffer, out uint count); + [DllImport("advapi32.dll")] static extern uint LsaEnumerateAccountRights(IntPtr handle, byte[] sid, out IntPtr buffer, out uint count); + [DllImport("advapi32.dll")] static extern uint LsaAddAccountRights(IntPtr handle, byte[] sid, [In] LSA_UNICODE_STRING[] rights, uint count); + [DllImport("advapi32.dll")] static extern uint LsaRemoveAccountRights(IntPtr handle, byte[] sid, [MarshalAs(UnmanagedType.U1)] bool allRights, [In] LSA_UNICODE_STRING[] rights, uint count); + static void Check(uint status) { if (status != 0) throw new Win32Exception((int)LsaNtStatusToWinError(status), "Local LSA operation failed (NTSTATUS 0x" + status.ToString("X8") + ")."); } + static void CheckRight(string right) { if (right != "SeAuditPrivilege" && right != "SeSecurityPrivilege") throw new ArgumentException("Only the two audit-integrity rights are supported."); } + static IntPtr Open(bool write) { + LSA_OBJECT_ATTRIBUTES attributes = new LSA_OBJECT_ATTRIBUTES(); attributes.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES)); + IntPtr handle; Check(LsaOpenPolicy(IntPtr.Zero, ref attributes, 0x00000801u | (write ? 0x00000010u : 0u), out handle)); return handle; + } + static LSA_UNICODE_STRING Text(string value) { + LSA_UNICODE_STRING text = new LSA_UNICODE_STRING(); text.Buffer = Marshal.StringToHGlobalUni(value); + text.Length = checked((ushort)(value.Length * 2)); text.MaximumLength = checked((ushort)(text.Length + 2)); return text; + } + static byte[] Sid(string value) { SecurityIdentifier sid = new SecurityIdentifier(value); byte[] bytes = new byte[sid.BinaryLength]; sid.GetBinaryForm(bytes,0); return bytes; } + public static string[] Holders(string right) { + CheckRight(right); IntPtr handle = Open(false); IntPtr buffer = IntPtr.Zero; LSA_UNICODE_STRING text = Text(right); + try { + uint count; uint status = LsaEnumerateAccountsWithUserRight(handle, ref text, out buffer, out count); + if (status == 0x8000001Au) return new string[0]; // STATUS_NO_MORE_ENTRIES, documented empty assignment. + Check(status); List result = new List(); + for (uint i=0; i result = new List(); + for (uint i=0; i