Merge commit '29f03e352823af81b35869659928a94e70df4cf9' into feat/373-registry-sacl-recovery

# Conflicts:
#	.gitattributes
#	.github/workflows/release.yml
#	CHANGELOG-Japanese.md
#	CHANGELOG.md
#	website/docs/resources/changelog.ja.md
#	website/docs/resources/changelog.md
This commit is contained in:
Shirofune-Security committed 2026-09-22 11:43:28 +09:00
commit 70a812556d
32 files changed
+1108 -13

No files matched your search

+1 -1
View File
@@ -108,7 +108,7 @@ function Invoke-WelaConfigurationControl {
function Complete-WelaConfiguration {
param($Context, [string]$ResultsPath, $Plan,
[ValidateSet("native-windows-configuration", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only", "smb-audit-policies-only", "native-channel-settings-only", "wmi-namespace-sacl-only", "ad-object-sacl-only", "windows-powershell-transcription-policy-only", "wef-source-configuration-only", "wec-collector-subscriptions-only", "audit-integrity-local-policy-only", "adcs-audit-settings-only", "disabled-unlinked-gpo-creation-only")]
[ValidateSet("native-windows-configuration", "outgoing-ntlm-audit-policy-only", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only", "smb-audit-policies-only", "native-channel-settings-only", "wmi-namespace-sacl-only", "ad-object-sacl-only", "windows-powershell-transcription-policy-only", "wef-source-configuration-only", "wec-collector-subscriptions-only", "audit-integrity-local-policy-only", "adcs-audit-settings-only", "disabled-unlinked-gpo-creation-only")]
[string]$Scope = "native-windows-configuration",
[string]$SuccessMessage = 'Configuration completed; all requested controls verified.')
if ($Context.PSObject.Properties['CustomProfileGuard']) {
+5 -2
View File
@@ -53,12 +53,15 @@ function Get-WelaProviderPackSchema {
if ([guid]$provider.Id -eq [guid]::Empty) { throw 'Provider GUID is unknown.' }
if (@($logs[0].ProviderNames) -notcontains $Pack.provider -or @($provider.LogLinks.LogName) -notcontains $Pack.channel) { throw 'Provider/channel links disagree.' }
$events = @()
# EventMetadata.Id is Int64; WinRM includes unrelated IDs above Int32.MaxValue.
# Compare before parsing selected templates, without narrowing the native ID.
$expectedIds = @($Pack.events | ForEach-Object { [long]$_.id })
foreach ($event in $provider.Events) {
if (@($Pack.events.id) -contains [int]$event.Id -and $event.LogLink.LogName -eq $Pack.channel) {
if ($expectedIds -contains [long]$event.Id -and $event.LogLink.LogName -eq $Pack.channel) {
$fields = @(Get-WelaProviderTemplateFields -Template $event.Template)
$sha = [Security.Cryptography.SHA256]::Create()
try { $templateHash = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes([string]$event.Template)))).Replace('-','').ToLowerInvariant() } finally { $sha.Dispose() }
$events += [pscustomobject]@{ Id=[int]$event.Id; Version=[int]$event.Version; Channel=[string]$event.LogLink.LogName; Fields=$fields; TemplateSha256=$templateHash }
$events += [pscustomobject]@{ Id=[long]$event.Id; Version=[int]$event.Version; Channel=[string]$event.LogLink.LogName; Fields=$fields; TemplateSha256=$templateHash }
}
}
[pscustomobject]@{ State='Observed'; Provider=[string]$provider.Name; ProviderGuid=[string]$provider.Id; ChannelType=[string]$logs[0].LogType; Events=$events; Diagnostic=$null }
+77
View File
@@ -0,0 +1,77 @@
# Explicit outgoing audit policy; does not invoke the broad configure workflow.
function Get-WelaOutgoingAuditSnapshot {
if ($env:OS -ne 'Windows_NT' -or -not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell on Windows.' }
$os=Get-CimInstance Win32_OperatingSystem -Property BuildNumber,ProductType -ErrorAction Stop
$computer=Get-CimInstance Win32_ComputerSystem -Property DomainRole,PartOfDomain -ErrorAction Stop
$build=[int]$os.BuildNumber;$product=[int]$os.ProductType
if (-not (($product -eq 1 -and $build -in @(22000,22621,22631,26100,26200)) -or ($product -in @(2,3) -and $build -in @(20348,26100)))) { throw 'This Windows role/build has not been reviewed for the scoped command.' }
if ($computer.DomainRole -notin @(0,1,2,3,4,5) -or $computer.PartOfDomain -isnot [bool]) {throw 'Computer role/join context is unavailable.'}
$policy=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' RestrictSendingNTLMTraffic
if (-not $policy.KeyExists) {throw 'The existing MSV1_0 policy key is required; no parent key will be created.'}
[pscustomobject][ordered]@{Host=[pscustomobject][ordered]@{Build=$build;ProductType=$product;DomainRole=[int]$computer.DomainRole;PartOfDomain=$computer.PartOfDomain};Policy=$policy}
}
function Get-WelaOutgoingAuditDisposition {
param($Snapshot,[ValidateSet('PreserveOrAudit','Audit')][string]$Mode)
$p=$Snapshot.Policy
if ($p.ValueExists -and ($p.Type -cne 'DWord' -or $p.Value -notin @(0,1,2))) {return 'Unknown'}
if ($p.ValueExists -and $p.Value -eq 1) {return 'AlreadyCompliant'}
if ($p.ValueExists -and $p.Value -eq 2 -and $Mode -eq 'PreserveOrAudit') {return 'PreservedEnforcement'}
return 'ChangeRequired'
}
function Get-WelaOutgoingAuditPlan {
param([ValidateSet('PreserveOrAudit','Audit')][string]$Mode='PreserveOrAudit')
try {
$snapshot=Get-WelaOutgoingAuditSnapshot
$status=Get-WelaOutgoingAuditDisposition $snapshot $Mode
$diagnostic=switch($status){
Unknown {'Unknown registry type/value is preserved; investigate it before configuration.'}
PreservedEnforcement {'Deny all (2) is authentication enforcement, preserved by default. Explicit -OutgoingNtlmMode Audit authorizes replacing it with Audit all (1).'}
AlreadyCompliant {'Audit all (1) is configured; authentication, events and policy persistence are unverified.'}
default {'Set only outgoing NTLM Audit all (DWORD 1).'}
}
[pscustomobject]@{Status=$status;Mode=$Mode;Desired=1;Before=$snapshot;Diagnostic=$diagnostic;PolicySource=Get-WelaOutgoingNtlmPolicySource}
}catch{[pscustomobject]@{Status='Unknown';Mode=$Mode;Desired=1;Before=$null;Diagnostic=$_.ToString();PolicySource='Unknown'}}
}
function Invoke-WelaOutgoingAuditCommand {
param([ValidateSet('Audit','Plan','Configure')][string]$Action='Audit',[ValidateSet('PreserveOrAudit','Audit')][string]$Mode='PreserveOrAudit',[switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath)
if ($Action -ne 'Configure' -and ($Auto -or $DryRun -or $BackupPath)) {throw 'Consent, dry-run and backup options require Configure.'}
$plan=Get-WelaOutgoingAuditPlan $Mode
if ($Action -eq 'Configure') {
$context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
$path='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0';$name='RestrictSendingNTLMTraffic'
if ($plan.Status -eq 'Unknown') {
$context.Results.Add([pscustomobject]@{Id="Registry/$path/$name";Kind='Registry';Target=@{Path=$path;Name=$name};Desired=@{Value=1;Type='DWord'};Before=$plan.Before;After=$null;Status='Failed';Diagnostic=$plan.Diagnostic})
}else{
$state=@{Observed=$null;PlannedHost=($plan.Before.Host|ConvertTo-Json -Compress);Mode=$Mode;Path=$path;Name=$name}
$read={param($s)
$snapshot=Get-WelaOutgoingAuditSnapshot
if (($snapshot.Host|ConvertTo-Json -Compress) -cne $s.PlannedHost) {throw 'Observed host context changed; review a new plan.'}
if ((Get-WelaOutgoingAuditDisposition $snapshot $s.Mode) -eq 'Unknown') {throw 'Unknown registry type/value is preserved.'}
$s.Observed=$snapshot
return $snapshot
}
$test={param($snapshot) $snapshot.Policy.ValueExists -and $snapshot.Policy.Type -ceq 'DWord' -and $snapshot.Policy.Value -eq 1}
$preserve=if($Mode -eq 'PreserveOrAudit'){{param($snapshot) if($snapshot.Policy.ValueExists -and $snapshot.Policy.Type -ceq 'DWord' -and $snapshot.Policy.Value -eq 2){'Preserved Deny all enforcement; explicit Audit mode is required to replace it.'}}}else{$null}
$apply={param($s)
$fresh=Get-WelaOutgoingAuditSnapshot
if (($fresh|ConvertTo-Json -Depth 8 -Compress) -cne ($s.Observed|ConvertTo-Json -Depth 8 -Compress)) {throw 'Outgoing NTLM state changed after the original journal snapshot; no write was attempted.'}
if ((Get-WelaOutgoingAuditDisposition $fresh $s.Mode) -ne 'ChangeRequired') {throw 'The current state no longer authorizes this write.'}
Set-ItemProperty -LiteralPath $s.Path -Name $s.Name -Value 1 -Type DWord -ErrorAction Stop
'Only outgoing NTLM Audit all (1) was requested; no authentication or event-generation test was performed.'
}
Invoke-WelaConfigurationControl -Context $context -Id "Registry/$path/$name" -Kind Registry -Target @{Path=$path;Name=$name} -Desired @{Value=1;Type='DWord'} -Read $read -Compliant $test -PreserveWhen $preserve -Apply $apply -CallbackState $state -Description $plan.Diagnostic
}
$report=Complete-WelaConfiguration -Context $context -Scope 'outgoing-ntlm-audit-policy-only' -SuccessMessage 'Outgoing NTLM configuration results recorded; inspect preserved/skipped controls separately.'
$report|Add-Member NoteProperty Plan $plan
}else{$report=[pscustomobject]@{ExitCode=$(if($plan.Status -eq 'Unknown'){1}else{0});Scope='outgoing-ntlm-audit-policy-only';Action=$Action;Plan=$plan}}
$report|Add-Member NoteProperty EventGeneration 'Not verified; registry compliance does not establish authentication, NTLM events, forwarding, GPO persistence or Sigma readiness.'
$report|Add-Member NoteProperty ReadyRuleCredit 0
if ($ResultsPath) {
try {$report|ConvertTo-Json -Depth 16|Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop}
catch {$report.ExitCode=1;Write-Host "[Failed] Writing outgoing NTLM results: $_" -ForegroundColor Red}
}
return $report
}
+3 -2
View File
@@ -30,13 +30,14 @@ function Get-WelaWefControlState {
'SubscriptionManager' { return Get-WelaRegistryState -Path $Target.Path -Name $Target.Name }
'ForwardedEvents' { return Get-WelaNativeChannel -Name 'ForwardedEvents' }
'Subscription' {
$ids = @((Invoke-WelaNative -FilePath 'wecutil.exe' -Arguments @('es')).Output | ForEach-Object { $_.ToString().Trim() } | Where-Object { $_ })
$ids = @(Get-WelaWecSubscriptionIds)
if ($ids -notcontains $Target.Id) { return [pscustomobject]@{ Exists=$false; Xml=$null; Key=$null; Definition=$null } }
# Keep evidence as a plain string. Windows PowerShell 5.1's JSON
# serializer expands ETS properties on strings (for example a test
# reader's PSDrive/PSProvider graph), unlike modern PowerShell.
$xml = [string]::Concat((Invoke-WelaNative -FilePath 'wecutil.exe' -Arguments @('gs',$Target.Id,'/f:xml')).Diagnostic)
$xml = [string]::Concat((Read-WelaWecSubscriptionXml -Id $Target.Id))
$model = ConvertFrom-WelaWefSubscription -Xml $xml -SourceSids $Target.SourceSids -Observed
if($model.Id -cne $Target.Id){throw 'Native subscription identity differs from the selected ID.'}
return [pscustomobject]@{ Exists=$true; Xml=$xml; Key=$model.Key; Definition=$model.Definition }
}
default { throw "Unsupported WEF control kind: $Kind" }