mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-08 15:25:24 +02:00
Merge dev and preserve independent recovery and WEC commands
This commit is contained in:
commit
e068bd8f52
31 files changed
+1563
-14
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;}}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user