From c7cfb0d1128db7ae6397365a6be671d617eb7034 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 07:15:31 +0900 Subject: [PATCH] Reject LDAP options before profile command dispatch --- CHANGELOG-Japanese.md | 2 +- CHANGELOG.md | 2 +- WELA.ps1 | 8 ++--- docs/ldap-diagnostics.md | 2 ++ tests/LdapDiagnostics.Tests.ps1 | 48 ++++++++++++++++++++++++-- website/docs/resources/changelog.ja.md | 2 +- website/docs/resources/changelog.md | 2 +- 7 files changed, 55 insertions(+), 11 deletions(-) diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 77a063c9..962c7ead 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,7 +4,7 @@ **改善:** -- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#383) (@Shirofune-Security) +- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。LDAP専用オプションを他のプロファイルコマンドへ指定すると、実行前に拒否します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#383) (@Shirofune-Security) - MDIのドメイン/Exchange Configuration監査と、明示的に選択した証明書テンプレート/登録サービスオブジェクト向けに、任意実行の`ad-object-sacl`監査・計画・設定・保守的なロールバックを追加しました。接続先DCとスキーマGUIDを検証し、既存のセキュリティ設定を保持したまま不足する監査ACEだけをSACLに追加します。変更前のSDDLと追加ACEを保存し、書き込み後と最終状態を確認します。任意のdMSA前提条件が不明な場合は未確認のスキップ項目として報告し、独立した他の5種類のドメインクラスの監査ACEは引き続き設定します。実効監査ポリシー、継承・レプリケーション、4662/5136イベントの証拠は隔離DCで別途検証が必要です。Sigma検知範囲の向上は未検証です。 (#402) (@Shirofune-Security) - Microsoft WEF Appendix Cのチャネルを監査・計画・設定する任意実行の`channel-settings`を追加しました。CAPI2の有効化、原典の正確なバイト数、明示的に指定したEvent Log Readersの読み取りACEに対応します。既存のセキュリティ記述子・ACEと大きいバッファを保持し、安全に扱えないACLは変更しません。共通の復旧記録、書き込み直前の状態確認と読み戻しで失敗を報告します。Appendix E/Fの標準チャネル一覧からSysmonとEMETを除外し、実際のIDによるアクセス・イベント生成・収集は未検証と表示します。Windowsラボでの検証は別途必要です。 (#401) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index b62472cf..563f3b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Improvements:** -- Preserved LDAP 1644 diagnostics during normal configuration instead of automatically enabling Field Engineering level 5 on DCs. Added explicit `ldap-diagnostics` audit/plan/configure modes for preservation, tunable diagnostics and MDI legacy cleanup, with role/build checks, typed recovery snapshots, race guards, ordered readback and final drift checks. Generated events, volume and forwarding remain isolated-DC validation. (#383) (@Shirofune-Security) +- Preserved LDAP 1644 diagnostics during normal configuration instead of automatically enabling Field Engineering level 5 on DCs. Added explicit `ldap-diagnostics` audit/plan/configure modes for preservation, tunable diagnostics and MDI legacy cleanup, with role/build checks, typed recovery snapshots, race guards, ordered readback and final drift checks. LDAP-only options are rejected before unrelated profile commands can run. Generated events, volume and forwarding remain isolated-DC validation. (#383) (@Shirofune-Security) - Added opt-in `ad-object-sacl` audit, plan, configure and conservative rollback actions for MDI domain/Exchange Configuration auditing and explicitly selected certificate template/enrollment service objects. Exact DC binding, schema GUID checks, additive SACL-only changes, pre-write SDDL/ACE receipts and read-back preserve existing security entries. Unknown optional dMSA prerequisites are reported as a separate skipped gap while the five independent domain class ACEs continue. Effective audit policy, inheritance/replication and 4662/5136 event evidence remain separate isolated-DC checks; no Sigma uplift is claimed. (#402) (@Shirofune-Security) - Added opt-in `channel-settings` audit, plan and configure actions for Microsoft WEF Appendix C, including CAPI2 enablement, exact source byte sizes and an explicitly requested Event Log Readers read ACE. Existing descriptor components/ACEs and larger buffers are preserved or unsafe ACL edits are refused; shared journaling, fresh-state guards and readback report failures. Native Appendix E/F channel inventories exclude Sysmon/EMET and retain unverified identity access, generation and ingestion prerequisites. Windows lab evidence remains pending. (#401) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index 4b3c6683..a02ddce6 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -1794,15 +1794,15 @@ if (($PSBoundParameters.ContainsKey('ChannelAction') -or $PSBoundParameters.Cont throw 'Channel options require channel-settings. No command was run.' } +if ($Cmd -ne 'ldap-diagnostics' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('LdapAction','LdapMode','LdapSearchTimeMs','LdapExpensiveThreshold','LdapInefficientThreshold') }).Count) { + throw 'LDAP options require the dedicated ldap-diagnostics command. No command was run.' +} + if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'configure') -and -not $Help) { Invoke-WelaProfileCommand -Command $Cmd.ToLower() return } -if ($Cmd -ne 'ldap-diagnostics' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('LdapAction','LdapMode','LdapSearchTimeMs','LdapExpensiveThreshold','LdapInefficientThreshold') }).Count) { - throw 'LDAP options require the dedicated ldap-diagnostics command. No command was run.' -} - switch ($Cmd.ToLower()) { 'ldap-diagnostics' { if ($Help) { Write-Host 'Usage: ./WELA.ps1 ldap-diagnostics [-LdapAction Audit|Plan|Configure] [-LdapMode Preserve|Diagnostic|MdiCleanup] [-LdapSearchTimeMs positive-ms] [-LdapExpensiveThreshold positive-count] [-LdapInefficientThreshold positive-count] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath report.json]. See docs/ldap-diagnostics.md.'; return } diff --git a/docs/ldap-diagnostics.md b/docs/ldap-diagnostics.md index 623f25ea..3919aab7 100644 --- a/docs/ldap-diagnostics.md +++ b/docs/ldap-diagnostics.md @@ -4,6 +4,8 @@ Normal `configure` preserves existing LDAP diagnostics and explains that 1644 is The dedicated local workflow supports Server 2022/2025 DC builds 20348/26100 in 64-bit Windows PowerShell 5.1 or PowerShell 7. Client, member server and member-server CA hosts are not applicable. Unknown or conflicting role/build evidence blocks writes; no NTDS settings are created on a non-DC. +LDAP options require the dedicated `ldap-diagnostics` command. Supplying them to another command, including `configure -Profile`, stops before that command runs. + ```powershell ./WELA.ps1 ldap-diagnostics -LdapAction Audit -ResultsPath ldap-before.json ./WELA.ps1 ldap-diagnostics -LdapAction Plan -LdapMode Diagnostic -LdapSearchTimeMs 100 diff --git a/tests/LdapDiagnostics.Tests.ps1 b/tests/LdapDiagnostics.Tests.ps1 index 4bb5b975..2ff5c8c7 100644 --- a/tests/LdapDiagnostics.Tests.ps1 +++ b/tests/LdapDiagnostics.Tests.ps1 @@ -11,7 +11,7 @@ function Reject([scriptblock]$Action,[string]$Pattern) { function Reset-Fixture { $script:values=@{}; $script:writes=@(); $script:role='Applicable'; $script:readError=$false $script:writeError=$false; $script:ignoreWrite=$false; $script:race=$false; $script:reads=0 - $script:types=@{}; $script:journal=$null + $script:types=@{}; $script:journal=$null; $script:failName=$null } function Get-WelaLdapHost { [pscustomobject]@{Status=$script:role;Diagnostic='fixture';ComputerName='dc1';Build=26100} } function Get-WelaRegistryState { @@ -25,14 +25,14 @@ function New-WelaRegistryKey { param($Path) } function Set-ItemProperty { param($LiteralPath,$Name,$Value,$Type,$ErrorAction) Assert ($script:journal -and (Test-Path $script:journal)) 'Every native write must follow the complete pre-change journal.' - if ($script:writeError) { throw 'write denied' } + if ($script:writeError -or $Name -eq $script:failName) { throw 'write denied' } $script:writes += $Name if (-not $script:ignoreWrite) { $script:values[$Name]=$Value; $script:types[$Name]=$Type } } function Remove-ItemProperty { param($LiteralPath,$Name,$ErrorAction) Assert ($script:journal -and (Test-Path $script:journal)) 'Every removal must follow a recovery journal.' - if ($script:writeError) { throw 'remove denied' } + if ($script:writeError -or $Name -eq $script:failName) { throw 'remove denied' } $script:writes += $Name if (-not $script:ignoreWrite) { $script:values.Remove($Name); $script:types.Remove($Name) } } @@ -95,6 +95,31 @@ try { default { Assert ($result.ExitCode -eq 1 -and $script:writes.Count -eq 0) "Scenario $scenario must block before native writes." } } } + # Exercise actual partial mutation boundaries: recovery is journal-based and + # does not automatically overwrite the successfully changed subset. + foreach ($mode in @('Diagnostic','MdiCleanup')) { + Reset-Fixture + $thresholds=@{SearchTime=100;Expensive=10000;Inefficient=1000} + if ($mode -eq 'MdiCleanup') { + $thresholds=@{} + foreach ($definition in Get-WelaLdapDefinitions) { $script:values[$definition.Name]=1; $script:types[$definition.Name]='DWord' } + $script:values['15 Field Engineering']=5 + $script:failName='Search Time Threshold (msecs)' + } else { $script:failName='15 Field Engineering' } + $script:values['Unrelated diagnostic']=7; $script:types['Unrelated diagnostic']='DWord' + $plan=Get-WelaLdapPlan (Get-WelaLdapSnapshot) $mode $thresholds + $context=New-FixtureContext + Set-WelaLdapDiagnostics $context $plan + $result=Complete-WelaConfiguration $context + $journal=Get-Content $script:journal -Raw | ConvertFrom-Json + Assert ($result.ExitCode -eq 1 -and $journal.Before.Values.Count -eq 4) 'A failure after earlier writes retains the complete recovery snapshot.' + Assert ($script:values['Unrelated diagnostic'] -eq 7) 'Partial failures preserve unrelated registry values.' + if ($mode -eq 'Diagnostic') { + Assert ($script:writes.Count -eq 3 -and -not $script:values.ContainsKey('15 Field Engineering')) 'Failed verbosity write leaves verified thresholds and does not claim success.' + } else { + Assert ($script:writes.Count -eq 1 -and -not $script:values.ContainsKey('15 Field Engineering') -and $script:values['Search Time Threshold (msecs)'] -eq 1 -and $script:values['Expensive Search Results Threshold'] -eq 1 -and $script:values['Inefficient Search Results Threshold'] -eq 1) 'Cleanup stops after the first failed threshold removal without restoring verbosity or removing later values.' + } + } Reset-Fixture; $script:role='NotApplicable' $report=Invoke-WelaLdapCommand -Action Audit Assert ($report.Snapshot.Values.Count -eq 0 -and $script:reads -eq 0) 'Member/client/non-DC CA never queries or creates NTDS settings.' @@ -104,6 +129,23 @@ try { # Execute the real general configure body with all native boundaries replaced. $ast=[Management.Automation.Language.Parser]::ParseFile((Join-Path $script:ScriptRoot 'WELA.ps1'),[ref]$null,[ref]$null) + # Preserve the real ordering of the LDAP guard and early profile dispatch, while + # replacing the profile handler so configure can never change this test host. + $dispatchNodes=@($ast.EndBlock.Statements | Where-Object { + $_ -is [Management.Automation.Language.IfStatementAst] -and + ($_.Extent.Text -match 'LDAP options require' -or $_.Extent.Text -match 'Invoke-WelaProfileCommand -Command') + }) + Assert ($dispatchNodes.Count -eq 2) 'Exercise both real CLI boundaries in source order.' + $dispatch=[scriptblock]::Create('param($Cmd,$Profile,$LdapAction,$LdapMode,$LdapSearchTimeMs,$LdapExpensiveThreshold,$LdapInefficientThreshold)' + [Environment]::NewLine + (($dispatchNodes | ForEach-Object {$_.Extent.Text}) -join [Environment]::NewLine)) + function Invoke-WelaProfileCommand { param($Command) $script:profileDispatched=$true } + foreach ($command in @('plan','audit','audit-settings','configure')) { + foreach ($option in @('LdapAction','LdapMode','LdapSearchTimeMs','LdapExpensiveThreshold','LdapInefficientThreshold')) { + $script:profileDispatched=$false + $arguments=@{Cmd=$command;Profile='fixture'}; $arguments[$option]='fixture' + Reject { & $dispatch @arguments } 'LDAP options require' + Assert (-not $script:profileDispatched) "LDAP option $option must block $command before unrelated profile dispatch." + } + } $node=$ast.Find({param($n) $n -is [Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq 'ConfigureAuditSettings'},$false) . ([scriptblock]::Create($node.Extent.Text)) function TestWindows {$true}; function TestAdministrator {$true} diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index faae013c..7edbbdb9 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,7 +7,7 @@ **改善:** -- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#383) (@Shirofune-Security) +- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。LDAP専用オプションを他のプロファイルコマンドへ指定すると、実行前に拒否します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#383) (@Shirofune-Security) - MDIのドメイン/Exchange Configuration監査と、明示的に選択した証明書テンプレート/登録サービスオブジェクト向けに、任意実行の`ad-object-sacl`監査・計画・設定・保守的なロールバックを追加しました。接続先DCとスキーマGUIDを検証し、既存のセキュリティ設定を保持したまま不足する監査ACEだけをSACLに追加します。変更前のSDDLと追加ACEを保存し、書き込み後と最終状態を確認します。任意のdMSA前提条件が不明な場合は未確認のスキップ項目として報告し、独立した他の5種類のドメインクラスの監査ACEは引き続き設定します。実効監査ポリシー、継承・レプリケーション、4662/5136イベントの証拠は隔離DCで別途検証が必要です。Sigma検知範囲の向上は未検証です。 (#402) (@Shirofune-Security) - Microsoft WEF Appendix Cのチャネルを監査・計画・設定する任意実行の`channel-settings`を追加しました。CAPI2の有効化、原典の正確なバイト数、明示的に指定したEvent Log Readersの読み取りACEに対応します。既存のセキュリティ記述子・ACEと大きいバッファを保持し、安全に扱えないACLは変更しません。共通の復旧記録、書き込み直前の状態確認と読み戻しで失敗を報告します。Appendix E/Fの標準チャネル一覧からSysmonとEMETを除外し、実際のIDによるアクセス・イベント生成・収集は未検証と表示します。Windowsラボでの検証は別途必要です。 (#401) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 533d5a64..5cdbc775 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,7 +7,7 @@ **Improvements:** -- Preserved LDAP 1644 diagnostics during normal configuration instead of automatically enabling Field Engineering level 5 on DCs. Added explicit `ldap-diagnostics` audit/plan/configure modes for preservation, tunable diagnostics and MDI legacy cleanup, with role/build checks, typed recovery snapshots, race guards, ordered readback and final drift checks. Generated events, volume and forwarding remain isolated-DC validation. (#383) (@Shirofune-Security) +- Preserved LDAP 1644 diagnostics during normal configuration instead of automatically enabling Field Engineering level 5 on DCs. Added explicit `ldap-diagnostics` audit/plan/configure modes for preservation, tunable diagnostics and MDI legacy cleanup, with role/build checks, typed recovery snapshots, race guards, ordered readback and final drift checks. LDAP-only options are rejected before unrelated profile commands can run. Generated events, volume and forwarding remain isolated-DC validation. (#383) (@Shirofune-Security) - Added opt-in `ad-object-sacl` audit, plan, configure and conservative rollback actions for MDI domain/Exchange Configuration auditing and explicitly selected certificate template/enrollment service objects. Exact DC binding, schema GUID checks, additive SACL-only changes, pre-write SDDL/ACE receipts and read-back preserve existing security entries. Unknown optional dMSA prerequisites are reported as a separate skipped gap while the five independent domain class ACEs continue. Effective audit policy, inheritance/replication and 4662/5136 event evidence remain separate isolated-DC checks; no Sigma uplift is claimed. (#402) (@Shirofune-Security) - Added opt-in `channel-settings` audit, plan and configure actions for Microsoft WEF Appendix C, including CAPI2 enablement, exact source byte sizes and an explicitly requested Event Log Readers read ACE. Existing descriptor components/ACEs and larger buffers are preserved or unsafe ACL edits are refused; shared journaling, fresh-state guards and readback report failures. Native Appendix E/F channel inventories exclude Sysmon/EMET and retain unverified identity access, generation and ingestion prerequisites. Windows lab evidence remains pending. (#401) (@Shirofune-Security)