Files
WELA/scripts/WefDeployment.ps1
T

352 lines
29 KiB
PowerShell

# Domain/Kerberos source configuration and create-only collector subscriptions.
# All writes run through Configuration.ps1; assessment never grants Sigma credit.
function Get-WelaWefHost {
$computer = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
[pscustomobject]@{ DomainJoined=($computer.PartOfDomain -eq $true); Fqdn=([string]$computer.DNSHostName + '.' + [string]$computer.Domain); DomainRole=[int]$computer.DomainRole }
}
function Get-WelaWefControlState {
param([string]$Kind, $Target)
switch ($Kind) {
'Service' {
$service = Get-CimInstance Win32_Service -Filter ("Name='{0}'" -f $Target.Name) -ErrorAction Stop
if (-not $service -or $service.StartMode -notin @('Auto','Manual','Disabled') -or $service.State -notin @('Running','Stopped')) { throw 'Service state is absent, pending or unsupported.' }
return [pscustomobject]@{ Name=$Target.Name; StartMode=[string]$service.StartMode; State=[string]$service.State }
}
'Wsman' {
# The WSMan provider can offer to start WinRM when accessed. Audit/Plan
# must remain read-only, so never enter it while the service is stopped.
if ((Get-WelaWefControlState Service @{ Name='WinRM' }).State -ne 'Running') { throw 'WinRM is stopped; WSMan settings were not queried to avoid implicit service startup.' }
$item = Get-Item -LiteralPath $Target.Path -ErrorAction Stop
if (-not $item.PSObject.Properties['Value'] -or -not $item.PSObject.Properties['SourceOfValue']) { throw 'WSMan value/provenance is unreadable.' }
return [pscustomobject]@{ Value=([string]$item.Value).ToLowerInvariant(); SourceOfValue=[string]$item.SourceOfValue }
}
'Readers' {
$group = Get-LocalGroup -SID 'S-1-5-32-573' -ErrorAction Stop
$members = @(Get-LocalGroupMember -Group $group -ErrorAction Stop)
if (@($members | Where-Object { -not $_.SID }).Count) { throw 'Unreadable group membership; no additive write can be verified.' }
return [pscustomobject]@{ GroupSid='S-1-5-32-573'; MemberSids=@($members | ForEach-Object { $_.SID.ToString() } | Sort-Object -Unique) }
}
'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 { $_ })
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)
$model = ConvertFrom-WelaWefSubscription -Xml $xml -SourceSids $Target.SourceSids -Observed
return [pscustomobject]@{ Exists=$true; Xml=$xml; Key=$model.Key; Definition=$model.Definition }
}
default { throw "Unsupported WEF control kind: $Kind" }
}
}
function Get-WelaWefStateKey {
param($Value)
ConvertTo-Json -InputObject $Value -Depth 25 -Compress
}
function Test-WelaWefControl {
param($Value, $Entry)
switch ($Entry.Kind) {
'Service' { return $Value.StartMode -eq 'Auto' -and $Value.State -eq 'Running' }
'Wsman' { return $Value.Value -ceq $Entry.Desired.Value }
'Readers' {
$expected=@(@($Entry.Before.MemberSids) + 'S-1-5-20' | Sort-Object -Unique)
return $Value.MemberSids -contains 'S-1-5-20' -and -not @(Compare-Object $expected @($Value.MemberSids)).Count
}
'SubscriptionManager' { return $Value.ValueExists -and $Value.Type -eq 'String' -and $Value.Value -ceq $Entry.Desired.Value }
'ForwardedEvents' {
return (Test-WelaNativeChannelSnapshot $Value) -and $Value.IsEnabled -and $Value.MaximumSizeInBytes -eq $Entry.Before.MaximumSizeInBytes -and
$Value.LogMode -eq $Entry.Before.LogMode -and $Value.SecurityDescriptor -ceq $Entry.Before.SecurityDescriptor
}
'Subscription' { return $Value.Exists -and $Value.Key -ceq $Entry.Desired.Key }
}
return $false
}
function New-WelaWefEntry {
param([string]$Kind, $Target, $Desired)
$before=$null; $diagnostic=''
try { $before = Get-WelaWefControlState -Kind $Kind -Target $Target } catch { $diagnostic=$_.ToString() }
$entry = [pscustomobject]@{ Kind=$Kind; Target=$Target; Desired=$Desired; Before=$before; Status='Unknown'; Diagnostic=$diagnostic }
if ($before) { $entry.Status = if (Test-WelaWefControl $before $entry) { 'RequestedSettingsMatch' } else { 'ChangeRequired' } }
if ($Kind -eq 'Wsman' -and $before -and $before.SourceOfValue -and $entry.Status -ne 'RequestedSettingsMatch') { $entry.Status='ManualReview'; $entry.Diagnostic='Policy-owned WSMan value is not overwritten.' }
if ($Kind -eq 'SubscriptionManager' -and $before.ValueExists -and $entry.Status -ne 'RequestedSettingsMatch') { $entry.Status='ManualReview'; $entry.Diagnostic='Selected slot already contains another value/type; select a free slot or manage its owning policy.' }
if ($Kind -eq 'Subscription' -and $before.Exists -and $entry.Status -ne 'RequestedSettingsMatch') { $entry.Status='ManualReview'; $entry.Diagnostic='An existing different subscription is never updated or replaced.' }
if ($Kind -eq 'Readers' -and $entry.Status -eq 'ChangeRequired') {
try {
if ((Get-WelaWefHost).DomainRole -in @(4,5)) { $entry.Status='ManualReview'; $entry.Diagnostic='Domain controller BUILTIN membership has AD/domain policy authority; WELA never changes it through this local workflow. Arrange and verify the forwarding identity membership separately.' }
} catch { $entry.Status='Unknown'; $entry.Diagnostic='Host role cannot be verified for a safe local group update. ' + $_.ToString() }
}
return $entry
}
function Get-WelaWefHardeningEntries {
param([string]$Role)
if ($Role -eq 'Source') {
New-WelaWefEntry Wsman @{ Path='WSMan:\localhost\Client\Auth\Digest' } @{ Value='false' }
} else {
New-WelaWefEntry Wsman @{ Path='WSMan:\localhost\Service\Auth\CbtHardeningLevel' } @{ Value='strict' }
New-WelaWefEntry Wsman @{ Path='WSMan:\localhost\Shell\AllowRemoteShellAccess' } @{ Value='false' }
}
}
function Test-WelaWefAdmx {
# Verify the OS's actual SubscriptionManager list mapping before registry writes.
$path = Join-Path $env:windir 'PolicyDefinitions/EventForwarding.admx'
$doc = Read-WelaWefXml (Get-Content -LiteralPath $path -Raw -ErrorAction Stop)
$policies = @($doc.SelectNodes("//*[local-name()='policy' and @name='SubscriptionManager']"))
if ($policies.Count -ne 1 -or $policies[0].GetAttribute('class') -cne 'Machine') { throw 'Local EventForwarding ADMX SubscriptionManager policy is unsupported.' }
$lists = @($policies[0].SelectNodes("./*[local-name()='elements']/*[local-name()='list']"))
if ($lists.Count -ne 1 -or $lists[0].GetAttribute('key') -ine 'Software\Policies\Microsoft\Windows\EventLog\EventForwarding\SubscriptionManager' -or
$lists[0].GetAttribute('valuePrefix') -ne '' -or $lists[0].GetAttribute('valueType') -notin @('','string')) { throw 'Local SubscriptionManager list mapping is unsupported; no guessed policy mapping is written.' }
return $true
}
function Get-WelaWefCollectorPrerequisites {
param($Config)
$checks = @()
try {
$hostState=Get-WelaWefHost
$ok=$hostState.DomainJoined -and $hostState.Fqdn -ieq $Config.CollectorFqdn -and $hostState.DomainRole -in @(2,3)
$checks += [pscustomobject]@{ Name='Domain member server identity'; Verified=[bool]$ok; Evidence=$hostState; Diagnostic='Dedicated collector workload isolation is an operator prerequisite, not detected from domain role.' }
} catch { $checks += [pscustomobject]@{ Name='Domain member server identity'; Verified=$false; Evidence=$null; Diagnostic=$_.ToString() } }
try {
if ((Get-WelaWefControlState Service @{ Name='WinRM' }).State -ne 'Running') { throw 'WinRM is stopped; listener enumeration was not attempted.' }
$listeners = @(Get-WSManInstance -ResourceURI 'winrm/config/Listener' -Enumerate -ErrorAction Stop)
$matched = @($listeners | Where-Object { $_.Address -ceq $Config.ListenerAddress -and $_.Transport -ieq 'HTTP' -and [string]$_.Port -eq '5985' -and [string]$_.Enabled -ieq 'true' -and $_.URLPrefix -ieq 'wsman' })
$checks += [pscustomobject]@{ Name='Existing matching HTTP listener'; Verified=($matched.Count -eq 1); Evidence=@($listeners | Select-Object Address,Transport,Port,Enabled,URLPrefix,ListeningOn); Diagnostic='Listener definition only; reachability is not tested.' }
} catch { $checks += [pscustomobject]@{ Name='Existing matching HTTP listener'; Verified=$false; Evidence=$null; Diagnostic=$_.ToString() } }
try {
$rules = @(Get-NetFirewallRule -Name $Config.IngressRuleName -PolicyStore ActiveStore -ErrorAction Stop)
if ($rules.Count -ne 1) { throw 'Expected exactly one existing effective firewall rule.' }
$rule=$rules[0]; $ports=@($rule | Get-NetFirewallPortFilter -ErrorAction Stop); $addresses=@($rule | Get-NetFirewallAddressFilter -ErrorAction Stop)
$scopeMatches=$addresses.Count -eq 1 -and
(@(Compare-Object @($Config.IngressLocalAddresses | Sort-Object -Unique) @($addresses[0].LocalAddress | Sort-Object -Unique)).Count -eq 0) -and
(@(Compare-Object @($Config.IngressRemoteAddresses | Sort-Object -Unique) @($addresses[0].RemoteAddress | Sort-Object -Unique)).Count -eq 0)
$ok=[string]$rule.Enabled -eq 'True' -and [string]$rule.Direction -eq 'Inbound' -and [string]$rule.Action -eq 'Allow' -and [string]$rule.Profile -eq 'Domain' -and
$ports.Count -eq 1 -and [string]$ports[0].Protocol -in @('TCP','6') -and [string]$ports[0].LocalPort -eq '5985' -and $scopeMatches
$checks += [pscustomobject]@{ Name='Existing scoped domain ingress rule'; Verified=[bool]$ok; Evidence=@{ Rule=($rule | Select-Object Name,Enabled,Direction,Action,Profile,PolicyStoreSourceType,EnforcementStatus); Ports=$ports | Select-Object Protocol,LocalPort,RemotePort; Addresses=$addresses | Select-Object LocalAddress,RemoteAddress }; Diagnostic='Exact selected rule definition only; other rules, network reachability and effective packet acceptance are not established.' }
} catch { $checks += [pscustomobject]@{ Name='Existing scoped domain ingress rule'; Verified=$false; Evidence=$null; Diagnostic=$_.ToString() } }
foreach ($name in @('WinRM','Wecsvc')) {
$entry=New-WelaWefEntry Service @{ Name=$name } @{ StartMode='Auto'; State='Running' }
$checks += [pscustomobject]@{ Name="$name service"; Verified=($entry.Status -eq 'RequestedSettingsMatch'); Evidence=$entry.Before; Diagnostic=$entry.Diagnostic }
}
foreach ($entry in @(Get-WelaWefHardeningEntries Collector)) { $checks += [pscustomobject]@{ Name=$entry.Target.Path; Verified=($entry.Status -eq 'RequestedSettingsMatch'); Evidence=$entry.Before; Diagnostic=$entry.Diagnostic } }
$entry=New-WelaWefEntry Wsman @{ Path='WSMan:\localhost\Service\Auth\Kerberos' } @{ Value='true' }
$checks += [pscustomobject]@{ Name='Collector Kerberos authentication'; Verified=($entry.Status -eq 'RequestedSettingsMatch'); Evidence=$entry.Before; Diagnostic=$entry.Diagnostic }
return $checks
}
function Get-WelaWefPrerequisites {
param($Model,[string]$Role)
if ($Role -eq 'Collector') { return @(Get-WelaWefCollectorPrerequisites $Model.Config) }
$checks=@()
try { $hostState=Get-WelaWefHost; $checks += [pscustomobject]@{ Name='Source domain membership'; Verified=[bool]$hostState.DomainJoined; Evidence=$hostState; Diagnostic='' } }
catch { $checks += [pscustomobject]@{ Name='Source domain membership'; Verified=$false; Evidence=$null; Diagnostic=$_.ToString() } }
try { $valid=Test-WelaWefAdmx; $checks += [pscustomobject]@{ Name='Local SubscriptionManager ADMX mapping'; Verified=[bool]$valid; Evidence=$null; Diagnostic='' } }
catch { $checks += [pscustomobject]@{ Name='Local SubscriptionManager ADMX mapping'; Verified=$false; Evidence=$null; Diagnostic=$_.ToString() } }
$entries=@(New-WelaWefEntry Service @{ Name='WinRM' } @{ StartMode='Auto'; State='Running' })
$entries+=@(Get-WelaWefHardeningEntries Source)
$entries+=New-WelaWefEntry Wsman @{ Path='WSMan:\localhost\Client\Auth\Kerberos' } @{ Value='true' }
$entries+=New-WelaWefEntry Readers @{ GroupSid='S-1-5-32-573' } @{ AddMemberSid='S-1-5-20' }
foreach ($entry in $entries) { $checks += [pscustomobject]@{ Name=($entry.Kind + ':' + ($entry.Target | ConvertTo-Json -Compress)); Verified=($entry.Status -eq 'RequestedSettingsMatch'); Evidence=$entry.Before; Diagnostic=$entry.Diagnostic } }
foreach ($channel in @($Model.Subscriptions | ForEach-Object { $_.Query.Channels } | Sort-Object -Unique)) {
$observed=Get-WelaNativeChannel -Name $channel
$checks += [pscustomobject]@{ Name=$channel; Verified=($observed.State -eq 'Enabled'); Evidence=$observed; Diagnostic='Channel enablement only; effective token access and event generation are not tested.' }
}
return $checks
}
function Set-WelaWefPrerequisiteCheck {
param($Context,$Model,[string]$Role)
$state=@{ Model=$Model; Role=$Role }
$read={ param($state) return ,@(Get-WelaWefPrerequisites $state.Model $state.Role) }
$test={ param($value,$state) return -not @($value | Where-Object { -not $_.Verified }).Count }
$apply={ param($state) throw 'Required WEF prerequisites remain unmet; configure them explicitly and retry. No automatic topology changes were attempted.' }
Invoke-WelaConfigurationControl -Context $Context -Id 'WEF/Prerequisites' -Kind WefPrerequisite -Target @{ Role=$Role } -Desired @{ AllVerified=$true } -Read $read -Compliant $test -Apply $apply -CallbackState $state
}
function Add-WelaWefFailure {
param($Context,[string]$Id,[string]$Diagnostic,$Evidence)
$Context.Results.Add([pscustomobject]@{ Id=$Id; Kind='WefPrerequisite'; Target=$null; Desired=$null; Before=$Evidence; After=$null; Status='Failed'; Diagnostic=$Diagnostic })
Write-Host "[Failed] $Id $Diagnostic" -ForegroundColor Red
}
function Set-WelaWefEntry {
param($Context, $Entry, $Config)
if ($Entry.Status -in @('Unknown','ManualReview')) { Add-WelaWefFailure $Context $Entry.Kind $Entry.Diagnostic $Entry.Before; return }
$state=@{ Entry=$Entry; Config=$Config; Initial=$true; Snapshot=$null; BackupPath=$Context.BackupPath }
$read={
param($state)
$entry=$state.Entry; $current=Get-WelaWefControlState $entry.Kind $entry.Target
if ($state.Initial) {
if ((Get-WelaWefStateKey $current) -cne (Get-WelaWefStateKey $entry.Before)) { throw 'WEF control changed since planning; review a fresh plan.' }
$state.Initial=$false; $state.Snapshot=$current
}
return $current
}
$test={ param($current,$state) Test-WelaWefControl $current $state.Entry }
$apply={
param($state)
$entry=$state.Entry
$fresh=Get-WelaWefControlState $entry.Kind $entry.Target
if ((Get-WelaWefStateKey $fresh) -cne (Get-WelaWefStateKey $state.Snapshot)) { throw 'WEF control changed after the recovery snapshot; no write attempted.' }
switch ($entry.Kind) {
'Service' {
if ($fresh.StartMode -ne 'Auto') { Set-Service -Name $entry.Target.Name -StartupType Automatic -ErrorAction Stop }
if ($fresh.State -ne 'Running') { Start-Service -Name $entry.Target.Name -ErrorAction Stop }
}
'Wsman' {
if ($fresh.SourceOfValue) { throw 'Policy-owned WSMan settings are not overwritten.' }
Set-Item -LiteralPath $entry.Target.Path -Value $entry.Desired.Value -ErrorAction Stop
}
'Readers' {
$hostState=Get-WelaWefHost
if (-not $hostState.DomainJoined -or $hostState.DomainRole -notin @(1,3)) { throw 'Only a confirmed domain member workstation/server can receive a local Event Log Readers update; DC or unknown group authority requires manual administration.' }
$group=Get-LocalGroup -SID 'S-1-5-32-573' -ErrorAction Stop
Add-LocalGroupMember -Group $group -Member 'S-1-5-20' -ErrorAction Stop
$after=Get-WelaWefControlState Readers $entry.Target
$expected=@($fresh.MemberSids + 'S-1-5-20' | Sort-Object -Unique)
if (@(Compare-Object $expected @($after.MemberSids)).Count) { throw 'Group membership changed beyond the requested additive NETWORK SERVICE member.' }
}
'SubscriptionManager' {
$null=Test-WelaWefAdmx
if ($fresh.ValueExists) { throw 'Existing SubscriptionManager values are not overwritten.' }
New-WelaRegistryKey $entry.Target.Path
# Recheck after key creation; a concurrent policy writer may populate the slot.
if ((Get-WelaRegistryState $entry.Target.Path $entry.Target.Name).ValueExists) { throw 'SubscriptionManager slot was populated concurrently.' }
New-ItemProperty -LiteralPath $entry.Target.Path -Name $entry.Target.Name -Value $entry.Desired.Value -PropertyType String -ErrorAction Stop | Out-Null
}
'ForwardedEvents' {
if (-not (Test-WelaNativeChannelSnapshot $fresh)) { throw 'ForwardedEvents settings cannot be preserved from an unreadable snapshot.' }
Invoke-WelaNative -FilePath 'wevtutil.exe' -Arguments @('sl','ForwardedEvents','/e:true')
$after=Get-WelaWefControlState ForwardedEvents $entry.Target
if ($after.MaximumSizeInBytes -ne $fresh.MaximumSizeInBytes -or $after.LogMode -ne $fresh.LogMode -or $after.SecurityDescriptor -cne $fresh.SecurityDescriptor) { throw 'ForwardedEvents buffer, mode or ACL drifted during enablement.' }
}
'Subscription' {
$prerequisites=@(Get-WelaWefCollectorPrerequisites $state.Config)
if (@($prerequisites | Where-Object { -not $_.Verified }).Count) { throw 'Collector prerequisites changed or remain unmet; subscription creation is blocked.' }
if ($fresh.Exists) { throw 'Existing subscriptions are never overwritten.' }
$file=Join-Path $state.BackupPath ('subscription-' + [guid]::NewGuid().ToString('N') + '.xml')
$bytes=(New-Object Text.UTF8Encoding($false)).GetBytes($entry.Desired.Xml)
$stream=[IO.File]::Open($file,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
try { $stream.Write($bytes,0,$bytes.Length) } finally { $stream.Dispose() }
# Hold a read-sharing lock over the exact prepared XML throughout native import.
$lock=[IO.File]::Open($file,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
try {
$observedBytes=New-Object byte[] $lock.Length
if ($lock.Read($observedBytes,0,$observedBytes.Length) -ne $bytes.Length -or [Convert]::ToBase64String($observedBytes) -cne [Convert]::ToBase64String($bytes)) { throw 'Prepared subscription XML changed before import.' }
Invoke-WelaNative -FilePath 'wecutil.exe' -Arguments @('cs',$file)
} finally { $lock.Dispose() }
}
}
}
Invoke-WelaConfigurationControl -Context $Context -Id ("WEF/{0}/{1}" -f $Entry.Kind,($Entry.Target | ConvertTo-Json -Compress)) -Kind $Entry.Kind -Target $Entry.Target -Desired $Entry.Desired -Read $read -Compliant $test -Apply $apply -CallbackState $state -Description 'Apply this explicitly selected native WEF setting; preserve unrelated configuration.'
}
function Get-WelaWefInventory {
param($InputModel,[string]$Role)
foreach ($subscription in $InputModel.Subscriptions) {
$channels=@()
foreach ($name in $subscription.Query.Channels) { $channels += Get-WelaNativeChannel -Name $name }
$runtime=$null; $observed=$null; $observationError=''
if ($Role -eq 'Collector') {
try { $observed=Get-WelaWefControlState Subscription @{ Id=$subscription.Id; SourceSids=$subscription.SourceSids } }
catch { $observationError=$_.ToString() }
try { $native=Invoke-WelaNative -FilePath 'wecutil.exe' -Arguments @('gr',$subscription.Id); $runtime=[pscustomobject]@{ State='CommandSucceeded'; Raw=$native.Diagnostic; Diagnostic='Localized native runtime status is retained without inferring event arrival.' } }
catch { $runtime=[pscustomobject]@{ State='Unknown'; Raw=$null; Diagnostic=$_.ToString() } }
}
[pscustomobject]@{ Id=$subscription.Id; RequestedEnabled=$subscription.Definition.Enabled; RequestedDefinition=$subscription.Definition; ObservedEnabled=$(if ($observed.Exists) { $observed.Definition.Enabled } else { $null }); ObservedSubscription=$observed; ObservationError=$observationError; Filters=$subscription.Query.Filters; SourceChannels=$channels; ChannelObservationLocation=$(if ($Role -eq 'Collector') { 'Collector only; remote source states are not observed' } else { 'Local source' }); Runtime=$runtime; EffectiveSourceReadAccess='Not tested'; EventArrival='Not tested'; ForwardedSigmaCoverage='Not assessed' }
}
}
function Invoke-WelaWefCommand {
param([ValidateSet('Source','Collector')][string]$Role,[ValidateSet('Audit','Plan','Configure')][string]$Action='Audit',
[string]$ConfigPath,[switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath)
if (-not $ConfigPath) { throw '-WefConfigPath is required.' }
$model=Import-WelaWefConfig -Path $ConfigPath -Role $Role; $config=$model.Config
if ($env:OS -ne 'Windows_NT') { throw 'Native WEF commands require Windows.' }
if ($DryRun -and $Action -ne 'Configure') { throw '-DryRun requires WefAction Configure; Audit and Plan are read-only.' }
$scope=if ($Role -eq 'Source') { 'wef-source-configuration-only' } else { 'wec-collector-subscriptions-only' }
$hostState=$null; $hostError=''
try { $hostState=Get-WelaWefHost } catch { $hostError=$_.ToString() }
$hostReady=$hostState -and $hostState.DomainJoined -and ($Role -eq 'Source' -or ($hostState.Fqdn -ieq $config.CollectorFqdn -and $hostState.DomainRole -eq 3))
$entries=@(); $services=if ($Role -eq 'Source') { @('WinRM') } else { @('WinRM','Wecsvc') }
foreach ($service in $services) { $entries += New-WelaWefEntry Service @{ Name=$service } @{ StartMode='Auto'; State='Running' } }
$entries += @(Get-WelaWefHardeningEntries $Role)
if ($Role -eq 'Source') {
$entries += New-WelaWefEntry Readers @{ GroupSid='S-1-5-32-573' } @{ AddMemberSid='S-1-5-20' }
$entries += New-WelaWefEntry SubscriptionManager @{ Path='HKLM:\Software\Policies\Microsoft\Windows\EventLog\EventForwarding\SubscriptionManager'; Name=[string]$config.SubscriptionManagerSlot } @{ Value=('Server={0},Refresh={1}' -f $config.CollectorUri,$config.RefreshSeconds) }
} else {
$entries += New-WelaWefEntry ForwardedEvents @{ Channel='ForwardedEvents' } @{ IsEnabled=$true }
foreach ($subscription in $model.Subscriptions) { $entries += New-WelaWefEntry Subscription @{ Id=$subscription.Id; SourceSids=$subscription.SourceSids } @{ Key=$subscription.Key; Xml=$subscription.Xml } }
}
$channelPlan=@()
if ($Role -eq 'Source' -and $config.ApplyChannelProfile) { $channelProfile=Get-WelaNativeChannelProfile; $channelPlan=@(Get-WelaNativeChannelPlan -Profile $channelProfile -GrantEventLogReaders:$config.GrantCapi2Read) }
if ($Action -eq 'Configure') {
$context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
if (-not $hostReady) { Add-WelaWefFailure $context HostIdentity ('Domain membership / collector member-server FQDN prerequisite is unmet. ' + $hostError) $hostState }
else {
# Service startup is separate from listeners/firewall configuration. No qc/remoting shortcut.
foreach ($entry in @($entries | Where-Object Kind -eq 'Service')) { Set-WelaWefEntry $context $entry $config }
foreach ($entry in @(Get-WelaWefHardeningEntries $Role)) {
if ($config.Hardening -eq 'ApplyASD') { Set-WelaWefEntry $context $entry $config }
}
if ($Role -eq 'Source') {
$admxReady=$false
try { $admxReady=Test-WelaWefAdmx } catch { Add-WelaWefFailure $context SubscriptionManagerAdmx $_.ToString() $null }
foreach ($entry in @($entries | Where-Object Kind -eq 'Readers')) {
if ($config.GrantNetworkServiceRead) { Set-WelaWefEntry $context $entry $config }
}
if ($channelPlan.Count) { Set-WelaNativeChannelControls -Context $context -Plan $channelPlan -Profile $channelProfile.id }
if ($admxReady -and -not @($context.Results | Where-Object Status -eq 'Failed').Count) {
$sourcePrerequisites=@(Get-WelaWefPrerequisites $model Source)
if (@($sourcePrerequisites | Where-Object { -not $_.Verified }).Count) { Add-WelaWefFailure $context SourcePrerequisites 'SubscriptionManager configuration is blocked until local source prerequisites are verified.' $sourcePrerequisites }
else { foreach ($entry in @($entries | Where-Object Kind -eq 'SubscriptionManager')) { Set-WelaWefEntry $context $entry $config } }
}
} else {
$prerequisites=@(Get-WelaWefCollectorPrerequisites $config)
if (@($prerequisites | Where-Object { -not $_.Verified }).Count) { Add-WelaWefFailure $context CollectorPrerequisites 'Subscription creation is blocked until the explicit listener, ingress, service and ASD hardening prerequisites are verified.' $prerequisites }
else {
foreach ($entry in @($entries | Where-Object Kind -eq 'ForwardedEvents')) { Set-WelaWefEntry $context $entry $config }
if (-not @($context.Results | Where-Object Status -eq 'Failed').Count) {
foreach ($subscription in $model.Subscriptions) {
$entry=New-WelaWefEntry Subscription @{ Id=$subscription.Id; SourceSids=$subscription.SourceSids } @{ Key=$subscription.Key; Xml=$subscription.Xml }
Set-WelaWefEntry $context $entry $config
}
}
}
}
}
Set-WelaWefPrerequisiteCheck $context $model $Role
$report=Complete-WelaConfiguration -Context $context -Scope $scope -SuccessMessage 'Requested local WEF controls read back. Runtime source access and event arrival remain unverified.'
} else { $report=[pscustomobject]@{ Scope=$scope; ExitCode=0; DryRun=$false } }
$current=@()
foreach ($entry in $entries) { $current += New-WelaWefEntry $entry.Kind $entry.Target $entry.Desired }
$prerequisites=@(Get-WelaWefPrerequisites $model $Role)
$inventory=@(Get-WelaWefInventory $model $Role)
$unmet=@($current | Where-Object Status -ne 'RequestedSettingsMatch')
$sourceChannelProblems=@($inventory | ForEach-Object SourceChannels | Where-Object State -ne 'Enabled')
$localMatch=$hostReady -and -not $unmet.Count -and -not @($prerequisites | Where-Object { -not $_.Verified }).Count -and ($Role -ne 'Source' -or -not $sourceChannelProblems.Count)
if ($Action -eq 'Configure' -and -not $DryRun -and -not $localMatch) { $report.ExitCode=1 }
if ($Action -ne 'Configure' -and (-not $hostReady -or @($current | Where-Object Status -in @('Unknown','ManualReview')).Count)) { $report.ExitCode=1 }
$report | Add-Member NoteProperty Action $Action
$report | Add-Member NoteProperty Role $Role
$report | Add-Member NoteProperty CollectorUri $config.CollectorUri
$report | Add-Member NoteProperty HostIdentity $hostState
$report | Add-Member NoteProperty LocalConfigurationStatus $(if ($localMatch) { 'RequestedSettingsMatch' } else { 'Incomplete' })
$report | Add-Member NoteProperty Controls $current
$report | Add-Member NoteProperty Prerequisites $prerequisites
$report | Add-Member NoteProperty ChannelPlan $channelPlan
$report | Add-Member NoteProperty Subscriptions $inventory
$report | Add-Member NoteProperty UnverifiedPrerequisites @('Source identity authorization/group token refresh and effective channel read access','Domain trust/Kerberos, endpoint reachability and packet acceptance','Network logon rights, event generation, subscription runtime health and representative collector arrivals','GPO refresh persistence, collection capacity/retention and forwarded Sigma coverage')
Write-Host "Local WEF configuration: $($report.LocalConfigurationStatus). Effective read access, event arrival and forwarded Sigma coverage are not verified." -ForegroundColor Yellow
$current | Select-Object Kind,Target,Status,Diagnostic | Format-Table -AutoSize | Out-Host
if ($ResultsPath) { try { $report | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } catch { $report.ExitCode=1; Write-Host "[Failed] Writing WEF results: $_" -ForegroundColor Red } }
return $report
}