Files
WELA/scripts/ChannelRead.ps1
T
田中ザック Isaac Mathis fd7a7924aa Verify actual current-token access to built-in event channels (#432)
* Add actual current-token native channel read evidence

* Use supported workflow shells and link channel-read changelogs

* Handle real event exceptions and bind loaded token helper to source

* Capture query-token interval after evidence and metadata preparation

* Retain native child process exit evidence across PowerShell engines

* Bound native reader fixture pipe draining and child termination

* Preserve native errors from attributed channel query statuses
2026-09-21 09:18:46 +09:00

142 lines
11 KiB
PowerShell

# Queries the actual current primary token. No credential, impersonation or configuration adapters.
function Get-WelaChannelReadSources {
$sources=[ordered]@{}
foreach($path in @('WELA.ps1','scripts/ChannelRead.ps1','scripts/ChannelReadNative.cs','scripts/WefArrival.ps1','modules/NativeProviders.psm1','config/native_channel_profile.json')) {
$sources[$path]=(Get-FileHash -LiteralPath (Join-Path $script:ScriptRoot $path) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
}
[pscustomobject]$sources
}
function Get-WelaChannelReadKey { param($Value) ConvertTo-Json -InputObject $Value -Depth 20 -Compress }
function Get-WelaChannelReader {
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'channel-read requires native 64-bit Windows.'}
$nativeBytes=[IO.File]::ReadAllBytes((Join-Path $script:ScriptRoot 'scripts/ChannelReadNative.cs'))
$nativeHash=Get-WelaArrivalHash $nativeBytes
if(-not ('Wela.ChannelRead.Token' -as [type])){
Add-Type -TypeDefinition ([Text.UTF8Encoding]::new($false,$true).GetString($nativeBytes).TrimStart([char]0xfeff)) -ErrorAction Stop
[Wela.ChannelRead.Token]::SourceSha256=$nativeHash
}
if([Wela.ChannelRead.Token]::SourceSha256 -cne $nativeHash){throw 'Loaded channel-reader helper differs from current source; start a fresh PowerShell process.'}
$threadIdentity=[Security.Principal.WindowsIdentity]::GetCurrent($true)
if($null -ne $threadIdentity){$threadIdentity.Dispose();throw 'Impersonated readers are unsupported; launch WELA under the intended primary token.'}
$identity=[Security.Principal.WindowsIdentity]::GetCurrent()
try {
$stats=[Wela.ChannelRead.Token]::Read($identity.Token)
[pscustomobject][ordered]@{
Computer=[Environment]::MachineName;ProcessId=$PID;UserSid=$identity.User.Value;UserName=$identity.Name
TokenId=$stats.TokenId.ToString();AuthenticationId=$stats.AuthenticationId.ToString();ModifiedId=$stats.ModifiedId.ToString()
GroupSids=@($identity.Groups|ForEach-Object Value|Sort-Object)
GroupCount=$stats.GroupCount;PrivilegeCount=$stats.PrivilegeCount
ElevatedAdministrator=([Security.Principal.WindowsPrincipal]::new($identity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
TokenType='Primary';Impersonation='Absent'
}
} finally {$identity.Dispose()}
}
function Get-WelaChannelReadHost {
$os=Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
$computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
$build=[int]$os.BuildNumber
if(($os.ProductType -eq 1 -and $build -notin @(22000,22621,22631,26100,26200)) -or ($os.ProductType -in @(2,3) -and $build -notin @(20348,26100)) -or $os.ProductType -notin @(1,2,3)){throw 'Host is outside reviewed Windows 11 / Server 2022 and 2025 builds.'}
$version=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
[pscustomobject][ordered]@{Computer=[Environment]::MachineName;Build=$build;UBR=$version.UBR;Edition=$version.EditionID;ProductType=[int]$os.ProductType;DomainRole=[int]$computer.DomainRole;DomainJoined=[bool]$computer.PartOfDomain;Domain=[string]$computer.Domain}
}
function Get-WelaChannelReadSelection {
param([string[]]$Channels)
$profile=Get-Content -LiteralPath (Join-Path $script:ScriptRoot 'config/native_channel_profile.json') -Raw -ErrorAction Stop|ConvertFrom-Json
$allowed=@($profile.querySets.Baseline.channels.name)+@($profile.querySets.Suspect.channels.name)
if(-not $Channels -or $Channels.Count -gt 8){throw 'Select between one and eight reviewed built-in channels.'}
$seen=@{}
foreach($channel in $Channels){
if($channel -cnotin $allowed -or $seen.ContainsKey($channel)){throw 'Unknown, mis-cased or duplicate channel; only exact native WEF inventory names are accepted.'}
$seen[$channel]=$true
$channel
}
}
function Get-WelaChannelReadFailure {
param([Exception]$Exception)
$current=$Exception;$code=$null;$absent=$false
while($current){
if($current -is [UnauthorizedAccessException]){$code=5;break}
if($current.PSObject.Properties['ErrorCode']){$code=[int]$current.ErrorCode}
if($current -is [ComponentModel.Win32Exception]){$code=$current.NativeErrorCode}
if($current -is [Diagnostics.Eventing.Reader.EventLogNotFoundException]){$absent=$true}
# Modern EventLogException stores Win32 codes in HRESULT; .NET Framework
# does not reliably expose its private native code. Never parse localized text.
if($current -is [Diagnostics.Eventing.Reader.EventLogException]){
$hr=([long]$current.HResult -band 0xffffffffL)
if(($hr -band 0xffff0000L) -eq 0x80070000L){$code=[int]($hr -band 0xffffL)}
}
$current=$current.InnerException
}
$state=if($code -eq 5){'Denied'}elseif($absent -or $code -in @(2,3,15007)){'Absent'}else{'Unknown'}
[pscustomobject]@{Status=$state;NativeError=$code;Diagnostic=$Exception.Message}
}
function Assert-WelaChannelQueryStatus {
param([string]$Channel,[object[]]$LogStatus)
# A native status is attributable only to the one exact channel queried.
if($LogStatus.Count -ne 1 -or $LogStatus[0].LogName -cne $Channel -or $LogStatus[0].StatusCode -isnot [int]){throw 'Query status is incomplete, mismatched or mistyped.'}
if($LogStatus[0].StatusCode -ne 0){throw [ComponentModel.Win32Exception]::new($LogStatus[0].StatusCode)}
}
function Read-WelaChannelLatest {
param([string]$Channel)
$reader=$null;$event=$null
$result=[pscustomobject][ordered]@{Channel=$Channel;PathType='LogName';Session='Local';XPath='*';ReverseDirection=$true;MaximumEvents=1;ReadTimeoutMs=5000;StartedUtc=[DateTime]::UtcNow.ToString('o');CompletedUtc=$null;Status='Unknown';NativeError=$null;LogStatus=@();Event=$null;Diagnostic=''}
try {
$query=[Diagnostics.Eventing.Reader.EventLogQuery]::new($Channel,[Diagnostics.Eventing.Reader.PathType]::LogName,'*')
$query.ReverseDirection=$true;$query.TolerateQueryErrors=$false
$reader=[Diagnostics.Eventing.Reader.EventLogReader]::new($query);$reader.BatchSize=1
$event=$reader.ReadEvent([TimeSpan]::FromMilliseconds(5000))
$result.LogStatus=@($reader.LogStatus|ForEach-Object{[pscustomobject]@{LogName=$_.LogName;StatusCode=$_.StatusCode}})
Assert-WelaChannelQueryStatus -Channel $Channel -LogStatus $result.LogStatus
if($null -eq $event){$result.Status='ReadAllowedEmpty'}else{
if($event.LogName -cne $Channel -or $null -eq $event.RecordId -or $event.RecordId -le 0 -or -not $event.ProviderName -or $event.ProviderName.Length -gt 512 -or -not $event.MachineName -or $event.MachineName.Length -gt 255){throw 'Returned event provenance is incomplete or mismatches the selected local channel.'}
# Only bounded System metadata is exported. Message, payload and raw XML remain unexported.
$result.Event=[pscustomobject][ordered]@{Channel=$event.LogName;RecordId=[long]$event.RecordId;Provider=$event.ProviderName;EventId=[int]$event.Id;Version=$event.Version;Computer=$event.MachineName;TimeCreatedUtc=$(if($event.TimeCreated){$event.TimeCreated.ToUniversalTime().ToString('o')}else{$null})}
$result.Status='EventObserved'
}
}catch{
$failure=Get-WelaChannelReadFailure $_.Exception
$result.Status=$failure.Status;$result.NativeError=$failure.NativeError;$result.Diagnostic=$failure.Diagnostic;$result.Event=$null
}finally{if($event){$event.Dispose()};if($reader){$reader.Dispose()};$result.CompletedUtc=[DateTime]::UtcNow.ToString('o')}
$result
}
function Invoke-WelaChannelRead {
param([string[]]$Channels,[string]$OutputPath)
$sources=Get-WelaChannelReadSources;$sourceKey=Get-WelaChannelReadKey $sources
$selected=@(Get-WelaChannelReadSelection $Channels)
if(-not $OutputPath){throw 'channel-read requires a new ChannelReadOutputPath.'}
$hostState=Get-WelaChannelReadHost;$hostKey=Get-WelaChannelReadKey $hostState
$output=New-WelaArrivalOutput -Path $OutputPath -SourcePath $script:ScriptRoot
# Filesystem ACL and metadata APIs may temporarily adjust available privileges.
# Finish preparation before capturing the token used by the actual event queries.
$metadataByName=@{}
foreach($channel in $selected){$metadataByName[$channel]=Get-WelaNativeChannel -Name $channel}
$before=Get-WelaChannelReader;$readerKey=Get-WelaChannelReadKey $before
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaNativeChannelRead';RecordedUtc=[DateTime]::UtcNow.ToString('o');ExitCode=1;Status='Unverified';Host=$hostState;ReaderBefore=$before;ReaderAfter=$null;ReaderInterval='After output/metadata preparation, before first query through final checks';Sources=$sources;Results=@();Diagnostic='';ReadyRuleCredit=0;ConfigurationChanges=0;Scope='Actual current primary-token local query access at observation time only';EventGeneration='Not tested';Forwarding='Not tested';OutputPath=$output}
try {
foreach($channel in $selected){
if((Get-WelaChannelReadKey (Get-WelaChannelReader)) -cne $readerKey){throw 'Reader token changed before query.'}
# Channel configuration may require rights the actual event query does not.
$metadata=$metadataByName[$channel]
$query=Read-WelaChannelLatest $channel
$readerAfter=Get-WelaChannelReader
$row=[pscustomobject]@{Channel=$channel;ConfigurationObservation=$metadata;Query=$query;AccessVerified=$false;ReaderStable=$false}
$report.Results+= $row
if((Get-WelaChannelReadKey $readerAfter) -cne $readerKey){throw 'Reader token changed during query.'}
$row.ReaderStable=$true
$row.AccessVerified=$query.Status -in @('ReadAllowedEmpty','EventObserved')
}
$finalHostKey=Get-WelaChannelReadKey (Get-WelaChannelReadHost);$finalSourceKey=Get-WelaChannelReadKey (Get-WelaChannelReadSources)
$report.ReaderAfter=Get-WelaChannelReader
if((Get-WelaChannelReadKey $report.ReaderAfter) -cne $readerKey -or $finalHostKey -cne $hostKey -or $finalSourceKey -cne $sourceKey){throw 'Reader, host or implementation changed during observation.'}
$report.Status='Completed'
$report.ExitCode=if(@($report.Results|Where-Object{-not $_.AccessVerified}).Count){1}else{0}
}catch{
$report.Diagnostic=$_.Exception.Message
foreach($row in $report.Results){$row.AccessVerified=$false}
}
$json=$report|ConvertTo-Json -Depth 20
if([Text.Encoding]::UTF8.GetByteCount($json) -gt 1048576){throw 'Channel-read report exceeds the one MiB bound; no successful evidence was written.'}
$null=Write-WelaArrivalArtifact $output 'result.json' $json
$report
}