From b2b7e0a9f7bdb0d3624e55a98f5c775d8abfd23d Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 07:01:50 +0900 Subject: [PATCH 1/5] Correct legacy token audit GUID and validate catalog mapping uncertainty --- .github/workflows/audit-catalog-mappings.yml | 25 +++++++ CHANGELOG-Japanese.md | 2 + CHANGELOG.md | 2 + WELA.ps1 | 5 +- config/baselines.json | 2 +- docs/audit-catalog-mappings.md | 17 +++++ modules/AuditCatalog.psm1 | 72 ++++++++++++++++++++ scripts/Review-AuditCatalog.ps1 | 24 +++++++ tests/AuditCatalogMappings.Tests.ps1 | 62 +++++++++++++++++ tests/AuditCatalogMappings.Windows.Tests.ps1 | 21 ++++++ tests/AuditProfileOutput.Tests.ps1 | 1 + tests/NativeProviders.Tests.ps1 | 2 + website/docs/resources/changelog.ja.md | 2 + website/docs/resources/changelog.md | 2 + 14 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/audit-catalog-mappings.yml create mode 100644 docs/audit-catalog-mappings.md create mode 100644 modules/AuditCatalog.psm1 create mode 100644 scripts/Review-AuditCatalog.ps1 create mode 100644 tests/AuditCatalogMappings.Tests.ps1 create mode 100644 tests/AuditCatalogMappings.Windows.Tests.ps1 diff --git a/.github/workflows/audit-catalog-mappings.yml b/.github/workflows/audit-catalog-mappings.yml new file mode 100644 index 00000000..e5bf0fb6 --- /dev/null +++ b/.github/workflows/audit-catalog-mappings.yml @@ -0,0 +1,25 @@ +name: Audit catalog mapping regressions +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + audit-catalog-mappings: + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Catalog, ambiguity and renderer fixtures on Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditCatalogMappings.Tests.ps1 + - name: Native identifier observations on Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditCatalogMappings.Windows.Tests.ps1 + - name: Catalog, ambiguity and renderer fixtures on PowerShell 7 + shell: pwsh + run: ./tests/AuditCatalogMappings.Tests.ps1 + - name: Native identifier observations on PowerShell 7 + shell: pwsh + run: ./tests/AuditCatalogMappings.Windows.Tests.ps1 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 9cb8750f..51caeda7 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,8 @@ **改善:** +- 従来のToken Right Adjusted EventsのGUIDを修正し、すべてのベースラインでRPCとトークン監査の状態を個別に評価するようにしました。実行時のカタログID・明示的な名前別表記の検証と、ハッシュ付きの読み取り専用EventID対応表レビューを追加しました。曖昧な対応、カテゴリだけの対応、不明な対応から検知可能とは判断しません。 (#380) (@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 34855338..2a9151cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Improvements:** +- Corrected the legacy Token Right Adjusted Events GUID so RPC and token auditing are assessed independently in every baseline. Added runtime catalog identity/alias checks and a read-only, fingerprinted EventID mapping review that preserves ambiguous, category-only and unknown candidates without detection credit. (#380) (@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 30ace2ba..19b22321 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -57,6 +57,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" . (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1") . (Join-Path $ScriptRoot "scripts/WmiNamespaceAuditing.ps1") Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop +Import-Module (Join-Path $ScriptRoot "modules/AuditCatalog.psm1") -ErrorAction Stop Import-Module (Join-Path $ScriptRoot "modules/NativeProviders.psm1") -ErrorAction Stop Import-Module (Join-Path $ScriptRoot "modules/EventLogSettings.psm1") -ErrorAction Stop . (Join-Path $ScriptRoot "scripts/EventLogConfiguration.ps1") @@ -327,7 +328,9 @@ function GetBaselineConfig { if (-not (Test-Path -Path $script:BaselineConfigPath)) { throw "Baseline config not found: $script:BaselineConfigPath" } - return Get-Content -Path $script:BaselineConfigPath -Raw | ConvertFrom-Json + $data = Get-Content -Path $script:BaselineConfigPath -Raw | ConvertFrom-Json + Assert-WelaAuditCatalog -Catalog $data.catalog -CanonicalCatalog (Import-WelaAuditProfiles).catalog + return $data } function GetBaselineNames { diff --git a/config/baselines.json b/config/baselines.json index 368ad7db..708405ec 100644 --- a/config/baselines.json +++ b/config/baselines.json @@ -456,7 +456,7 @@ "subCategory": "Token Right Adjusted Events", "select": { "type": "guid", - "guid": "0CCE922E-69AE-11D9-BED3-505054503030" + "guid": "0CCE924A-69AE-11D9-BED3-505054503030" }, "currentSetting": { "type": "auditpol" diff --git a/docs/audit-catalog-mappings.md b/docs/audit-catalog-mappings.md new file mode 100644 index 00000000..1ee7d66d --- /dev/null +++ b/docs/audit-catalog-mappings.md @@ -0,0 +1,17 @@ +# Audit identifiers and EventID mapping review + +`Token Right Adjusted Events` now uses `0CCE924A-69AE-11D9-BED3-505054503030` in the legacy baseline catalog, matching the versioned audit catalog. RPC keeps `0CCE922E-69AE-11D9-BED3-505054503030`. This fixes legacy ASD/Microsoft assessments that previously displayed RPC state for token auditing. Recommendations and enablement masks are unchanged. + +Every baseline load checks canonical name/GUID pairs and rejects duplicate legacy identifiers. Only three explicit spelling aliases are accepted: `Non-Sensitive Privilege Use`, `User / Device Claims`, and `Central Policy Staging`. They map to the existing canonical names; no fuzzy matching or arbitrary GUID aliases are allowed. + +Review the bundled EventID candidates without reading or changing Windows: + +```powershell +./scripts/Review-AuditCatalog.ps1 -ResultsPath mapping-review.json +``` + +The export fingerprints the mapping file, lists candidates and reasons per EventID, and separates blank category headings. Missing mappings or candidates with only a category GUID are unknown. Multiple subcategories, unresolved object types, outcomes and role context remain conditional. `DetectionReady` is always false: an identifier review cannot verify a detection. This helper does not replace full rule eligibility or establish the complete Boolean/field requirements of a Sigma rule. + +The bundled CSV remains a historical candidate map, not a universally valid event-generation contract. For example, 4703 appears against both Token Right Adjusted and Authorization Policy Change. Microsoft's [Token Right Adjusted page](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/audit-token-right-adjusted) lists it, while the [4703 event page](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4703) names Authorization Policy Change. WELA retains the conflict rather than inventing a build-independent resolution. Microsoft also states that Token Right Adjusted has no Failure events; setting a Failure mask is not proof of Failure records. + +Fixtures check malformed/duplicate identifiers, unknown events, ambiguous 4703/object mappings, and independent RPC/token state through all four legacy baseline renderers. Windows CI runs [`auditpol /list /subcategory:* /v`](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/auditpol-list) and native policy queries under Windows PowerShell 5.1/PowerShell 7. These checks validate identifiers/readback, not generated event XML. Build-specific client, member-server, DC and CA event/outcome validation remains separate acceptance work for issue #380. Native Windows functionality only; Sysmon is out of scope. diff --git a/modules/AuditCatalog.psm1 b/modules/AuditCatalog.psm1 new file mode 100644 index 00000000..e8563fee --- /dev/null +++ b/modules/AuditCatalog.psm1 @@ -0,0 +1,72 @@ +# Identifier validation is separate from recommendations and detection readiness. +Set-StrictMode -Version 2.0 + +function ConvertTo-WelaCanonicalAuditName { + param([string]$Name) + # Only reviewed spelling aliases; do not accept arbitrary fuzzy matches. + switch -CaseSensitive ($Name) { + 'Non-Sensitive Privilege Use' { return 'Non Sensitive Privilege Use' } + 'User / Device Claims' { return 'User/Device Claims' } + 'Central Policy Staging' { return 'Central Access Policy Staging' } + default { return $Name } + } +} + +function Assert-WelaAuditCatalog { + [CmdletBinding()] + param([Parameter(Mandatory)]$Catalog, [Parameter(Mandatory)]$CanonicalCatalog) + $canonical = @{}; $canonicalGuids = @{}; $seen = @{}; $ids = @{} + foreach ($row in $CanonicalCatalog) { + if (-not $row.id -or $canonical.ContainsKey($row.id) -or + $row.guid -notmatch '^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$' -or $canonicalGuids.ContainsKey($row.guid)) { + throw 'Invalid or duplicate canonical audit identifier.' + } + $canonical[$row.id] = $row.guid; $canonicalGuids[$row.guid] = $true + } + foreach ($row in @($Catalog | Where-Object { $_.currentSetting.type -eq 'auditpol' })) { + $name = ConvertTo-WelaCanonicalAuditName $row.subCategory + if (-not $row.id -or $ids.ContainsKey($row.id)) { throw "Duplicate or empty legacy audit id: $($row.id)" } + if ($row.select.type -ne 'guid' -or -not $canonical.ContainsKey($name) -or $canonical[$name] -ine $row.select.guid) { + throw "Audit catalog name/GUID mismatch: $($row.subCategory) / $($row.select.guid)" + } + if ($seen.ContainsKey($row.select.guid)) { throw "Duplicate legacy audit GUID: $($row.select.guid)" } + $seen[$row.select.guid] = $true; $ids[$row.id] = $true + } +} + +function Get-WelaEventMappingReview { + [CmdletBinding()] + param( + [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Mappings, + [Parameter(Mandatory)]$CanonicalCatalog, + [Parameter(Mandatory)][ValidateRange(0,65535)][int]$EventId + ) + $byGuid = @{} + foreach ($row in $CanonicalCatalog) { $byGuid[$row.guid] = $row.id } + # Empty category-heading rows must never become EventID 0 through a cast. + $matches = @($Mappings | Where-Object { $_.'Event ID' -match '^\d+$' -and [int]$_.'Event ID' -eq $EventId }) + $candidates = @(); $uncertain = @() + foreach ($row in $matches) { + if (-not $row.Subcategory -or -not $byGuid.ContainsKey($row.GUID)) { + $uncertain += 'CategoryOnlyOrUnknownGuid'; continue + } + if ((ConvertTo-WelaCanonicalAuditName $row.Subcategory) -cne $byGuid[$row.GUID]) { + $uncertain += 'NameGuidMismatch'; continue + } + $candidates += [pscustomobject]@{ Name=$byGuid[$row.GUID]; Guid=$row.GUID } + } + $candidates = @($candidates | Sort-Object Guid -Unique) + $reasons = @($uncertain | Select-Object -Unique) + if (-not $matches.Count) { $reasons += 'NoMapping' } + if ($candidates.Count -gt 1) { $reasons += 'MultipleSubcategories' } + # Even an unambiguous mapping does not prove outcome, object, fields or source role. + $reasons += 'OutcomeObjectAndRoleUnverified' + [pscustomobject]@{ + EventId=$EventId + State=$(if (-not $matches.Count -or -not $candidates.Count) { 'Unknown' } else { 'Conditional' }) + MappingCount=$matches.Count; Candidates=$candidates; Reasons=$reasons + DetectionReady=$false + } +} + +Export-ModuleMember -Function Assert-WelaAuditCatalog, Get-WelaEventMappingReview diff --git a/scripts/Review-AuditCatalog.ps1 b/scripts/Review-AuditCatalog.ps1 new file mode 100644 index 00000000..c2ae1ede --- /dev/null +++ b/scripts/Review-AuditCatalog.ps1 @@ -0,0 +1,24 @@ +# Developer-facing, read-only catalog review. No Windows query or policy changes. +param([string]$ResultsPath) +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +Import-Module (Join-Path $PSScriptRoot '../modules/AuditCatalog.psm1') -Force +$root = Split-Path $PSScriptRoot -Parent +$canonical = (Import-WelaAuditProfiles).catalog +$legacy = Get-Content (Join-Path $root 'config/baselines.json') -Raw | ConvertFrom-Json +Assert-WelaAuditCatalog -Catalog $legacy.catalog -CanonicalCatalog $canonical +$mappingPath = Join-Path $root 'config/eid_subcategory_mapping.csv' +$mappings = @(Import-Csv -LiteralPath $mappingPath) +$rows = @($mappings | Where-Object { $_.'Event ID' -match '^\d+$' } | ForEach-Object { [int]$_.'Event ID' } | Sort-Object -Unique | ForEach-Object { + Get-WelaEventMappingReview -Mappings $mappings -CanonicalCatalog $canonical -EventId $_ +}) +$result = [pscustomobject]@{ + SchemaVersion=1; Scope='catalog-identifiers-and-mapping-uncertainty'; GeneratedUtc=[DateTime]::UtcNow.ToString('o') + CanonicalCount=$canonical.Count; LegacyCount=@($legacy.catalog | Where-Object { $_.currentSetting.type -eq 'auditpol' }).Count + MappingSha256=(Get-FileHash -LiteralPath $mappingPath -Algorithm SHA256).Hash + CategoryHeadingRows=@($mappings | Where-Object { -not $_.'Event ID' }).Count + Provenance='Bundled mapping candidates, not a build-specific event-generation contract. See docs/audit-catalog-mappings.md.' + Events=$rows; DetectionReadyCount=0 +} +if ($ResultsPath) { $result | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } +$result diff --git a/tests/AuditCatalogMappings.Tests.ps1 b/tests/AuditCatalogMappings.Tests.ps1 new file mode 100644 index 00000000..c6d3ca8a --- /dev/null +++ b/tests/AuditCatalogMappings.Tests.ps1 @@ -0,0 +1,62 @@ +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +Import-Module (Join-Path $PSScriptRoot '../modules/AuditCatalog.psm1') -Force +$script:count = 0 +function Assert($Condition, $Message) { if (-not $Condition) { throw $Message }; $script:count++ } +function Reject([scriptblock]$Action) { + try { & $Action; throw 'Expected rejection did not occur.' } + catch { Assert ($_.Exception.Message -match 'mismatch|Duplicate|duplicate') "Unexpected failure: $_" } +} +$root = Split-Path $PSScriptRoot -Parent +$canonical = (Import-WelaAuditProfiles).catalog +$legacyPath = Join-Path $root 'config/baselines.json' +$legacy = Get-Content $legacyPath -Raw | ConvertFrom-Json +Assert-WelaAuditCatalog $legacy.catalog $canonical +foreach ($case in @('wrong-guid','duplicate-guid','unknown-name','duplicate-id')) { + $copy = Get-Content $legacyPath -Raw | ConvertFrom-Json + $row = $copy.catalog | Where-Object subCategory -eq 'Token Right Adjusted Events' + switch ($case) { + 'wrong-guid' { $row.select.guid='0CCE922E-69AE-11D9-BED3-505054503030' } + 'duplicate-guid' { $copy.catalog += $row } + 'unknown-name' { $row.subCategory='Token Right Adjusted Typo' } + 'duplicate-id' { ($copy.catalog | Where-Object subCategory -eq 'RPC Events').id=$row.id } + } + Reject { Assert-WelaAuditCatalog $copy.catalog $canonical } +} +$mappings = @(Import-Csv (Join-Path $root 'config/eid_subcategory_mapping.csv')) +$ambiguous = Get-WelaEventMappingReview $mappings $canonical 4703 +Assert ($ambiguous.State -eq 'Conditional' -and $ambiguous.Candidates.Count -eq 2 -and $ambiguous.Reasons -contains 'MultipleSubcategories' -and -not $ambiguous.DetectionReady) '4703 conflicting source mappings must remain conditional.' +$object = Get-WelaEventMappingReview $mappings $canonical 4663 +Assert ($object.State -eq 'Conditional' -and -not $object.DetectionReady) 'Object EventID alone never establishes matching object/SACL/outcome.' +$unknown = Get-WelaEventMappingReview $mappings $canonical 65535 +Assert ($unknown.State -eq 'Unknown' -and $unknown.Candidates.Count -eq 0) 'Unknown events cannot acquire a policy or readiness.' +$zero = Get-WelaEventMappingReview $mappings $canonical 0 +Assert ($zero.MappingCount -eq 0 -and $zero.State -eq 'Unknown') 'Blank category rows must not turn into EventID zero.' +$category = Get-WelaEventMappingReview $mappings $canonical 4608 +Assert ($category.Reasons -contains 'CategoryOnlyOrUnknownGuid' -and -not $category.DetectionReady) 'Category GUIDs cannot establish advanced subcategory readiness.' +$rpc = Get-WelaEventMappingReview $mappings $canonical 5712 +Assert ($rpc.Candidates.Count -eq 1 -and $rpc.Candidates[0].Name -eq 'RPC Events' -and $rpc.State -eq 'Conditional') 'A unique mapping still retains outcome/context uncertainty.' +$bad = [pscustomobject]@{'Event ID'='5712';Subcategory='Token Right Adjusted Events';GUID='0CCE922E-69AE-11D9-BED3-505054503030'} +$mismatch = Get-WelaEventMappingReview @($bad) $canonical 5712 +Assert ($mismatch.State -eq 'Unknown' -and $mismatch.Reasons -contains 'NameGuidMismatch') 'Mismatched candidate metadata must not be accepted.' + +# Exercise the actual legacy renderer for every named baseline with distinct RPC/token state. +. (Join-Path $root 'WELA.ps1') help -Role Client -Build 26100 6>$null | Out-Null +function GetAuditpol { @{ '0CCE922E-69AE-11D9-BED3-505054503030'='Failure'; '0CCE924A-69AE-11D9-BED3-505054503030'='Success' } } +function CheckRegistryValue { $false } +function Get-WelaNativeSources { @() } +function Get-WelaNativeSourceState { 'Unknown' } +function Get-WelaOutgoingNtlmState { [pscustomobject]@{Description='Unknown';PolicySource='Unknown'} } +function Get-WelaDomainNtlmState { [pscustomobject]@{Description='Unknown'} } +foreach ($baselineName in $legacy.baselines.PSObject.Properties.Name) { + $rows = BuildAuditResult -all_rules @() -Baseline $baselineName -enabledguid @('0CCE924A-69AE-11D9-BED3-505054503030') + Assert (($rows | Where-Object SubCategory -eq 'RPC Events').CurrentSetting -eq 'Failure') "$baselineName must read RPC independently." + Assert (($rows | Where-Object SubCategory -eq 'Token Right Adjusted Events').CurrentSetting -eq 'Success') "$baselineName must read Token independently." +} +$tmp = Join-Path ([IO.Path]::GetTempPath()) ('wela-mapping-review-'+[guid]::NewGuid().ToString('N')+'.json') +try { + & (Join-Path $root 'scripts/Review-AuditCatalog.ps1') -ResultsPath $tmp | Out-Null + $export = Get-Content $tmp -Raw | ConvertFrom-Json + Assert ($export.DetectionReadyCount -eq 0 -and $export.MappingSha256.Length -eq 64 -and $export.Events.Count -gt 100) 'Review export retains corpus fingerprint and explicit no-readiness semantics.' +} finally { Remove-Item $tmp -ErrorAction SilentlyContinue } +Write-Host "PASS: $script:count catalog/mapping assertions; all legacy baselines preserve distinct RPC/token state." diff --git a/tests/AuditCatalogMappings.Windows.Tests.ps1 b/tests/AuditCatalogMappings.Windows.Tests.ps1 new file mode 100644 index 00000000..202710ef --- /dev/null +++ b/tests/AuditCatalogMappings.Windows.Tests.ps1 @@ -0,0 +1,21 @@ +# Query only: no audit-policy writes or benign event generation. +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +Import-Module (Join-Path $PSScriptRoot '../modules/AuditCatalog.psm1') -Force +$legacy = Get-Content (Join-Path $PSScriptRoot '../config/baselines.json') -Raw | ConvertFrom-Json +$canonical = (Import-WelaAuditProfiles).catalog +Assert-WelaAuditCatalog $legacy.catalog $canonical +$listing = @(& auditpol.exe /list '/subcategory:*' /v 2>&1) +if ($LASTEXITCODE -ne 0) { throw "auditpol listing failed: $($listing -join ' ')" } +$text = $listing -join "`n" +$current = Get-WelaEffectiveAuditPolicy +foreach ($name in @('RPC Events','Token Right Adjusted Events')) { + $row = $legacy.catalog | Where-Object subCategory -eq $name + if ($text -notmatch [regex]::Escape($row.select.guid) -or -not $current.ContainsKey($row.select.guid)) { throw "Native Windows omitted $name / $($row.select.guid)." } + # The hosted image uses English; on localized hosts only GUID presence is asserted. + if ([Globalization.CultureInfo]::InstalledUICulture.TwoLetterISOLanguageName -eq 'en') { + if (-not @($listing | Where-Object { $_ -match [regex]::Escape($row.select.guid) -and $_ -match [regex]::Escape($name) }).Count) { throw "Native name/GUID mismatch for $name." } + } + Write-Host "$name $($row.select.guid) observed mask=$($current[$row.select.guid])" +} +Write-Host 'PASS: native audit identifiers queried; no policy changes or event-generation claims.' diff --git a/tests/AuditProfileOutput.Tests.ps1 b/tests/AuditProfileOutput.Tests.ps1 index 029cdb02..c577ffff 100644 --- a/tests/AuditProfileOutput.Tests.ps1 +++ b/tests/AuditProfileOutput.Tests.ps1 @@ -1,6 +1,7 @@ # Exercise the real profile, audit renderer, rule coverage and CSV output with # injected audit observations. Only temporary files are written; no Windows policy changes. $ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditCatalog.psm1') -Force Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force $tokens = $null; $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot '../WELA.ps1'), [ref]$tokens, [ref]$parseErrors) diff --git a/tests/NativeProviders.Tests.ps1 b/tests/NativeProviders.Tests.ps1 index 6683eba3..de0a3b8b 100644 --- a/tests/NativeProviders.Tests.ps1 +++ b/tests/NativeProviders.Tests.ps1 @@ -1,5 +1,7 @@ # Real catalog + public audit renderer/exports; Windows reads are injected at the OS boundary. $ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +Import-Module (Join-Path $PSScriptRoot '../modules/AuditCatalog.psm1') -Force Import-Module (Join-Path $PSScriptRoot '../modules/NativeProviders.psm1') -Force $module = Get-Module NativeProviders & $module { diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index a654f834..ddf721cf 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,8 @@ **改善:** +- 従来のToken Right Adjusted EventsのGUIDを修正し、すべてのベースラインでRPCとトークン監査の状態を個別に評価するようにしました。実行時のカタログID・明示的な名前別表記の検証と、ハッシュ付きの読み取り専用EventID対応表レビューを追加しました。曖昧な対応、カテゴリだけの対応、不明な対応から検知可能とは判断しません。 (#380) (@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 630d34bd..fd681d9f 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,8 @@ **Improvements:** +- Corrected the legacy Token Right Adjusted Events GUID so RPC and token auditing are assessed independently in every baseline. Added runtime catalog identity/alias checks and a read-only, fingerprinted EventID mapping review that preserves ambiguous, category-only and unknown candidates without detection credit. (#380) (@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) From 45b6be91a8a35e1f08f6138fff70e0c6fafc58d1 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 07:07:43 +0900 Subject: [PATCH 2/5] Link audit catalog changelog entries to PR 403 --- CHANGELOG-Japanese.md | 2 +- CHANGELOG.md | 2 +- website/docs/resources/changelog.ja.md | 2 +- website/docs/resources/changelog.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 51caeda7..8b5f63eb 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,7 +4,7 @@ **改善:** -- 従来のToken Right Adjusted EventsのGUIDを修正し、すべてのベースラインでRPCとトークン監査の状態を個別に評価するようにしました。実行時のカタログID・明示的な名前別表記の検証と、ハッシュ付きの読み取り専用EventID対応表レビューを追加しました。曖昧な対応、カテゴリだけの対応、不明な対応から検知可能とは判断しません。 (#380) (@Shirofune-Security) +- 従来のToken Right Adjusted EventsのGUIDを修正し、すべてのベースラインでRPCとトークン監査の状態を個別に評価するようにしました。実行時のカタログID・明示的な名前別表記の検証と、ハッシュ付きの読み取り専用EventID対応表レビューを追加しました。曖昧な対応、カテゴリだけの対応、不明な対応から検知可能とは判断しません。 (#403) (@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 2a9151cc..14b32425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Improvements:** -- Corrected the legacy Token Right Adjusted Events GUID so RPC and token auditing are assessed independently in every baseline. Added runtime catalog identity/alias checks and a read-only, fingerprinted EventID mapping review that preserves ambiguous, category-only and unknown candidates without detection credit. (#380) (@Shirofune-Security) +- Corrected the legacy Token Right Adjusted Events GUID so RPC and token auditing are assessed independently in every baseline. Added runtime catalog identity/alias checks and a read-only, fingerprinted EventID mapping review that preserves ambiguous, category-only and unknown candidates without detection credit. (#403) (@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/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index ddf721cf..716859a1 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,7 +7,7 @@ **改善:** -- 従来のToken Right Adjusted EventsのGUIDを修正し、すべてのベースラインでRPCとトークン監査の状態を個別に評価するようにしました。実行時のカタログID・明示的な名前別表記の検証と、ハッシュ付きの読み取り専用EventID対応表レビューを追加しました。曖昧な対応、カテゴリだけの対応、不明な対応から検知可能とは判断しません。 (#380) (@Shirofune-Security) +- 従来のToken Right Adjusted EventsのGUIDを修正し、すべてのベースラインでRPCとトークン監査の状態を個別に評価するようにしました。実行時のカタログID・明示的な名前別表記の検証と、ハッシュ付きの読み取り専用EventID対応表レビューを追加しました。曖昧な対応、カテゴリだけの対応、不明な対応から検知可能とは判断しません。 (#403) (@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 fd681d9f..6af23985 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,7 +7,7 @@ **Improvements:** -- Corrected the legacy Token Right Adjusted Events GUID so RPC and token auditing are assessed independently in every baseline. Added runtime catalog identity/alias checks and a read-only, fingerprinted EventID mapping review that preserves ambiguous, category-only and unknown candidates without detection credit. (#380) (@Shirofune-Security) +- Corrected the legacy Token Right Adjusted Events GUID so RPC and token auditing are assessed independently in every baseline. Added runtime catalog identity/alias checks and a read-only, fingerprinted EventID mapping review that preserves ambiguous, category-only and unknown candidates without detection credit. (#403) (@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) From 9e2b4b5b43ea4f0d5abeebd7482955cc721c21e4 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 07:09:07 +0900 Subject: [PATCH 3/5] Make LDAP 1644 diagnostics explicit and preserve existing DC settings --- .github/workflows/ldap-diagnostics.yml | 25 ++++ CHANGELOG-Japanese.md | 2 + CHANGELOG.md | 2 + WELA.ps1 | 33 +++++- docs/ldap-diagnostics.md | 35 ++++++ scripts/LdapDiagnostics.ps1 | 147 ++++++++++++++++++++++++ tests/LdapDiagnostics.Tests.ps1 | 124 ++++++++++++++++++++ tests/LdapDiagnostics.Windows.Tests.ps1 | 8 ++ website/docs/resources/changelog.ja.md | 2 + website/docs/resources/changelog.md | 2 + 10 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ldap-diagnostics.yml create mode 100644 docs/ldap-diagnostics.md create mode 100644 scripts/LdapDiagnostics.ps1 create mode 100644 tests/LdapDiagnostics.Tests.ps1 create mode 100644 tests/LdapDiagnostics.Windows.Tests.ps1 diff --git a/.github/workflows/ldap-diagnostics.yml b/.github/workflows/ldap-diagnostics.yml new file mode 100644 index 00000000..22dac232 --- /dev/null +++ b/.github/workflows/ldap-diagnostics.yml @@ -0,0 +1,25 @@ +name: LDAP diagnostic regressions +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + ldap-diagnostics: + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: LDAP fixtures on Windows PowerShell 5.1 + shell: powershell + run: ./tests/LdapDiagnostics.Tests.ps1 + - name: Native applicability on Windows PowerShell 5.1 + shell: powershell + run: ./tests/LdapDiagnostics.Windows.Tests.ps1 + - name: LDAP fixtures on PowerShell 7 + shell: pwsh + run: ./tests/LdapDiagnostics.Tests.ps1 + - name: Native applicability on PowerShell 7 + shell: pwsh + run: ./tests/LdapDiagnostics.Windows.Tests.ps1 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 9cb8750f..77a063c9 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,8 @@ **改善:** +- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。イベント生成・量・転送は隔離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 34855338..b62472cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **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) + - 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 30ace2ba..4b3c6683 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -37,6 +37,11 @@ [ValidateSet('List', 'Audit', 'Plan', 'Configure')][string]$WmiAction = 'List', [string[]]$WmiNamespace, [switch]$WmiIncludeChildren, + [ValidateSet('Audit','Plan','Configure')][string]$LdapAction = 'Audit', + [ValidateSet('Preserve','Diagnostic','MdiCleanup')][string]$LdapMode = 'Preserve', + [ValidateRange(1,2147483647)][int]$LdapSearchTimeMs, + [ValidateRange(1,2147483647)][int]$LdapExpensiveThreshold, + [ValidateRange(1,2147483647)][int]$LdapInefficientThreshold, [switch]$Help ) @@ -53,6 +58,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" . (Join-Path $ScriptRoot "scripts/Configuration.ps1") . (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1") . (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1") +. (Join-Path $ScriptRoot "scripts/LdapDiagnostics.ps1") . (Join-Path $ScriptRoot "scripts/AdObjectSacl.ps1") . (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1") . (Join-Path $ScriptRoot "scripts/WmiNamespaceAuditing.ps1") @@ -1426,9 +1432,7 @@ function ConfigureAuditSettings { Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto -Context $context Set-WelaDomainNtlmAudit -Auto:$Auto -Context $context if ($hostContext.Role -eq 'DomainController') { - Set-RegistryConfig -RegPaths @( - @{Path = 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics'; Name = '15 Field Engineering'; Value = 5} - ) -Auto:$Auto -Context $context + Write-Host 'LDAP 1644 diagnostics are preserved. MDI no longer requires them; use ldap-diagnostics for explicit Diagnostic or MdiCleanup changes.' -ForegroundColor Yellow } # Both audit display and mutation use the versioned role-aware profile. @@ -1694,6 +1698,9 @@ function Get-WelaUserProfiles { $usage = @" Usage: + ./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 ./WELA.ps1 channel-settings -ChannelAction Audit -WefQuerySet Both -ResultsPath channels.json ./WELA.ps1 channel-settings -ChannelAction Plan -GrantEventLogReaders ./WELA.ps1 channel-settings -ChannelAction Configure -GrantEventLogReaders -DryRun @@ -1761,13 +1768,13 @@ 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 'applocker-readiness' -and $AppLockerAction -eq 'Import') -and $Cmd -notin @('configure', 'configure-eventlogs') -and +if ($DryRun -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 'firewall-logging' -and $FirewallAction -eq 'Configure') -and -not ($Cmd -eq 'smb-auditing' -and $SmbAction -eq 'Configure') -and -not ($Cmd -eq 'channel-settings' -and $ChannelAction -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, wmi-auditing -WmiAction Configure, channel-settings -ChannelAction Configure, applocker-readiness -AppLockerAction Import, and ad-object-sacl -AdSaclAction Configure|Rollback. No command was run." + throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs, firewall-logging -FirewallAction Configure, smb-auditing -SmbAction Configure, wmi-auditing -WmiAction Configure, channel-settings -ChannelAction Configure, applocker-readiness -AppLockerAction Import, ad-object-sacl -AdSaclAction Configure|Rollback, and ldap-diagnostics -LdapAction 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.' @@ -1792,7 +1799,23 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi 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 } + if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath) { throw 'ldap-diagnostics observes the actual local DC and uses -LdapMode/-ResultsPath; audit profiles, role overrides and HTML output do not apply.' } + if ($LdapAction -eq 'Configure' -and $LdapMode -ne 'Preserve' -and -not (TestAdministrator)) { throw 'LDAP configuration requires Administrator privileges.' } + $thresholds=@{} + if ($PSBoundParameters.ContainsKey('LdapSearchTimeMs')) { $thresholds.SearchTime=$LdapSearchTimeMs } + if ($PSBoundParameters.ContainsKey('LdapExpensiveThreshold')) { $thresholds.Expensive=$LdapExpensiveThreshold } + if ($PSBoundParameters.ContainsKey('LdapInefficientThreshold')) { $thresholds.Inefficient=$LdapInefficientThreshold } + $report=Invoke-WelaLdapCommand -Action $LdapAction -Mode $LdapMode -Thresholds $thresholds -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $report + if ($report.ExitCode) { exit $report.ExitCode } + } 'channel-settings' { if ($Help) { Write-Host 'Usage: ./WELA.ps1 channel-settings [-ChannelAction Audit|Plan|Configure] [-ChannelProfile microsoft-wef-appendix-c] [-WefQuerySet Baseline|Suspect|Both] [-GrantEventLogReaders] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath file.json]' diff --git a/docs/ldap-diagnostics.md b/docs/ldap-diagnostics.md new file mode 100644 index 00000000..623f25ea --- /dev/null +++ b/docs/ldap-diagnostics.md @@ -0,0 +1,35 @@ +# Explicit LDAP 1644 diagnostics + +Normal `configure` preserves existing LDAP diagnostics and explains that 1644 is not selected. It no longer sets `15 Field Engineering=5` automatically on domain controllers. Microsoft's [current MDI guidance](https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-windows-event-collection#update-legacy-configurations) no longer requires these diagnostic settings. Security auditing and the other MDI prerequisites remain separate. + +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. + +```powershell +./WELA.ps1 ldap-diagnostics -LdapAction Audit -ResultsPath ldap-before.json +./WELA.ps1 ldap-diagnostics -LdapAction Plan -LdapMode Diagnostic -LdapSearchTimeMs 100 +./WELA.ps1 ldap-diagnostics -LdapAction Configure -LdapMode Diagnostic -LdapSearchTimeMs 100 -DryRun +./WELA.ps1 ldap-diagnostics -LdapAction Configure -LdapMode Diagnostic -LdapSearchTimeMs 100 -Auto -BackupPath C:\WelaRecovery\ldap-01 -ResultsPath ldap-result.json +``` + +`Preserve` is the default mode, including when Configure is selected. It changes no settings. `Diagnostic` explicitly requests Field Engineering level 5 and only the thresholds supplied by the operator. Omitted thresholds retain their existing typed values; they are not silently lowered to 1. Thresholds must be positive integers through 2147483647. Windows treats an absent/zero time threshold as its documented default; use cleanup to remove an override instead of supplying zero. + +| Option | NTDS Parameters value | Unit | Microsoft documented default, not a host observation | +|---|---|---|---:| +| `-LdapSearchTimeMs` | Search Time Threshold (msecs) | milliseconds | 30000 | +| `-LdapExpensiveThreshold` | Expensive Search Results Threshold | entry threshold | 10000 | +| `-LdapInefficientThreshold` | Inefficient Search Results Threshold | entry threshold | 1000 | + +These values are described in Microsoft's [1644 diagnostics procedure](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/event1644reader-analyze-ldap-query-performance). The example's 100 ms is a starting point to evaluate, not a baseline requirement. Level 5 also generates other Directory Service events. Measure event rate, log rollover and DC workload over a bounded interval before wider rollout. WELA neither issues LDAP searches nor measures that volume automatically. + +`MdiCleanup` is an explicit removal choice. It removes **all four named values** shown in the plan, including customized values: Field Engineering first, followed by the three thresholds. It leaves their registry keys and unrelated diagnostic values intact. Select it only after reviewing existing operator diagnostics; normal configure and Preserve never perform cleanup. + +```powershell +./WELA.ps1 ldap-diagnostics -LdapAction Plan -LdapMode MdiCleanup +./WELA.ps1 ldap-diagnostics -LdapAction Configure -LdapMode MdiCleanup -DryRun +``` + +Writes require a fresh complete snapshot and a saved `before.jsonl` recovery record containing original existence, type and value for every control. Unknown types and concurrent changes block writes. Each selected threshold is read back before verbose logging is enabled; errors stop the remaining changes and produce a nonzero result. Repeated configuration is idempotent, and the final check detects drift in selected and preserved values. Dry-run does not create a backup directory or modify Windows. A partial failure retains its journal; it does not trigger automatic restoration over newer changes. + +For recovery, compare fresh values with both the journal's original state and this run's desired values. Restore original values/types only where this run's changes still remain; remove only a value that was originally absent. Preserve any newer operator changes and keep the recovery journal. MDI cleanup is not a general rollback command. + +Tests cover default preservation, explicit setup/cleanup, positive bounds, type/read/write errors, journal ordering, stale plans, races, partial failures, repeated application and final drift. Windows CI queries native role applicability without configuring a DC. Before closing #383, use an isolated DC snapshot to verify a benign query against the selected thresholds, retain matching 1644 XML and before/after policy, measure volume, and verify forwarding. Registry readback is not event-generation or Sigma detection evidence. Sysmon is out of scope. diff --git a/scripts/LdapDiagnostics.ps1 b/scripts/LdapDiagnostics.ps1 new file mode 100644 index 00000000..3ba08ba2 --- /dev/null +++ b/scripts/LdapDiagnostics.ps1 @@ -0,0 +1,147 @@ +# Opt-in local DC diagnostics; separate from MDI-required Security auditing. +function Get-WelaLdapDefinitions { + $root = 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS' + @( + [pscustomobject]@{Id='FieldEngineering';Path="$root\Diagnostics";Name='15 Field Engineering';Unit='verbosity';DocumentedDefault=0} + [pscustomobject]@{Id='SearchTime';Path="$root\Parameters";Name='Search Time Threshold (msecs)';Unit='milliseconds';DocumentedDefault=30000} + [pscustomobject]@{Id='Expensive';Path="$root\Parameters";Name='Expensive Search Results Threshold';Unit='entry threshold';DocumentedDefault=10000} + [pscustomobject]@{Id='Inefficient';Path="$root\Parameters";Name='Inefficient Search Results Threshold';Unit='entry threshold';DocumentedDefault=1000} + ) +} + +function Get-WelaLdapHost { + if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { return [pscustomobject]@{Status='NotApplicable';Diagnostic='Windows domain controller required.'} } + $os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop + $system = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop + if ($os.ProductType -notin @(1,2,3) -or $system.DomainRole -notin @(0,1,2,3,4,5)) { throw 'Cannot classify the Windows role.' } + if ($os.ProductType -ne 2 -and $system.DomainRole -notin @(4,5)) { return [pscustomobject]@{Status='NotApplicable';Diagnostic='Client/member/CA without DC role: no NTDS diagnostics changes.'} } + if ($os.ProductType -ne 2 -or $system.DomainRole -notin @(4,5)) { throw 'Conflicting domain-controller role observations.' } + if ([int]$os.BuildNumber -notin @(20348,26100)) { return [pscustomobject]@{Status='Unknown';Diagnostic='This workflow supports Server 2022/2025 DC builds 20348/26100.'} } + if (-not [Environment]::Is64BitProcess) { throw 'Run the LDAP workflow in 64-bit PowerShell.' } + [pscustomobject]@{Status='Applicable';ComputerName=$env:COMPUTERNAME;Build=[int]$os.BuildNumber;Diagnostic='Local domain controller; event generation remains unverified.'} +} + +function Get-WelaLdapSnapshot { + $hostState = Get-WelaLdapHost + $values = @() + if ($hostState.Status -eq 'Applicable') { + foreach ($definition in Get-WelaLdapDefinitions) { + $values += [pscustomobject]@{Definition=$definition;State=(Get-WelaRegistryState -Path $definition.Path -Name $definition.Name)} + } + } + [pscustomobject]@{Host=$hostState;Values=$values} +} + +function Get-WelaLdapPlan { + param($Snapshot, [ValidateSet('Preserve','Diagnostic','MdiCleanup')][string]$Mode='Preserve', [hashtable]$Thresholds=@{}) + if ($Mode -ne 'Diagnostic' -and $Thresholds.Count) { throw 'Thresholds require Diagnostic mode.' } + foreach ($id in $Thresholds.Keys) { + if ($id -notin @('SearchTime','Expensive','Inefficient') -or $Thresholds[$id] -is [bool] -or + $Thresholds[$id] -isnot [ValueType] -or [double]$Thresholds[$id] -lt 1 -or [double]$Thresholds[$id] -gt 2147483647 -or + [double]$Thresholds[$id] -ne [int]$Thresholds[$id]) { throw "Invalid positive DWORD threshold: $id" } + } + $rows = foreach ($entry in $Snapshot.Values) { + $desired = $null; $operation = 'Preserve' + if ($Mode -eq 'MdiCleanup') { $operation='Remove' } + elseif ($Mode -eq 'Diagnostic') { + if ($entry.Definition.Id -eq 'FieldEngineering') { $operation='Set'; $desired=5 } + elseif ($Thresholds.ContainsKey($entry.Definition.Id)) { $operation='Set'; $desired=[int]$Thresholds[$entry.Definition.Id] } + } + [pscustomobject]@{Definition=$entry.Definition;Before=$entry.State;Operation=$operation;Desired=$desired} + } + [pscustomobject]@{ + Mode=$Mode;Host=$Snapshot.Host;Controls=@($rows) + Guidance='MDI no longer requires 1644. Diagnostic mode is an explicit troubleshooting/detection choice; MdiCleanup explicitly removes the four listed legacy values. Preserve makes no changes.' + Volume='Field Engineering level 5 can also generate other Directory Service events. Measure volume in a bounded window before wider rollout.' + VerificationScope='Registry policy only; 1644 generation, thresholds in practice, volume and forwarding are not established.' + } +} + +function Test-WelaLdapDesired { + param($Snapshot,$Plan) + if ($Snapshot.Host.Status -ne 'Applicable') { return $false } + foreach ($row in $Plan.Controls) { + $actual = @($Snapshot.Values | Where-Object { $_.Definition.Id -eq $row.Definition.Id }) + if ($actual.Count -ne 1) { return $false } + $state=$actual[0].State + if ($row.Operation -eq 'Set' -and (-not $state.ValueExists -or $state.Type -ne 'DWord' -or $state.Value -ne $row.Desired)) { return $false } + if ($row.Operation -eq 'Remove' -and $state.ValueExists) { return $false } + if ($row.Operation -eq 'Preserve' -and -not (Test-WelaLdapValueEqual $state $row.Before)) { return $false } + } + return $true +} + +function Test-WelaLdapValueEqual { + param($Left,$Right) + # Creating an absent parent for another selected value must not appear as value drift. + return $Left.ValueExists -eq $Right.ValueExists -and $Left.Type -ceq $Right.Type -and + (ConvertTo-Json $Left.Value -Compress) -ceq (ConvertTo-Json $Right.Value -Compress) +} + +function Set-WelaLdapDiagnostics { + param($Context,$Plan) + $state = @{Plan=$Plan;Expected=$null} + $read = { + param($state) + $current = Get-WelaLdapSnapshot + if ($current.Host.Status -ne 'Applicable') { throw "LDAP diagnostics unavailable: $($current.Host.Diagnostic)" } + $current + } + $test = { + param($snapshot,$state) + if (-not $state.Expected) { $state.Expected=$snapshot } + Test-WelaLdapDesired $snapshot $state.Plan + } + $apply = { + param($state) + $fresh=Get-WelaLdapSnapshot + if ((ConvertTo-Json $fresh -Depth 12 -Compress) -cne (ConvertTo-Json $state.Expected -Depth 12 -Compress)) { throw 'LDAP state changed after the pre-change journal; no write performed.' } + # Plan is independently checked against the current pre-change state too. + foreach ($row in $state.Plan.Controls) { + $before=@($fresh.Values | Where-Object { $_.Definition.Id -eq $row.Definition.Id })[0].State + if (-not (Test-WelaLdapValueEqual $before $row.Before)) { throw 'LDAP plan is stale; review a fresh plan.' } + if ($before.ValueExists -and $before.Type -ne 'DWord') { throw "Unknown registry type for $($row.Definition.Name); preserved without mutation." } + } + # Configure thresholds before enabling verbose logging. Cleanup disables it first. + $ordered = if ($state.Plan.Mode -eq 'Diagnostic') { @($state.Plan.Controls | Sort-Object { $_.Definition.Id -eq 'FieldEngineering' }) } else { @($state.Plan.Controls) } + foreach ($row in $ordered) { + if ($row.Operation -eq 'Preserve') { continue } + $before=Get-WelaRegistryState -Path $row.Definition.Path -Name $row.Definition.Name + if (-not (Test-WelaLdapValueEqual $before $row.Before)) { throw "LDAP value changed before writing $($row.Definition.Name); remaining changes stopped." } + if ($row.Operation -eq 'Remove') { + if ($before.ValueExists) { Remove-ItemProperty -LiteralPath $row.Definition.Path -Name $row.Definition.Name -ErrorAction Stop } + } elseif (-not $before.ValueExists -or $before.Type -ne 'DWord' -or $before.Value -ne $row.Desired) { + New-WelaRegistryKey -Path $row.Definition.Path + Set-ItemProperty -LiteralPath $row.Definition.Path -Name $row.Definition.Name -Value $row.Desired -Type DWord -ErrorAction Stop + } + $after=Get-WelaRegistryState -Path $row.Definition.Path -Name $row.Definition.Name + if (($row.Operation -eq 'Remove' -and $after.ValueExists) -or + ($row.Operation -eq 'Set' -and (-not $after.ValueExists -or $after.Type -ne 'DWord' -or $after.Value -ne $row.Desired))) { + throw "LDAP value readback failed for $($row.Definition.Name); remaining changes stopped." + } + } + } + Invoke-WelaConfigurationControl -Context $Context -Id 'LdapDiagnostics/LocalDC' -Kind RegistrySet -Target 'NTDS diagnostics/parameters (four named values only)' -Desired $Plan ` + -Read $read -Compliant $test -Apply $apply -CallbackState $state -Description 'Apply the explicitly selected LDAP diagnostic mode and listed values.' +} + +function Invoke-WelaLdapCommand { + param([ValidateSet('Audit','Plan','Configure')][string]$Action='Audit', + [ValidateSet('Preserve','Diagnostic','MdiCleanup')][string]$Mode='Preserve', [hashtable]$Thresholds=@{}, + [switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath) + $snapshot=Get-WelaLdapSnapshot + $plan=Get-WelaLdapPlan -Snapshot $snapshot -Mode $Mode -Thresholds $Thresholds + $report=[pscustomobject]@{ExitCode=$(if ($snapshot.Host.Status -eq 'Unknown') {1} else {0});Scope='ldap-1644-diagnostics';Plan=$plan;Snapshot=$snapshot} + if ($Action -eq 'Configure' -and $Mode -ne 'Preserve') { + if ($snapshot.Host.Status -ne 'Applicable') { throw "LDAP configuration blocked: $($snapshot.Host.Diagnostic)" } + $context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath + Set-WelaLdapDiagnostics -Context $context -Plan $plan + $report=Complete-WelaConfiguration -Context $context -SuccessMessage 'Selected LDAP registry changes verified; event generation and forwarding remain unverified.' + $report.Scope='ldap-1644-diagnostics' + $report | Add-Member NoteProperty Plan $plan + } + if ($ResultsPath) { $report | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } + Write-Host $plan.Guidance + Write-Host $plan.Volume + $report +} diff --git a/tests/LdapDiagnostics.Tests.ps1 b/tests/LdapDiagnostics.Tests.ps1 new file mode 100644 index 00000000..4bb5b975 --- /dev/null +++ b/tests/LdapDiagnostics.Tests.ps1 @@ -0,0 +1,124 @@ +$ErrorActionPreference='Stop' +$script:ScriptRoot=Split-Path $PSScriptRoot -Parent +. (Join-Path $script:ScriptRoot 'scripts/Configuration.ps1') +. (Join-Path $script:ScriptRoot 'scripts/LdapDiagnostics.ps1') +$script:count=0 +function Assert($Condition,$Message) { if (-not $Condition) { throw $Message }; $script:count++ } +function Reject([scriptblock]$Action,[string]$Pattern) { + $message=''; try { & $Action | Out-Null } catch { $message=$_.Exception.Message } + Assert ($message -match $Pattern) "Expected '$Pattern', got '$message'." +} +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 +} +function Get-WelaLdapHost { [pscustomobject]@{Status=$script:role;Diagnostic='fixture';ComputerName='dc1';Build=26100} } +function Get-WelaRegistryState { + param($Path,$Name) + if ($script:readError) { throw 'read denied' } + $script:reads++ + if ($script:race -and $script:journal -and (Test-Path $script:journal)) { $script:values['15 Field Engineering']=4; $script:types['15 Field Engineering']='DWord' } + [pscustomobject]@{KeyExists=$true;ValueExists=$script:values.ContainsKey($Name);Value=$script:values[$Name];Type=$script:types[$Name]} +} +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' } + $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' } + $script:writes += $Name + if (-not $script:ignoreWrite) { $script:values.Remove($Name); $script:types.Remove($Name) } +} +$cleanup=@() +function New-FixtureContext([switch]$DryRun) { + $path=Join-Path ([IO.Path]::GetTempPath()) ('wela-ldap-'+[guid]::NewGuid().ToString('N')) + $script:cleanup += $path; $script:journal=Join-Path $path 'before.jsonl' + New-WelaConfigurationContext -Auto -DryRun:$DryRun -BackupPath $path +} +try { + Reset-Fixture + foreach ($mode in @('Preserve','Diagnostic','MdiCleanup')) { + $plan=Get-WelaLdapPlan (Get-WelaLdapSnapshot) $mode + Assert ($plan.Controls.Count -eq 4 -and $plan.VerificationScope -match 'not established') 'Every plan enumerates the complete bounded set and keeps event evidence separate.' + } + foreach ($value in @(-1,0,1.5,2147483648,$true)) { + Reject { Get-WelaLdapPlan (Get-WelaLdapSnapshot) Diagnostic @{SearchTime=$value} } 'Invalid' + } + Reject { Get-WelaLdapPlan (Get-WelaLdapSnapshot) MdiCleanup @{SearchTime=1} } 'require Diagnostic' + Reject { Get-WelaLdapPlan (Get-WelaLdapSnapshot) Diagnostic @{Typo=1} } 'Invalid' + $script:values['15 Field Engineering']=4; $script:types['15 Field Engineering']='DWord' + $preserved=Invoke-WelaLdapCommand -Action Configure + Assert ($script:writes.Count -eq 0 -and $preserved.Plan.Mode -eq 'Preserve' -and $script:values['15 Field Engineering'] -eq 4) 'Default command preserves another operator diagnostic level.' + + foreach ($scenario in @('apply','dry','cleanup','read','write','ignored','race','type','drift','preserved-drift','stale')) { + Reset-Fixture + $mode='Diagnostic'; $thresholds=@{SearchTime=100;Expensive=10000;Inefficient=1000} + if ($scenario -eq 'cleanup') { + $mode='MdiCleanup'; $thresholds=@{} + foreach ($definition in Get-WelaLdapDefinitions) { $script:values[$definition.Name]=1; $script:types[$definition.Name]='DWord' } + $script:values['15 Field Engineering']=5 + } + if ($scenario -eq 'preserved-drift') { $thresholds=@{} } + if ($scenario -eq 'type') { $script:values['15 Field Engineering']='5'; $script:types['15 Field Engineering']='String' } + $plan=Get-WelaLdapPlan (Get-WelaLdapSnapshot) $mode $thresholds + if ($scenario -eq 'read') { $script:readError=$true } + if ($scenario -eq 'write') { $script:writeError=$true } + if ($scenario -eq 'ignored') { $script:ignoreWrite=$true } + if ($scenario -eq 'race') { $script:race=$true } + if ($scenario -eq 'stale') { $script:values['15 Field Engineering']=2; $script:types['15 Field Engineering']='DWord' } + $context=New-FixtureContext -DryRun:($scenario -eq 'dry') + Set-WelaLdapDiagnostics $context $plan + if ($scenario -eq 'drift') { $script:values['15 Field Engineering']=0 } + if ($scenario -eq 'preserved-drift') { $script:values['Expensive Search Results Threshold']=5; $script:types['Expensive Search Results Threshold']='DWord' } + $result=Complete-WelaConfiguration $context + switch ($scenario) { + 'apply' { + Assert ($result.ExitCode -eq 0 -and $script:writes.Count -eq 4 -and $script:writes[-1] -eq '15 Field Engineering') 'Verify thresholds before enabling verbosity.' + $before=Get-Content $script:journal -Raw | ConvertFrom-Json + Assert ($before.Before.Values.Count -eq 4 -and @($before.Before.Values | Where-Object {$_.State.ValueExists}).Count -eq 0) 'Journal contains all exact missing-value states.' + $again=Get-WelaLdapPlan (Get-WelaLdapSnapshot) Diagnostic $thresholds + Set-WelaLdapDiagnostics $context $again + Assert ($script:writes.Count -eq 4 -and $context.Results[1].Status -eq 'AlreadyCompliant') 'Repeat configuration performs no duplicate writes.' + } + 'dry' { Assert ($result.ExitCode -eq 0 -and $script:writes.Count -eq 0 -and -not (Test-Path $context.BackupPath)) 'Dry-run makes no settings/journal changes.' } + 'cleanup' { Assert ($result.ExitCode -eq 0 -and $script:values.Count -eq 0 -and $script:writes[0] -eq '15 Field Engineering') 'Explicit MDI cleanup removes only named values, verbosity first.' } + 'ignored' { Assert ($result.ExitCode -eq 1 -and $script:writes.Count -eq 1 -and $script:writes -notcontains '15 Field Engineering') 'Failed threshold readback cannot proceed to enabling diagnostics.' } + 'drift' { Assert ($result.ExitCode -eq 1 -and $context.Results[0].Status -eq 'Overridden') 'Final diagnostic drift fails.' } + 'preserved-drift' { Assert ($result.ExitCode -eq 1) 'Unselected thresholds must remain preserved.' } + default { Assert ($result.ExitCode -eq 1 -and $script:writes.Count -eq 0) "Scenario $scenario must block before native writes." } + } + } + 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.' + Reject { Invoke-WelaLdapCommand -Action Configure -Mode Diagnostic } 'blocked' + Reset-Fixture; $script:role='Unknown' + Assert ((Invoke-WelaLdapCommand).ExitCode -eq 1) 'Unknown host applicability cannot look successful.' + + # 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) + $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} + function Get-WelaHostContext {[pscustomobject]@{Role='DomainController';Build=26100}} + function Get-WelaEffectiveAuditPolicy {@{}} + function Get-WelaAuditProfilePlan {[pscustomobject]@{profile='fixture'}} + function Assert-WelaAuditProfileTarget {} + function Set-WelaEventLogProfileControls {} + function Set-WelaEventLogControl {} + function Set-WelaRegistryControl {param($Context,$Path,$Name) if ($Path -like '*NTDS*') {throw 'General configure attempted NTDS diagnostics'} } + function Set-RegistryConfig {param($RegPaths) if (@($RegPaths | Where-Object {$_.Path -like '*NTDS*'}).Count) {throw 'General configure attempted NTDS diagnostics'} } + function Set-WelaOutgoingNtlmPolicy {};function Set-WelaDomainNtlmAudit {};function Show-WelaAuditProfilePrerequisites {};function Set-WelaProfileAuditControls {};function Set-WelaCertificateAuditControl {} + function Complete-WelaConfiguration {[pscustomobject]@{ExitCode=0}} + $script:PowerShellPolicyRoots=@(); $script:IncludeOptional=$false + $general=ConfigureAuditSettings -Auto -DryRun + Assert ($general.ExitCode -eq 0) 'Normal DC configure must not re-enable or disable LDAP diagnostics.' + Write-Host "PASS: $script:count LDAP diagnostics assertions; no Windows changes." +} finally { foreach ($path in $cleanup) { if (Test-Path $path) { Remove-Item $path -Recurse -Force } } } diff --git a/tests/LdapDiagnostics.Windows.Tests.ps1 b/tests/LdapDiagnostics.Windows.Tests.ps1 new file mode 100644 index 00000000..d2def0bc --- /dev/null +++ b/tests/LdapDiagnostics.Windows.Tests.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference='Stop' +$root=Split-Path $PSScriptRoot -Parent +. (Join-Path $root 'scripts/Configuration.ps1') +. (Join-Path $root 'scripts/LdapDiagnostics.ps1') +$result=Invoke-WelaLdapCommand -Action Audit +if ($result.Snapshot.Host.Status -notin @('Applicable','NotApplicable')) { throw 'Unexpected unknown native host applicability.' } +if ($result.Snapshot.Host.Status -eq 'NotApplicable' -and $result.Snapshot.Values.Count) { throw 'Non-DC incorrectly queried NTDS values.' } +Write-Host "PASS: native read-only LDAP applicability $($result.Snapshot.Host.Status); no registry writes or LDAP queries." diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index a654f834..faae013c 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,8 @@ **改善:** +- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。イベント生成・量・転送は隔離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 630d34bd..533d5a64 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,8 @@ **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) + - 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) 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 4/5] 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) From bca56fbad93d53821ed9cf513dd7beab9448ee52 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 07:18:27 +0900 Subject: [PATCH 5/5] Link LDAP diagnostics changelog to PR 404 --- CHANGELOG-Japanese.md | 2 +- CHANGELOG.md | 2 +- website/docs/resources/changelog.ja.md | 2 +- website/docs/resources/changelog.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 962c7ead..f98a4e1c 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,7 +4,7 @@ **改善:** -- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。LDAP専用オプションを他のプロファイルコマンドへ指定すると、実行前に拒否します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#383) (@Shirofune-Security) +- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。LDAP専用オプションを他のプロファイルコマンドへ指定すると、実行前に拒否します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#404) (@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 563f3b18..0b43cb89 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. LDAP-only options are rejected before unrelated profile commands can run. 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. (#404) (@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/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 7edbbdb9..2b8c26ec 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の旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。LDAP専用オプションを他のプロファイルコマンドへ指定すると、実行前に拒否します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#383) (@Shirofune-Security) +- 通常の設定でDCのField Engineeringを自動的にレベル5へ変更せず、既存のLDAP 1644診断設定を保持するようにしました。明示的な`ldap-diagnostics`の監査・計画・設定を追加し、保持、しきい値を指定した診断、MDIの旧設定削除を選択できます。役割・ビルド確認、型付き復旧記録、競合検出、順序付きの読み戻しと最終状態確認に対応します。LDAP専用オプションを他のプロファイルコマンドへ指定すると、実行前に拒否します。イベント生成・量・転送は隔離DCでの検証が必要です。 (#404) (@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 5cdbc775..2a8e100f 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. LDAP-only options are rejected before unrelated profile commands can run. 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. (#404) (@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)