Files
WELA/modules/NativeChannelAccess.psm1
T

124 lines
8.0 KiB
PowerShell

# Native channel metadata and lossless, read-only ACL planning. Windows PowerShell 5.1.
function Get-WelaNativeChannelProfile {
param([string]$Id = 'microsoft-wef-appendix-c')
$profile = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/native_channel_profile.json') -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if ($Id -ne $profile.id -or $profile.schemaVersion -ne 1 -or $profile.scope -ne 'native-channel-settings-only') { throw "Unknown/invalid native channel profile '$Id'." }
$names = @{}
foreach ($control in $profile.controls) {
if (-not $control.channel -or $control.channel -match '[*?\[\]\r\n]' -or $names.ContainsKey($control.channel)) { throw 'Invalid/duplicate native channel name.' }
$names[$control.channel] = $true
if ($null -ne $control.enabled -and ($control.enabled -isnot [bool] -or -not $control.enabled)) { throw 'Native channel profiles may only enable a channel or preserve its enabled state.' }
if ($control.sourceExampleBytes -isnot [int] -and $control.sourceExampleBytes -isnot [long]) { throw 'Channel size must be an integer byte count.' }
$null = ConvertTo-WelaEventLogBytes $control.sourceExampleBytes
if ($control.readerSid -and ($control.readerSid -ne 'S-1-5-32-573' -or $control.readerMask -ne 1)) { throw 'Only the Event Log Readers read grant is supported.' }
}
foreach ($name in @('Baseline', 'Suspect')) {
if (@($profile.querySets.$name.channels).Count -eq 0) { throw "Empty native WEF query inventory: $name" }
foreach ($channel in $profile.querySets.$name.channels) {
if (-not $channel.name -or $channel.name -match '[*?\[\]\r\n]|Sysmon' -or @($channel.queryIds).Count -eq 0) { throw 'Invalid native WEF query inventory.' }
}
}
return $profile
}
function Get-WelaDescriptorBytes {
param($Descriptor)
$bytes = New-Object byte[] $Descriptor.BinaryLength
$Descriptor.GetBinaryForm($bytes, 0)
return ,$bytes
}
function Test-WelaChannelDescriptorEqual {
param([string]$First, [string]$Second)
if (-not $First -or -not $Second) { return $false }
try {
$a = [System.Security.AccessControl.RawSecurityDescriptor]::new($First)
$b = [System.Security.AccessControl.RawSecurityDescriptor]::new($Second)
return [Convert]::ToBase64String((Get-WelaDescriptorBytes $a)) -ceq [Convert]::ToBase64String((Get-WelaDescriptorBytes $b))
} catch { return $false }
}
function Get-WelaChannelAccessPlan {
param([string]$SecurityDescriptor)
$result = [ordered]@{
State = 'Unknown'; Sid = 'S-1-5-32-573'; AccessMask = 1
ProposedDescriptor = $null; ExistingAceCount = $null; AddedAceIndex = $null
EffectiveReadAccess = 'Not tested'; Diagnostic = ''
}
try {
if (-not $SecurityDescriptor) { throw 'Channel security descriptor was not readable.' }
$original = [System.Security.AccessControl.RawSecurityDescriptor]::new($SecurityDescriptor)
if (-not ($original.ControlFlags -band [System.Security.AccessControl.ControlFlags]::DiscretionaryAclPresent) -or $null -eq $original.DiscretionaryAcl) {
throw 'Absent/null DACL requires manual review; adding a DACL would change unrelated access.'
}
$result.ExistingAceCount = $original.DiscretionaryAcl.Count
$grant = $false; $deny = $false; $unknownAce = $false
foreach ($ace in $original.DiscretionaryAcl) {
if ($ace -isnot [System.Security.AccessControl.KnownAce]) { $unknownAce = $true; continue }
# PowerShell 5.1 cannot bitwise-cast byte-backed AceFlags enums.
if ([int]$ace.AceFlags -band [int][System.Security.AccessControl.AceFlags]::InheritOnly) { continue }
$readMask = ($ace.AccessMask -band 1) -or ($ace.AccessMask -band 268435456) -or ($ace.AccessMask -band [int]::MinValue)
if ($readMask -and $ace.AceQualifier -eq [System.Security.AccessControl.AceQualifier]::AccessDenied) { $deny = $true }
if ($ace -is [System.Security.AccessControl.CommonAce] -and -not $ace.IsCallback -and
$ace.AceQualifier -eq [System.Security.AccessControl.AceQualifier]::AccessAllowed -and
$ace.SecurityIdentifier.Value -eq $result.Sid -and ($ace.AccessMask -band 1)) { $grant = $true }
}
if ($unknownAce) { throw 'An unknown ACE requires manual review; the descriptor is preserved without mutation.' }
if ($deny) { throw 'A read-deny ACE may affect the forwarding token; the descriptor is preserved for manual review.' }
if ($grant) { $result.State = 'GrantPresent'; return [pscustomobject]$result }
# Bind the binary overload explicitly on Windows PowerShell 5.1.
[byte[]]$originalBytes = Get-WelaDescriptorBytes $original
$copy = [System.Security.AccessControl.RawSecurityDescriptor]::new($originalBytes, 0)
$newAce = [System.Security.AccessControl.CommonAce]::new(
[System.Security.AccessControl.AceFlags]::None,
[System.Security.AccessControl.AceQualifier]::AccessAllowed, 1,
[System.Security.Principal.SecurityIdentifier]::new($result.Sid), $false, $null)
# Retain every existing ACE, including callback/object ACEs, in original order.
# Place the explicit allow before inherited entries; do not canonicalize others.
$index = $copy.DiscretionaryAcl.Count
for ($i = 0; $i -lt $copy.DiscretionaryAcl.Count; $i++) {
if ([int]$copy.DiscretionaryAcl[$i].AceFlags -band [int][System.Security.AccessControl.AceFlags]::Inherited) { $index = $i; break }
}
$copy.DiscretionaryAcl.InsertAce($index, $newAce)
$sddl = $copy.GetSddlForm([System.Security.AccessControl.AccessControlSections]::All)
$roundTrip = [System.Security.AccessControl.RawSecurityDescriptor]::new($sddl)
if ([Convert]::ToBase64String((Get-WelaDescriptorBytes $copy)) -cne [Convert]::ToBase64String((Get-WelaDescriptorBytes $roundTrip))) {
throw 'SDDL conversion was not lossless; refusing to replace the channel descriptor.'
}
$result.State = 'GrantRequired'; $result.ProposedDescriptor = $sddl; $result.AddedAceIndex = $index
} catch {
$result.State = 'ManualReview'; $result.Diagnostic = $_.Exception.Message
}
[pscustomobject]$result
}
function Get-WelaNativeChannelInventory {
param($Profile, [ValidateSet('Baseline', 'Suspect', 'Both')][string]$QuerySet = 'Both')
$selected = if ($QuerySet -eq 'Both') { @('Baseline', 'Suspect') } else { @($QuerySet) }
$entries = @{}
foreach ($set in $selected) {
foreach ($channel in $Profile.querySets.$set.channels) {
if (-not $entries.ContainsKey($channel.name)) { $entries[$channel.name] = @() }
$entries[$channel.name] += [pscustomobject]@{ QuerySet = $set; QueryIds = @($channel.queryIds) }
}
}
foreach ($name in @($entries.Keys | Sort-Object)) {
$channel = Get-WelaNativeChannel -Name $name
$unmet = @()
if ($channel.State -eq 'Not installed') { $unmet += 'Channel not installed; review role/query applicability.' }
elseif ($channel.State -ne 'Enabled') { $unmet += "Channel enabled state is $($channel.State)." }
if (-not $channel.SecurityDescriptor) { $unmet += 'Channel security descriptor unreadable.' }
# A channel ACE does not establish group membership, token access, producer
# configuration or WEF ingestion. No usable-rule credit is derived here.
$unmet += @('Event producer/audit policy and representative event generation not verified.',
'Intended forwarding identity token and actual event read not tested.',
'WEF subscription/transport and collector ingestion not verified.')
[pscustomobject]@{
Channel = $channel; Queries = @($entries[$name]); Prerequisites = $unmet
EffectiveReadAccess = 'Not tested'; EventGeneration = 'Not tested'; Forwarding = 'Not tested'
}
}
}
Export-ModuleMember -Function Get-WelaNativeChannelProfile, Test-WelaChannelDescriptorEqual, Get-WelaChannelAccessPlan, Get-WelaNativeChannelInventory