mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-02 20:44:42 +02:00
148 lines
9.7 KiB
PowerShell
148 lines
9.7 KiB
PowerShell
# 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
|
|
}
|