Merge dev and preserve independent recovery and WEC commands

This commit is contained in:
Shirofune-Security committed 2026-09-21 18:11:29 +09:00
commit e068bd8f52
31 files changed
+1563 -14

No files matched your search

+35 -6
View File
@@ -284,10 +284,17 @@ function Get-WelaAuditPolicyMask {
function Set-WelaAuditPolicyControl {
param($Context, $Policy, [ValidateRange(0, 3)][int]$Mask = 3,
[ValidateSet('exact', 'minimum')][string]$Mode = 'exact', [switch]$RequirePrecedence)
[ValidateSet('exact', 'minimum')][string]$Mode = 'exact', [switch]$RequirePrecedence,
$IpsecObservations, [scriptblock]$ReadIpsec = { Get-WelaIpsecPrerequisite })
$guid = $Policy.GUID
$state = @{ Guid = $guid; Mask = $Mask; Mode = $Mode; RequirePrecedence = [bool]$RequirePrecedence }
$read = { param($state) Get-WelaAuditPolicyMask -Guid $state.Guid }
$state = @{ Guid = $guid; Mask = $Mask; Mode = $Mode; RequirePrecedence = [bool]$RequirePrecedence; IpsecObservations=$IpsecObservations; ReadIpsec=$ReadIpsec }
$read = { param($state)
if ($null -ne $state.IpsecObservations) {
$evidence = & $state.ReadIpsec; $state.IpsecObservations.Add($evidence)
Assert-WelaIpsecPrerequisite $evidence
}
Get-WelaAuditPolicyMask -Guid $state.Guid
}
$test = {
param($value, $state)
if ($state.Mode -eq 'minimum') { return ($value -band $state.Mask) -eq $state.Mask }
@@ -311,6 +318,11 @@ function Set-WelaAuditPolicyControl {
$failure = if ($state.Mask -band 2) { 'enable' } else { 'disable' }
$arguments += "/success:$success", "/failure:$failure"
}
if ($null -ne $state.IpsecObservations) {
# This check runs after the operator prompt and durable recovery journal.
$evidence = & $state.ReadIpsec; $state.IpsecObservations.Add($evidence)
Assert-WelaIpsecPrerequisite $evidence
}
Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments $arguments
}
Invoke-WelaConfigurationControl -Context $Context -Id "AuditPolicy/$($Policy.Name)" -Kind AuditPolicy `
@@ -318,7 +330,7 @@ function Set-WelaAuditPolicyControl {
}
function Set-WelaProfileAuditControls {
param($Context, $Plan)
param($Context, $Plan, [scriptblock]$ReadIpsec = { Get-WelaIpsecPrerequisite })
if ($Plan.PSObject.Properties['CustomProfileSource']) {
$Context | Add-Member NoteProperty CustomProfileGuard ([pscustomobject]@{Source=$Plan.CustomProfileSource;Role=$Plan.role;Build=$Plan.build}) -Force
Assert-WelaConfigurationProfileGuard $Context
@@ -334,9 +346,26 @@ function Set-WelaProfileAuditControls {
$Context.Results.Add([pscustomobject]@{ Id = "AuditPolicy/$($policy.id)"; Kind = 'AuditPolicy'; Target = @{ Guid = $policy.guid }; Desired = $policy.requiredMask; Before = $null; After = $null; Status = 'Skipped'; Diagnostic = 'Audit precedence was not verified; dependent policy was not changed.' })
continue
}
$mode = if ($policy.mode -eq 'minimum') { 'minimum' } else { 'exact' }
Set-WelaAuditPolicyControl -Context $Context -Policy @{ GUID = $policy.guid; Name = $policy.id } -Mask $policy.requiredMask -Mode $mode -RequirePrecedence
$conditional = Test-WelaIpsecConditionalPolicy $Plan $policy
$observations = $null; $blocked = $false
if ($conditional) {
$observations = New-Object 'System.Collections.Generic.List[object]'
try {
$evidence = & $ReadIpsec; $observations.Add($evidence)
$blocked = $evidence.Status -ne 'Applicable'
$status = if ($evidence.Status -eq 'NotObservedWithinScope') { 'Skipped' } else { 'Failed' }
$diagnostic = "IPsec prerequisite $($evidence.Status); policy preserved. $($evidence.Diagnostic)"
} catch { $blocked = $true; $status = 'Failed'; $diagnostic = $_.ToString() }
if ($blocked) {
$Context.Results.Add([pscustomobject]@{Id="AuditPolicy/$($policy.id)";Kind='AuditPolicy';Target=@{Guid=$policy.guid};Desired=@{Mask=$policy.requiredMask;Mode='exact'};Before=$null;After=$null;Status=$status;Diagnostic=$diagnostic})
}
}
if (-not $blocked) {
$mode = if ($policy.mode -eq 'minimum') { 'minimum' } else { 'exact' }
Set-WelaAuditPolicyControl -Context $Context -Policy @{ GUID = $policy.guid; Name = $policy.id } -Mask $policy.requiredMask -Mode $mode -RequirePrecedence -IpsecObservations $observations -ReadIpsec $ReadIpsec
}
$row = $Context.Results[$Context.Results.Count - 1]
if ($conditional) { $row | Add-Member NoteProperty PrerequisiteObservations $observations }
$row | Add-Member NoteProperty Profile $Plan.profile
$row | Add-Member NoteProperty Version $Plan.version
$row | Add-Member NoteProperty SchemaSha256 $Plan.schemaSha256
+73
View File
@@ -0,0 +1,73 @@
# Read-only local NetSecurity evidence. No policy, service or traffic changes.
function Test-WelaIpsecConditionalPolicy {
param($Plan, $Policy)
return (-not $Plan.PSObject.Properties['CustomProfileSource'] -and
$Plan.profile -ceq 'microsoft-stronger-reviewed-2026-09' -and
$Policy.guid -ieq '0CCE9218-69AE-11D9-BED3-505054503030' -and $Policy.mode -eq 'optional')
}
function Get-WelaIpsecPrerequisite {
[CmdletBinding()]
param([switch]$Offline,
[scriptblock]$ReadRules = { NetSecurity\Get-NetIPsecRule -PolicyStore ActiveStore -ErrorAction Stop },
[scriptblock]$ReadAssociations = { NetSecurity\Get-NetIPsecMainModeSA -ErrorAction Stop })
$started = [DateTime]::UtcNow.ToString('o')
$rules = @(); $associations = @(); $reads = @(); $diagnostics = @()
if ($Offline) { $diagnostics += 'Offline scenario; this host was not queried.' }
else {
foreach ($source in @('ActiveStoreRules', 'MainModeAssociations')) {
$status = 'Complete'; $errorText = ''; $items = @()
try {
$reader = if ($source -eq 'ActiveStoreRules') { $ReadRules } else { $ReadAssociations }
# Keep at most 4096 observations per native source. A cap is not an empty/successful inventory.
$items = @(& $reader | Select-Object -First 4097)
if ($items.Count -gt 4096) { throw 'Observation cap exceeded (4096 records).' }
$seen = @{}
foreach ($item in $items) {
if ($source -eq 'ActiveStoreRules') {
foreach ($property in @('Name', 'Enabled', 'InboundSecurity', 'OutboundSecurity', 'PrimaryStatus')) {
if ($null -eq $item -or -not $item.PSObject.Properties[$property] -or $null -eq $item.$property) { throw "Missing native rule property: $property." }
}
$name = [string]$item.Name
$enabled = [string]$item.Enabled; $inbound = [string]$item.InboundSecurity; $outbound = [string]$item.OutboundSecurity; $health = [string]$item.PrimaryStatus
if (-not $name -or $name.Length -gt 1024 -or $seen.ContainsKey($name) -or $enabled -cnotin @('True','False') -or
$inbound -cnotin @('None','Request','Require') -or $outbound -cnotin @('None','Request','Require') -or
$health -cnotin @('OK','Inactive','Error','Unknown')) { throw "Unrecognized or duplicate native IPsec rule observation: Name='$name', Enabled='$enabled', InboundSecurity='$inbound', OutboundSecurity='$outbound', PrimaryStatus='$health'." }
$seen[$name] = $true
$qualifies = $enabled -ceq 'True' -and ($inbound -cne 'None' -or $outbound -cne 'None') -and $health -ceq 'OK'
$rules += [pscustomobject]@{ Name=$name; Enabled=$enabled; InboundSecurity=$inbound; OutboundSecurity=$outbound; PrimaryStatus=$health; Qualifies=$qualifies }
if ($enabled -ceq 'True' -and ($inbound -cne 'None' -or $outbound -cne 'None') -and $health -cne 'OK') { throw 'Enabled non-exemption rule has uncertain effective health.' }
} else {
foreach ($property in @('Name','LocalEndpoint','RemoteEndpoint')) {
if ($null -eq $item -or -not $item.PSObject.Properties[$property] -or -not [string]$item.$property) { throw "Missing native association property: $property." }
}
$name = [string]$item.Name; $local = [string]$item.LocalEndpoint; $remote = [string]$item.RemoteEndpoint
$address = $null
if ($name.Length -gt 1024 -or $seen.ContainsKey($name) -or -not [Net.IPAddress]::TryParse($local,[ref]$address) -or -not [Net.IPAddress]::TryParse($remote,[ref]$address)) { throw 'Unrecognized or duplicate main-mode association.' }
$seen[$name] = $true
$associations += [pscustomobject]@{ Name=$name; LocalEndpoint=$local; RemoteEndpoint=$remote }
}
}
} catch { $status = 'Unknown'; $errorText = $_.Exception.Message; $diagnostics += "$source`: $errorText" }
$reads += [pscustomobject]@{ Source=$source; Status=$status; ObservedCount=$items.Count; Diagnostic=$errorText }
}
}
$status = if ($Offline -or @($reads | Where-Object Status -ne Complete).Count) { 'Unknown' }
elseif (@($rules | Where-Object Qualifies).Count -or $associations.Count) { 'Applicable' }
else { 'NotObservedWithinScope' }
[pscustomobject][ordered]@{
SchemaVersion=1; Status=$status; Scope='Local NetSecurity ActiveStore rules and current main-mode SAs'
StartedUtc=$started; CompletedUtc=[DateTime]::UtcNow.ToString('o'); ComputerName=$env:COMPUTERNAME
Basis=$(if ($status -eq 'Applicable') { 'Enabled healthy non-exemption effective rule or current main-mode SA observed.' } else { 'No complete positive prerequisite evidence.' })
Reads=$reads; Rules=$rules; MainModeAssociations=$associations; Diagnostic=($diagnostics -join ' ')
Limitations='Point-in-time local scope. Configured rules do not prove matching traffic, successful negotiation or audit events. Absence does not exclude legacy IPsec, VPN or other providers. No event-volume, failure-outcome or Sigma credit.'
}
}
function Assert-WelaIpsecPrerequisite {
param($Evidence)
if ($null -eq $Evidence -or $Evidence.Status -cne 'Applicable') {
$status = if ($Evidence) { $Evidence.Status } else { 'Unknown' }
throw "IPsec Main Mode prerequisite is $status; this conditional audit setting was not changed. $($Evidence.Diagnostic)"
}
}
+193
View File
@@ -0,0 +1,193 @@
# Explicit native audit-switch activation. No registry policy, security, share or service writes.
function Get-WelaSmbRuntimeKey { param($Value) ConvertTo-Json -InputObject $Value -Depth 24 -Compress }
function Get-WelaSmbRuntimeSources {
$result=[ordered]@{}
foreach($name in @('WELA.ps1','scripts/SmbRuntimeActivation.ps1','scripts/SmbAuditing.ps1','scripts/Configuration.ps1','scripts/WefArrival.ps1')) {
$result[$name]=(Get-FileHash -LiteralPath (Join-Path $script:ScriptRoot $name) -Algorithm SHA256 -ErrorAction Stop).Hash
}
[pscustomobject]$result
}
function Assert-WelaSmbRuntimeCommand {
param($Command,[ValidateSet('Server','Client')][string]$Side,[ValidateSet('Get','Set')][string]$Verb,[string]$ModuleBase)
# SmbShare exports functions from these native nested CDXML modules.
if($Command.Name -cne "$Verb-Smb${Side}Configuration" -or $Command.ModuleName -cne "Smb${Side}Configuration" -or
[string]$Command.CommandType -cne 'Function' -or [IO.Path]::GetFullPath($Command.Module.ModuleBase) -ine $ModuleBase){
$observed=[pscustomobject]@{Name=$Command.Name;ModuleName=$Command.ModuleName;ModuleBase=$Command.Module.ModuleBase;Type=[string]$Command.CommandType}
throw "SMB commands must resolve to the reviewed native SmbShare CDXML module. Expected $ModuleBase; observed $(Get-WelaSmbRuntimeKey $observed)"
}
if($Verb -eq 'Set') {
$component=if($Side -eq 'Server'){'LanmanServer'}else{'LanmanWorkstation'}
foreach($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $component)) {
if(-not $Command.Parameters.ContainsKey($definition.Name) -or $Command.Parameters[$definition.Name].ParameterType -ne [bool]) {
throw "Native setter lacks the exact Boolean parameter $($definition.Name)."
}
}
}
}
function Get-WelaSmbRuntimeCommands {
$base=[IO.Path]::GetFullPath((Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell/v1.0/Modules/SmbShare'))
$commands=[ordered]@{}
foreach($side in @('Server','Client')) {
foreach($verb in @('Get','Set')) {
$name="SmbShare\$verb-Smb${side}Configuration"
$found=@(Get-Command -Name $name -ErrorAction Stop)
if($found.Count -ne 1){throw 'Expected exactly one native module-qualified SMB command.'}
Assert-WelaSmbRuntimeCommand -Command $found[0] -Side $side -Verb $verb -ModuleBase $base
$commands[$name]=[pscustomobject]@{ModuleName=$found[0].ModuleName;ModuleBase=$base;ModuleVersion=$found[0].Module.Version.ToString();CommandType=$found[0].CommandType.ToString()}
}
}
$files=@(Get-ChildItem -LiteralPath $base -File -Recurse -ErrorAction Stop | Where-Object Extension -in @('.psd1','.psm1','.cdxml','.dll','.ps1xml') | Sort-Object FullName)
if($files.Count -lt 1 -or $files.Count -gt 100){throw 'Unexpected native SMB module inventory.'}
$hashes=[ordered]@{}
foreach($file in $files){
if($file.Length -gt 16MB -or ($file.Attributes -band [IO.FileAttributes]::ReparsePoint)){throw 'Unsupported SMB module source.'}
$hashes[$file.FullName]=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
}
[pscustomobject]@{Commands=[pscustomobject]$commands;Files=[pscustomobject]$hashes}
}
function ConvertTo-WelaSmbRuntimeConfiguration {
param($Configuration,[ValidateSet('Server','Client')][string]$Side)
if($Configuration.CimClass.CimClassName -cne "MSFT_Smb${Side}Configuration"){throw 'Expected one actual native SMB configuration CIM instance.'}
$properties=@($Configuration.CimInstanceProperties | Sort-Object Name)
if($properties.Count -lt 3 -or $properties.Count -gt 160){throw 'Unexpected SMB configuration property count.'}
$result=[ordered]@{}
foreach($property in $properties) {
if($result.Contains($property.Name)){throw 'Duplicate SMB configuration property.'}
$value=$property.Value
foreach($item in @($value)) {
if($null -ne $item -and $item -isnot [bool] -and $item -isnot [string] -and
$item -isnot [byte] -and $item -isnot [uint16] -and $item -isnot [uint32] -and $item -isnot [uint64] -and
$item -isnot [int16] -and $item -isnot [int32] -and $item -isnot [int64]){throw "Unsupported native configuration value: $($property.Name)"}
if($item -is [string] -and $item.Length -gt 8192){throw 'Native configuration string exceeds bound.'}
}
if(@($value).Count -gt 128){throw 'Native configuration array exceeds bound.'}
$result[$property.Name]=[pscustomobject]@{CimType=$property.CimType.ToString();Value=$value}
}
$component=if($Side -eq 'Server'){'LanmanServer'}else{'LanmanWorkstation'}
foreach($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $component)) {
if(-not $result.Contains($definition.Name) -or $result[$definition.Name].Value -isnot [bool] -or $result[$definition.Name].CimType -cne 'Boolean') {
throw "Native getter lacks the exact Boolean property $($definition.Name)."
}
}
[pscustomobject]$result
}
function Get-WelaSmbRuntimeState {
$hostState=Get-WelaSmbAuditHost
if($hostState.Status -ne 'Candidate'){throw "SMB runtime activation is $($hostState.Status): $($hostState.Diagnostic)"}
$commands=Get-WelaSmbRuntimeCommands
$policies=[ordered]@{}
foreach($definition in Get-WelaSmbAuditDefinitions) {
$capability=Get-WelaSmbAuditCapability -Definition $definition -HostState $hostState
if($capability.Status -ne 'Supported'){throw "Unverified $($definition.Component)/$($definition.Name): $($capability.Diagnostic)"}
$policies["$($definition.Component)/$($definition.Name)"]=[pscustomobject]@{
Path=$definition.Path;Name=$definition.Name;AdmxSha256=$capability.AdmxSha256
Policy=Get-WelaRegistryState -Path $definition.Path -Name $definition.Name
}
}
$configurations=[ordered]@{}
foreach($side in @('Server','Client')) {
$command="SmbShare\Get-Smb${side}Configuration"
$native=@(& $command -ErrorAction Stop)
if($native.Count -ne 1){throw 'Expected exactly one native SMB configuration.'}
$configurations[$side]=ConvertTo-WelaSmbRuntimeConfiguration -Configuration $native[0] -Side $side
}
[pscustomobject][ordered]@{Computer=[Environment]::MachineName;Host=$hostState;Commands=$commands;Sources=Get-WelaSmbRuntimeSources;Policies=[pscustomobject]$policies;Configurations=[pscustomobject]$configurations}
}
function Get-WelaSmbRuntimePlan {
param($State)
foreach($definition in Get-WelaSmbAuditDefinitions) {
$id="$($definition.Component)/$($definition.Name)"
$policy=$State.Policies.$id.Policy
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
$value=$State.Configurations.$side.($definition.Name).Value
$compatible=($policy.ValueExists -is [bool] -and -not $policy.ValueExists) -or
($policy.ValueExists -eq $true -and $policy.Type -ceq 'DWord' -and
($policy.Value -is [int] -or $policy.Value -is [long] -or $policy.Value -is [uint32]) -and $policy.Value -eq 1)
[pscustomobject][ordered]@{Id=$id;Side=$side;Name=$definition.Name;Before=$value;Desired=$true;Policy=$policy
Status=$(if(-not $compatible){'BlockedPolicy'}elseif($value){'AlreadyActive'}else{'ActivationRequired'})
Diagnostic=$(if(-not $compatible){'Existing policy is not absent or DWORD 1; review its authority. It will not be overwritten.'}elseif($policy.ValueExists){'Policy DWORD 1 and runtime Boolean are separate observations.'}else{'Policy value is absent; explicit activation changes native local configuration only.'})}
}
}
function Set-WelaSmbRuntimeFlag {
param([string]$Id)
$matches=@(Get-WelaSmbAuditDefinitions | Where-Object {"$($_.Component)/$($_.Name)" -ceq $Id})
if($matches.Count -ne 1){throw 'Unknown SMB audit switch.'}
$definition=$matches[0]
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
$command="SmbShare\Set-Smb${side}Configuration"
$parameters=@{Confirm=$false;Force=$true;ErrorAction='Stop'}
$parameters[$definition.Name]=$true
$null=& $command @parameters
}
function Write-WelaSmbRuntimeReceipt {
param([string]$Root,[string]$Name,$Value)
if($Name -notmatch '^(plan|result|[1-6]-(pending|confirmed))\.json$'){throw 'Unexpected receipt filename.'}
$null=Resolve-WelaArrivalPath $Root
$path=Join-Path $Root $Name
$bytes=[Text.UTF8Encoding]::new($false).GetBytes((Get-WelaSmbRuntimeKey $Value))
if($bytes.Length -gt 4MB){throw 'SMB activation receipt exceeds bound.'}
$stream=[IO.File]::Open($path,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)}finally{$stream.Dispose()}
$expected=Get-WelaArrivalHash $bytes
if((Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant() -cne $expected){throw 'SMB receipt readback differs.'}
[pscustomobject]@{Name=$Name;Bytes=$bytes.Length;Sha256=$expected}
}
function Invoke-WelaSmbRuntimeActivation {
param([ValidateSet('Plan','Activate')][string]$Action='Plan',[string]$OutputPath,[switch]$Auto,[switch]$DryRun)
if($DryRun -and $Action -ne 'Activate'){throw 'DryRun requires SmbRuntimeAction Activate.'}
if($Action -eq 'Plan' -and ($Auto -or $OutputPath)){throw 'Plan reads only; Auto and OutputPath apply to Activate.'}
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaSmbRuntimeActivation';Action=$Action;DryRun=[bool]$DryRun;RecordedUtc=[DateTime]::UtcNow.ToString('o')
Status='Unverified';ExitCode=1;Before=$null;After=$null;Controls=@();Results=@();Artifacts=@();OutputPath=$null;Diagnostic=''
VerificationScope='Native local audit switches at the recorded observations; policy authority and persistence are unknown';ReadyRuleCredit=0;EventGeneration='Not tested';Forwarding='Not tested'}
try {
$state=Get-WelaSmbRuntimeState;$report.Before=$state
$report.Controls=@(Get-WelaSmbRuntimePlan $state)
if(@($report.Controls | Where-Object Status -eq BlockedPolicy).Count){throw 'One or more policy values conflict or are malformed. No audit flags were changed.'}
if($Action -eq 'Plan' -or $DryRun){$report.Status=if($DryRun){'DryRun'}else{'Planned'};$report.ExitCode=0;return $report}
if(-not $OutputPath){throw 'Activate requires a new SmbRuntimeOutputPath on a local fixed drive.'}
$output=New-WelaArrivalOutput -Path $OutputPath -SourcePath $script:ScriptRoot;$report.OutputPath=$output
$report.Artifacts+=Write-WelaSmbRuntimeReceipt $output 'plan.json' ([pscustomobject]@{State=$state;Controls=$report.Controls})
$expectedKey=Get-WelaSmbRuntimeKey $state
$index=0;$stopped=$false
foreach($control in $report.Controls) {
$index++
$row=[pscustomobject][ordered]@{Id=$control.Id;Before=$control.Before;After=$null;Status='Skipped';Diagnostic='';PendingReceipt=$null;ConfirmedReceipt=$null}
$report.Results+= $row
if($stopped){$row.Diagnostic='A prior activation failed; no further changes were attempted.';continue}
try {
$fresh=Get-WelaSmbRuntimeState
if((Get-WelaSmbRuntimeKey $fresh) -cne $expectedKey){throw 'Host, source, policy or native configuration drifted after the snapshot.'}
if($control.Before){$row.After=$true;$row.Status='AlreadyActive';continue}
if(-not $Auto -and (Read-Host "Activate only SMB audit flag $($control.Id)? (y/N)") -cnotin @('y','Y')){$row.Diagnostic='Declined by operator.';continue}
$row.PendingReceipt=Write-WelaSmbRuntimeReceipt $output "$index-pending.json" ([pscustomobject]@{Kind='Pending';Id=$control.Id;Before=$fresh;Desired=$true;RecordedUtc=[DateTime]::UtcNow.ToString('o')})
# Re-read after interaction and durable intent, immediately before the setter.
if((Get-WelaSmbRuntimeKey (Get-WelaSmbRuntimeState)) -cne $expectedKey){throw 'Context drifted before the native setter; activation refused.'}
Set-WelaSmbRuntimeFlag -Id $control.Id
$after=Get-WelaSmbRuntimeState;$row.After=$after.Configurations.($control.Side).($control.Name).Value
# The only permitted delta is this one Boolean. All policies and every
# other native configuration property (including security) must match.
$next=Get-WelaSmbRuntimeKey $fresh | ConvertFrom-Json
$next.Configurations.($control.Side).($control.Name).Value=$true
if((Get-WelaSmbRuntimeKey $after) -cne (Get-WelaSmbRuntimeKey $next)){throw 'Native readback did not show exactly the requested audit-only delta.'}
$row.ConfirmedReceipt=Write-WelaSmbRuntimeReceipt $output "$index-confirmed.json" ([pscustomobject]@{Kind='Confirmed';Id=$control.Id;Pending=$row.PendingReceipt;After=$after;RecordedUtc=[DateTime]::UtcNow.ToString('o')})
$state=$after;$expectedKey=Get-WelaSmbRuntimeKey $state
$row.Status='Activated';$row.Diagnostic='Native Boolean True observed; policy tuple and all other configuration properties preserved.'
}catch{$row.Status='Failed';$row.Diagnostic=$_.Exception.Message;$stopped=$true}
}
$report.After=Get-WelaSmbRuntimeState
if((Get-WelaSmbRuntimeKey $report.After) -cne $expectedKey){throw 'Final context differs from the last verified configuration. Review partial receipts; no automatic rollback is attempted.'}
if(@($report.Results | Where-Object Status -notin @('Activated','AlreadyActive')).Count){throw 'Some flags were not activated. Inspect per-control results and receipts.'}
$report.Status='RuntimeAuditingActive';$report.ExitCode=0
}catch{$report.Diagnostic=$_.Exception.Message}
if($report.OutputPath){$null=Write-WelaSmbRuntimeReceipt $report.OutputPath 'result.json' $report}
$report
}
+139
View File
@@ -0,0 +1,139 @@
# Reviewed, existing-only Enabled changes; runtime observations are separate evidence.
function Initialize-WelaWecStateNative {
$path=Join-Path $PSScriptRoot 'WecStateNative.cs';$hash=(Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash
if(-not('Wela.WecState.Edit' -as [type])){Add-Type -Path $path -ErrorAction Stop;$script:WelaWecStateNativeHash=$hash}
if($script:WelaWecStateNativeHash -cne $hash){throw 'Loaded native state setter differs from source; start a fresh process.'}
}
function Get-WelaWecStateContext {
$context=Get-WelaWecUpdateContext
Initialize-WelaWecStateNative
$identity=[Security.Principal.WindowsIdentity]::GetCurrent()
try {$context.Reader=[ordered]@{Name=$identity.Name;Sid=$identity.User.Value;AuthenticationType=$identity.AuthenticationType;ImpersonationLevel=[string]$identity.ImpersonationLevel;Groups=@($identity.Groups.Value|Sort-Object);TokenStatistics=[Wela.WecState.Edit]::TokenKey($identity.Token)}}finally{$identity.Dispose()}
$channel=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('ForwardedEvents')
try {$context|Add-Member NoteProperty DestinationLog ([ordered]@{Name=$channel.LogName;Enabled=$channel.IsEnabled;Mode=[string]$channel.LogMode;MaximumBytes=$channel.MaximumSizeInBytes;Path=$channel.LogFilePath;SecurityDescriptor=$channel.SecurityDescriptor})}finally{$channel.Dispose()}
$context
}
function Get-WelaWecStateReviewKey {
param($Context)
# Separate CLI invocations can hold different token objects in the same logon.
# Bind plan/apply to the actual logon, and compare complete token statistics
# within each operation to reject privilege or token changes during writes.
$copy=$Context|ConvertTo-Json -Depth 16 -Compress|ConvertFrom-Json
$copy.Reader.TokenStatistics=$Context.Reader.TokenStatistics.Substring(16,16)
$copy|ConvertTo-Json -Depth 16 -Compress
}
function Get-WelaWecStateSources {
$root=Split-Path $PSScriptRoot -Parent;$sources=[ordered]@{}
foreach($name in @('scripts/WecState.ps1','scripts/WecStateNative.cs','scripts/WecUpdate.ps1','modules/WefSubscriptions.psm1','modules/WecSubscriptionXml.cs','scripts/Configuration.ps1','scripts/ControlApplicability.ps1','scripts/WefArrival.ps1','modules/AuditProfiles.psm1','scripts/WecRuntime.ps1','scripts/WecRuntimeNative.cs')){$sources[$name]=(Get-FileHash -LiteralPath (Join-Path $root $name) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
$sources|ConvertTo-Json -Compress
}
function Get-WelaWecStateDefinition {
param([string]$Xml,[string[]]$SourceSids)
$model=ConvertFrom-WelaWefSubscription -Xml $Xml -SourceSids $SourceSids -Observed
$doc=Read-WelaWefXml $Xml;$root=$doc.DocumentElement;$whole=Get-WelaWefXmlKey $root
$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('s',$root.NamespaceURI)
$null=$root.RemoveChild($root.SelectSingleNode('s:Enabled',$ns))
[pscustomobject]@{Id=$model.Id;Xml=$Xml;WholeKey=$whole;PreservedKey=(Get-WelaWefXmlKey $root);Enabled=$model.Definition.Enabled;QueryKey=$model.Query.Key;Description=$model.Definition.Description;SourceAuthorization=$model.Definition.SourceAuthorization}
}
function Read-WelaWecStateDefinition {
param([string]$Id,[string[]]$SourceSids)
if($Id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$'){throw 'Invalid exact subscription ID.'}
$definition=Get-WelaWecStateDefinition (Read-WelaWecSubscriptionXml $Id) $SourceSids
if($definition.Id -cne $Id){throw 'Native subscription identity differs from the selected ID.'}
$definition
}
function New-WelaWecStateEdit {
param($Before)
Initialize-WelaWecStateNative
$edit=[Wela.WecState.Edit]::new($Before.Id)
try {
if($edit.OriginalEnabled -ne $Before.Enabled -or (ConvertFrom-WelaWefQuery $edit.OriginalQuery).Key -cne $Before.QueryKey -or $edit.OriginalDescription -cne $Before.Description -or $edit.OriginalAuthorization -cne $Before.SourceAuthorization){throw 'Native handle state differs from the reviewed definition.'}
$edit
}catch{$edit.Dispose();throw}
}
function Assert-WelaWecStatePlan {
param($Plan)
Assert-WelaArrivalObject $Plan @('SchemaVersion','Kind','Id','SourceSids','ContextKey','Sources','BeforeXml','DesiredEnabled','RecordedUtc')
if(($Plan.SchemaVersion -isnot [int] -and $Plan.SchemaVersion -isnot [long]) -or $Plan.SchemaVersion -ne 1 -or $Plan.Kind -cne 'WelaWecStatePlan' -or $Plan.Id -isnot [string] -or $Plan.Id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$' -or $Plan.SourceSids -isnot [array] -or $Plan.ContextKey -isnot [string] -or $Plan.Sources -isnot [string] -or $Plan.BeforeXml -isnot [string] -or $Plan.DesiredEnabled -isnot [bool]){throw 'Unknown or mistyped state plan.'}
$null=ConvertTo-WelaArrivalUtc $Plan.RecordedUtc
foreach($sid in $Plan.SourceSids){if($sid -isnot [string]){throw 'Source SID must be a string.'}}
$null=Get-WelaWefAuthorization $Plan.SourceSids
$before=Get-WelaWecStateDefinition $Plan.BeforeXml $Plan.SourceSids
if($before.Id -cne $Plan.Id){throw 'Plan identity contradicts its original subscription.'}
}
function Read-WelaWecStateRuntime {
param([string]$Id)
try {Get-WelaWecRuntime -Id $Id -MaximumSources 32}
catch {[pscustomobject]@{Status='Unknown';Diagnostic=$_.Exception.Message;ReadyRuleCredit=0}}
}
function Assert-WelaWecStateArtifacts {
param([string]$Root,$Artifacts)
foreach($artifact in $Artifacts){if((Get-FileHash -LiteralPath (Join-Path $Root $artifact.Name) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'Saved state evidence changed before completion.'}}
}
function Invoke-WelaWecState {
param([ValidateSet('Plan','Apply')][string]$Action='Plan',[string]$Id,[string[]]$SourceSids,[ValidateSet('Enabled','Disabled')][string]$State,[string]$PlanPath,[string]$PlanHash,[Parameter(Mandatory)][string]$OutputPath)
$ErrorActionPreference='Stop'
if($Action -eq 'Plan'){
if(-not $Id -or -not $SourceSids -or -not $State -or $PlanPath -or $PlanHash){throw 'Plan requires exact ID, explicit source SIDs, Enabled or Disabled state and new output; no prior plan.'}
$sourceInput=$null;$sourcePath=Join-Path (Split-Path $PSScriptRoot -Parent) 'scripts'
}else{
if(-not $PlanPath -or $PlanHash -cnotmatch '^[a-f0-9]{64}$' -or $Id -or $SourceSids -or $State){throw 'Apply accepts only a reviewed plan path, its SHA256 and new output.'}
$sourceInput=Read-WelaWecUpdateFile $PlanPath;$sourcePath=$sourceInput.Path
}
$output=New-WelaArrivalOutput $OutputPath $sourcePath
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaWecState';Action=$Action;Status='Refused';ExitCode=1;RecordedUtc=[DateTime]::UtcNow.ToString('o');OutputPath=$output;PlanHash=$null;BeforeEnabled=$null;DesiredEnabled=$null;NativeSaveAttempted=$false;NativeErrorCode=$null;After=$null;RuntimeBefore=$null;RuntimeAfter=$null;Artifacts=@();Diagnostic='';ReadyRuleCredit=0;Delivery='Not established';BookmarkContinuity='Not established';Scope='Only Enabled on one existing native source-initiated subscription. Disable interrupts collection; enable/save activates it. No listener, firewall, service or authorization changes. Sysmon excluded.'}
$edit=$null;$plan=$null;$before=$null
try {
$context=Get-WelaWecStateContext;$contextKey=$context|ConvertTo-Json -Depth 16 -Compress;$sources=Get-WelaWecStateSources
if($Action -eq 'Plan'){
$before=Read-WelaWecStateDefinition $Id $SourceSids
$plan=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaWecStatePlan';Id=$Id;SourceSids=@($SourceSids);ContextKey=(Get-WelaWecStateReviewKey $context);Sources=$sources;BeforeXml=$before.Xml;DesiredEnabled=($State -eq 'Enabled');RecordedUtc=[DateTime]::UtcNow.ToString('o')}
Assert-WelaWecStatePlan $plan
if($plan.DesiredEnabled -and -not $context.DestinationLog.Enabled){throw 'ForwardedEvents must already be enabled before planning activation; no channel changes are made.'}
$report.RuntimeBefore=Read-WelaWecStateRuntime $Id
if((Read-WelaWecStateDefinition $Id $SourceSids).WholeKey -cne $before.WholeKey -or ((Get-WelaWecStateContext|ConvertTo-Json -Depth 16 -Compress) -cne $contextKey) -or (Get-WelaWecStateSources) -cne $sources){throw 'Host, reader, implementation or subscription drift during planning.'}
$planText=$plan|ConvertTo-Json -Depth 20
if([Text.Encoding]::UTF8.GetByteCount($planText) -gt 4194304){throw 'Reviewed plan exceeds the four-MiB apply limit.'}
$artifact=Write-WelaWecUpdateArtifact $output 'plan.json' $planText;$report.Artifacts+=$artifact;$report.PlanHash=$artifact.Sha256;$report.Status='ReviewRequired'
}else{
if($sourceInput.Hash -cne $PlanHash){throw 'Reviewed plan hash differs from the selected file bytes.'}
$plan=ConvertFrom-WelaArrivalJson $sourceInput.Text;Assert-WelaWecStatePlan $plan;$report.PlanHash=$sourceInput.Hash
if($plan.DesiredEnabled -and -not $context.DestinationLog.Enabled){throw 'ForwardedEvents must already be enabled before activation; no channel changes are made.'}
if($plan.ContextKey -cne (Get-WelaWecStateReviewKey $context) -or $plan.Sources -cne $sources){throw 'Actual host/reader/token/service or implementation sources differ from the reviewed plan.'}
$before=Get-WelaWecStateDefinition $plan.BeforeXml $plan.SourceSids
$report.BeforeEnabled=$before.Enabled;$report.DesiredEnabled=$plan.DesiredEnabled
$report.RuntimeBefore=Read-WelaWecStateRuntime $plan.Id
if((Read-WelaWecStateDefinition $plan.Id $plan.SourceSids).WholeKey -cne $before.WholeKey){throw 'Current subscription differs from the reviewed complete definition.'}
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'reviewed-plan.json' $sourceInput.Text
if($before.Enabled -eq $plan.DesiredEnabled){$report.Status='AlreadyMatches'}else{
$edit=New-WelaWecStateEdit $before
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'before-save.json' ([ordered]@{Status='Pending';RecordedUtc=[DateTime]::UtcNow.ToString('o');Context=$context;BeforeXml=$before.Xml;DesiredEnabled=$plan.DesiredEnabled;PlanHash=$sourceInput.Hash}|ConvertTo-Json -Depth 20)
Assert-WelaWecStateArtifacts $output $report.Artifacts
if((Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash -or (Get-WelaWecStateSources) -cne $sources -or ((Get-WelaWecStateContext|ConvertTo-Json -Depth 16 -Compress) -cne $contextKey) -or (Read-WelaWecStateDefinition $plan.Id $plan.SourceSids).WholeKey -cne $before.WholeKey){throw 'Plan, code, context or complete subscription changed immediately before save.'}
try {$edit.Save($plan.DesiredEnabled)}finally{$report.NativeSaveAttempted=[bool]$edit.SaveAttempted}
$report.Status='SavedAwaitingReadback'
}
$report.RuntimeAfter=Read-WelaWecStateRuntime $plan.Id
$after=Read-WelaWecStateDefinition $plan.Id $plan.SourceSids;$report.After=$after
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'after.xml' $after.Xml
if($after.Enabled -ne $plan.DesiredEnabled -or $after.PreservedKey -cne $before.PreservedKey -or ((Get-WelaWecStateContext|ConvertTo-Json -Depth 16 -Compress) -cne $contextKey) -or (Get-WelaWecStateSources) -cne $sources -or (Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash){throw 'Readback, preserved configuration, context, plan or implementation differs after operation.'}
if($report.NativeSaveAttempted){$report.Status='StateChangedAndVerified'}
}
$report.BeforeEnabled=$before.Enabled;$report.DesiredEnabled=$plan.DesiredEnabled
Assert-WelaWecStateArtifacts $output $report.Artifacts
$report.ExitCode=0
}catch{
$report.Status=if($report.NativeSaveAttempted){'SaveAttemptedUnverified'}else{'Refused'};$report.ExitCode=1;$report.Diagnostic=$_.Exception.Message
$errorObject=$_.Exception
while($errorObject){if($errorObject -is [ComponentModel.Win32Exception]){$report.NativeErrorCode=$errorObject.NativeErrorCode;break};$errorObject=$errorObject.InnerException}
if($report.NativeSaveAttempted -and $plan){
# A failed activation can still persist Enabled. Never imply rollback.
$report.RuntimeAfter=Read-WelaWecStateRuntime $plan.Id
try {$report.After=Read-WelaWecStateDefinition $plan.Id $plan.SourceSids;$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'failed-after.xml' $report.After.Xml}
catch {$report.Diagnostic+=' Final definition unavailable: '+$_.Exception.Message}
}
}
finally{if($edit){$edit.Dispose()}}
$null=Write-WelaWecUpdateArtifact $output 'manifest.json' ($report|ConvertTo-Json -Depth 32)
$report
}
+72
View File
@@ -0,0 +1,72 @@
// Existing-only native WEC Enabled setter. No create/delete or other setters.
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
namespace Wela.WecState {
public sealed class Edit : IDisposable {
[StructLayout(LayoutKind.Explicit, Size=16)] struct Variant {
[FieldOffset(0)] public int Boolean; [FieldOffset(8)] public uint Count; [FieldOffset(12)] public uint Type;
}
[DllImport("wecapi.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern IntPtr EcOpenSubscription(string name,uint access,uint flags);
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcGetSubscriptionProperty(IntPtr handle,int property,uint flags,uint size,IntPtr value,out uint used);
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcSetSubscriptionProperty(IntPtr handle,int property,uint flags,ref Variant value);
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcSaveSubscription(IntPtr handle,uint flags);
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcClose(IntPtr handle);
[DllImport("advapi32.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool GetTokenInformation(IntPtr token,int information,IntPtr buffer,int size,out int used);
IntPtr handle; readonly string name,oldQuery,oldDescription,oldAuthorization; readonly bool oldEnabled;
public bool OriginalEnabled {get{return oldEnabled;}}
public string OriginalQuery {get{return oldQuery;}}
public string OriginalDescription {get{return oldDescription;}}
public string OriginalAuthorization {get{return oldAuthorization;}}
public bool SaveAttempted {get;private set;}
// TOKEN_STATISTICS: TokenId, AuthenticationId and ModifiedId, plus token type.
public static string TokenKey(IntPtr token) {
IntPtr buffer=Marshal.AllocHGlobal(56);
try {int used;if(!GetTokenInformation(token,10,buffer,56,out used))throw new Win32Exception(Marshal.GetLastWin32Error());if(used!=56)throw new InvalidOperationException("Unexpected TOKEN_STATISTICS size.");
byte[] bytes=new byte[56];Marshal.Copy(buffer,bytes,0,bytes.Length);return BitConverter.ToString(bytes).Replace("-","");
}finally{Marshal.FreeHGlobal(buffer);}
}
static object Read(IntPtr h,int property) {
uint size=16;
for(int attempt=0;attempt<3;attempt++) {
IntPtr buffer=Marshal.AllocHGlobal((int)size);
try {
uint used;bool ok=EcGetSubscriptionProperty(h,property,0,size,buffer,out used);int error=Marshal.GetLastWin32Error();
if(!ok){if(error!=122)throw new Win32Exception(error);if(used<=size||used>1048576)throw new InvalidOperationException("Invalid native property buffer size.");size=used;continue;}
if(used<16||used>size)throw new InvalidOperationException("Invalid native property length.");
int type=Marshal.ReadInt32(buffer,12);
if(property==0){if(type!=1)throw new InvalidOperationException("Enabled is not a scalar Boolean.");int value=Marshal.ReadInt32(buffer);if(value!=0&&value!=1)throw new InvalidOperationException("Invalid native Boolean.");return value==1;}
if(type==0&&property==6)return "";
if(type!=4)throw new InvalidOperationException("Expected scalar native string.");
IntPtr pointer=Marshal.ReadIntPtr(buffer);long offset=pointer.ToInt64()-buffer.ToInt64();
if(pointer==IntPtr.Zero||offset<16||offset>used-2)throw new InvalidOperationException("Native string pointer is outside its buffer.");
StringBuilder text=new StringBuilder();
for(int i=0;i<524288&&offset+2L*i+2<=used;i++){char c=(char)(ushort)Marshal.ReadInt16(pointer,2*i);if(c==0)return text.ToString();text.Append(c);}
throw new InvalidOperationException("Unterminated native string.");
}finally{Marshal.FreeHGlobal(buffer);}
}
throw new InvalidOperationException("Native property changed repeatedly.");
}
void Check(IntPtr h) {
if((bool)Read(h,0)!=oldEnabled||!String.Equals((string)Read(h,10),oldQuery,StringComparison.Ordinal)||!String.Equals((string)Read(h,6),oldDescription,StringComparison.Ordinal)||!String.Equals((string)Read(h,31),oldAuthorization,StringComparison.Ordinal))throw new InvalidOperationException("Native enabled/query/description/authorization changed since review.");
}
public Edit(string id) {
if(String.IsNullOrWhiteSpace(id)||id.Length>128||id.IndexOf('\0')>=0)throw new ArgumentException("Invalid subscription ID.");
name=id;handle=EcOpenSubscription(name,3,2);if(handle==IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error());
try{oldEnabled=(bool)Read(handle,0);oldQuery=(string)Read(handle,10);oldDescription=(string)Read(handle,6);oldAuthorization=(string)Read(handle,31);}catch{Dispose();throw;}
}
public void Save(bool enabled) {
if(handle==IntPtr.Zero)throw new ObjectDisposedException("Edit");
if(SaveAttempted)throw new InvalidOperationException("A native edit may be saved only once.");
if(enabled==oldEnabled)throw new InvalidOperationException("Idempotent state must not save or reactivate a subscription.");
IntPtr fresh=EcOpenSubscription(name,1,2);if(fresh==IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error());
try{Check(fresh);}finally{EcClose(fresh);}
Variant value=new Variant{Boolean=enabled?1:0,Count=0,Type=1};
if(!EcSetSubscriptionProperty(handle,0,0,ref value))throw new Win32Exception(Marshal.GetLastWin32Error());
SaveAttempted=true;
if(!EcSaveSubscription(handle,0))throw new Win32Exception(Marshal.GetLastWin32Error());
}
public void Dispose(){if(handle!=IntPtr.Zero){EcClose(handle);handle=IntPtr.Zero;}}
}
}