diff --git a/docs/powershell-logging.md b/docs/powershell-logging.md index 0250e258..e81fe981 100644 --- a/docs/powershell-logging.md +++ b/docs/powershell-logging.md @@ -22,7 +22,7 @@ -Auto -BackupPath C:\WELA-Recovery\new-run -ResultsPath new-result.json ``` -Use a native 64-bit Windows PowerShell 5.1 or PowerShell 7 host. The actual Windows role, build, patch and installed Windows PowerShell engine are read; caller role/build overrides are refused. Reviewed families are Windows 11 builds 22000, 22621, 22631, 26100 and 26200, and Server 2022/2025 builds 20348/26100. Native client, joined-member, DC and AD CS acceptance remains separate from hosted standalone-server tests. `Winmgmt` and `EventLog` must already run; the command starts no service. Configure requires elevation and a fresh recovery directory. Plan and Configure require explicit controls; there is no implicit enable-everything selection. Audit without selection inventories existing policy only. +Use a native 64-bit Windows PowerShell 5.1 or PowerShell 7 host. The actual process SID, Windows role, build, patch and installed Windows PowerShell engine are read; impersonated callers are refused and caller role/build overrides are refused. Reviewed families are Windows 11 builds 22000, 22621, 22631, 26100 and 26200, and Server 2022/2025 builds 20348/26100. Native client, joined-member, DC and AD CS acceptance remains separate from hosted standalone-server tests. `Winmgmt` and `EventLog` must already run; the command starts no service. Configure requires elevation and a fresh recovery directory. Plan and Configure require explicit controls; there is no implicit enable-everything selection. Audit without selection inventories existing policy only. The array examples are PowerShell syntax. When launching `powershell.exe -File` from another shell, use a reviewed PowerShell wrapper to bind multiple array elements correctly. Literal module names can contain ASCII letters, digits, dots, underscores and hyphens, up to 128 characters each. Up to 32 unique names can be selected. Paths and wildcard patterns are refused; the exact `*` value is accepted **only when explicitly supplied** to request all modules. Installation and execution of arbitrary selected modules are not performed or asserted. diff --git a/scripts/PowerShellLogging.ps1 b/scripts/PowerShellLogging.ps1 index e16e33e9..ff9cc361 100644 --- a/scripts/PowerShellLogging.ps1 +++ b/scripts/PowerShellLogging.ps1 @@ -43,6 +43,8 @@ function Get-WelaPsLoggingSources { function Get-WelaPsLoggingSnapshot { if ($env:OS -ne 'Windows_NT' -or -not [Environment]::Is64BitProcess) {throw 'Use native 64-bit PowerShell on Windows.'} foreach($service in @('Winmgmt','EventLog')) {if((Get-Service -Name $service -ErrorAction Stop).Status -ne 'Running'){throw "$service must already be running; no service is started."}} + $identity=[Security.Principal.WindowsIdentity]::GetCurrent() + try {if(-not $identity.User -or $identity.ImpersonationLevel -ne [Security.Principal.TokenImpersonationLevel]::None){throw 'An actual non-impersonated process identity is required.'};$operator=[pscustomobject]@{Sid=$identity.User.Value;ImpersonationLevel=[string]$identity.ImpersonationLevel}}finally{$identity.Dispose()} $os=Get-CimInstance Win32_OperatingSystem -Property BuildNumber,ProductType -ErrorAction Stop $computer=Get-CimInstance Win32_ComputerSystem -Property Name,Domain,DomainRole,PartOfDomain -ErrorAction Stop if ([string]$os.BuildNumber -notmatch '^\d+$' -or $computer.PartOfDomain -isnot [bool] -or $computer.DomainRole -notin @(0,1,2,3,4,5)) {throw 'Complete actual Windows role/build/join context is required.'} @@ -64,7 +66,7 @@ function Get-WelaPsLoggingSnapshot { try {$channel=Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational' -ErrorAction Stop;$channelState=[pscustomobject]@{Name=[string]$channel.LogName;Enabled=[bool]$channel.IsEnabled;MaximumBytes=[long]$channel.MaximumSizeInBytes;Mode=[string]$channel.LogMode;Security=[string]$channel.SecurityDescriptor}}finally{if($channel -is [IDisposable]){$channel.Dispose()}} $patch=Get-WelaRegistryState 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' UBR if (-not $patch.ValueExists -or $patch.Type -ne 'DWord' -or $patch.Value -lt 0) {throw 'Exact native patch evidence is required.'} - [pscustomobject][ordered]@{Host=[pscustomobject]@{Computer=[string]$computer.Name;Domain=[string]$computer.Domain;Build=$build;Patch=$patch.Value;ProductType=$product;DomainRole=$role;PartOfDomain=$joined;CertSvcPresent=[bool](Get-Service CertSvc -ErrorAction SilentlyContinue)};Engine=[pscustomobject]@{Target='Windows PowerShell 5.1';Version=$engine.Value;Path=$exe;Sha256=$engineHash;WelaHostVersion=$PSVersionTable.PSVersion.ToString()};Sources=@(Get-WelaPsLoggingSources);Machine=$machine;CurrentUser=$user;PowerShellCoreMachine=$coreMachine;PowerShellCoreUser=$coreUser;ProtectedEventLogging=$protected;Channel=$channelState} + [pscustomobject][ordered]@{Operator=$operator;Host=[pscustomobject]@{Computer=[string]$computer.Name;Domain=[string]$computer.Domain;Build=$build;Patch=$patch.Value;ProductType=$product;DomainRole=$role;PartOfDomain=$joined;CertSvcPresent=[bool](Get-Service CertSvc -ErrorAction SilentlyContinue)};Engine=[pscustomobject]@{Target='Windows PowerShell 5.1';Version=$engine.Value;Path=$exe;Sha256=$engineHash;WelaHostVersion=$PSVersionTable.PSVersion.ToString()};Sources=@(Get-WelaPsLoggingSources);Machine=$machine;CurrentUser=$user;PowerShellCoreMachine=$coreMachine;PowerShellCoreUser=$coreUser;ProtectedEventLogging=$protected;Channel=$channelState} } function Get-WelaPsLoggingValue { param($Tree,[string]$Path,[string]$Name) diff --git a/tests/PowerShellLogging.Tests.ps1 b/tests/PowerShellLogging.Tests.ps1 index 49c2804c..3907ea69 100644 --- a/tests/PowerShellLogging.Tests.ps1 +++ b/tests/PowerShellLogging.Tests.ps1 @@ -7,7 +7,7 @@ function Reject([scriptblock]$Action,[string]$Pattern){$message='';try{&$Action} function CloneFixture($Value){ConvertTo-WelaPsLoggingKey $Value|ConvertFrom-Json} function Row($Path,$Values=@(),$Children=@()){[pscustomobject]@{Path=$Path;Values=@($Values);Children=@($Children);Access='original-acl'}} function Fixture { - [pscustomobject][ordered]@{Host=[pscustomobject]@{Computer='fixture';Build=20348};Engine=[pscustomobject]@{Target='Windows PowerShell 5.1'};Sources=@('sha');Machine=[pscustomobject]@{Exists=$true;Keys=@((Row '' @() @('ModuleLogging','ScriptBlockLogging','Transcription')),(Row 'ModuleLogging' @([pscustomobject]@{Name='EnableModuleLogging';Type='DWord';Value=0}) @('ModuleNames')),(Row 'ModuleLogging\ModuleNames' @([pscustomobject]@{Name='existing';Type='String';Value='Existing.Module'})),(Row 'ScriptBlockLogging' @([pscustomobject]@{Name='EnableScriptBlockLogging';Type='DWord';Value=0},[pscustomobject]@{Name='EnableScriptBlockInvocationLogging';Type='DWord';Value=1})),(Row 'Transcription' @([pscustomobject]@{Name='EnableTranscripting';Type='DWord';Value=1})))};CurrentUser=@('preserved-user');PowerShellCoreMachine=@('preserved-core');PowerShellCoreUser=@('preserved-core-user');ProtectedEventLogging=@('preserved-protected');Channel=@('preserved-channel')} + [pscustomobject][ordered]@{Operator=[pscustomobject]@{Sid='S-1-5-21-1';ImpersonationLevel='None'};Host=[pscustomobject]@{Computer='fixture';Build=20348};Engine=[pscustomobject]@{Target='Windows PowerShell 5.1'};Sources=@('sha');Machine=[pscustomobject]@{Exists=$true;Keys=@((Row '' @() @('ModuleLogging','ScriptBlockLogging','Transcription')),(Row 'ModuleLogging' @([pscustomobject]@{Name='EnableModuleLogging';Type='DWord';Value=0}) @('ModuleNames')),(Row 'ModuleLogging\ModuleNames' @([pscustomobject]@{Name='existing';Type='String';Value='Existing.Module'})),(Row 'ScriptBlockLogging' @([pscustomobject]@{Name='EnableScriptBlockLogging';Type='DWord';Value=0},[pscustomobject]@{Name='EnableScriptBlockInvocationLogging';Type='DWord';Value=1})),(Row 'Transcription' @([pscustomobject]@{Name='EnableTranscripting';Type='DWord';Value=1})))};CurrentUser=@('preserved-user');PowerShellCoreMachine=@('preserved-core');PowerShellCoreUser=@('preserved-core-user');ProtectedEventLogging=@('preserved-protected');Channel=@('preserved-channel')} } function Mutate($Before,$Definition){ $after=CloneFixture $Before;$after.Machine.Exists=$true @@ -61,7 +61,7 @@ try { Reset;$script:corrupt=$true;$failed=Invoke-WelaPowerShellLogging -Action Configure -Control Module,ScriptBlock -ModuleName Microsoft.PowerShell.Utility -Auto -BackupPath (Join-Path $root 'corrupt') Assert ($failed.ExitCode -eq 1 -and $script:writes -eq 1) 'Preservation failure stops later writes' Reset;$snapshot=CloneFixture $script:observed;$definition=$definitions[0];$after=Mutate $snapshot $definition;Assert-WelaPsLoggingTransition $snapshot $after $definition;Assert $true 'Exact additive transition accepted' - foreach($property in @('Host','Sources','CurrentUser','PowerShellCoreMachine','PowerShellCoreUser','ProtectedEventLogging','Channel')){$changed=CloneFixture $after;$changed.$property='drift';Reject {Assert-WelaPsLoggingTransition $snapshot $changed $definition} 'Unselected state'} + foreach($property in @('Operator','Host','Sources','CurrentUser','PowerShellCoreMachine','PowerShellCoreUser','ProtectedEventLogging','Channel')){$changed=CloneFixture $after;$changed.$property='drift';Reject {Assert-WelaPsLoggingTransition $snapshot $changed $definition} 'Unselected state'} $changed=CloneFixture $after;$changed.Machine.Keys[0].Access='new-acl';Reject {Assert-WelaPsLoggingTransition $snapshot $changed $definition} 'descriptor' $changed=CloneFixture $after;$changed.Machine.Keys=@($changed.Machine.Keys|Where-Object Path -ne Transcription);Reject {Assert-WelaPsLoggingTransition $snapshot $changed $definition} 'disappeared' $changed=CloneFixture $after;$changed.Machine.Keys+=Row 'extra';Reject {Assert-WelaPsLoggingTransition $snapshot $changed $definition} 'unrequested' diff --git a/tests/PowerShellLogging.Windows.Tests.ps1 b/tests/PowerShellLogging.Windows.Tests.ps1 index 46e6fac9..d6be351d 100644 --- a/tests/PowerShellLogging.Windows.Tests.ps1 +++ b/tests/PowerShellLogging.Windows.Tests.ps1 @@ -111,7 +111,7 @@ try{ $id=[int]$system.SelectSingleNode('e:EventID',$ns).InnerText if($system.SelectSingleNode('e:Provider',$ns).GetAttribute('Name') -cne 'Microsoft-Windows-PowerShell' -or $system.SelectSingleNode('e:Provider',$ns).GetAttribute('Guid').Trim('{}') -ine 'a0c1853b-5c40-4b15-8766-3cf1c58f985a' -or [int]$system.SelectSingleNode('e:Execution',$ns).GetAttribute('ProcessID') -ne $process.Pid -or $system.SelectSingleNode('e:Channel',$ns).InnerText -cne 'Microsoft-Windows-PowerShell/Operational' -or $system.SelectSingleNode('e:Computer',$ns).InnerText -ine $env:COMPUTERNAME){continue} $time=[DateTimeOffset]::Parse($system.SelectSingleNode('e:TimeCreated',$ns).GetAttribute('SystemTime')).UtcDateTime;if($time -lt [DateTimeOffset]::Parse($process.StartedUtc).UtcDateTime -or $time -gt [DateTimeOffset]::Parse($process.ExitedUtc).UtcDateTime){continue} - if($system.SelectSingleNode('e:Security',$ns).GetAttribute('UserID') -cne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value){continue} + if($system.SelectSingleNode('e:Security',$ns).GetAttribute('UserID') -cne $original.Operator.Sid){continue} if($id -eq 4104 -and $data.ScriptBlockText -ceq $workerText -and $data.Path -ieq $workerPath -and $data.MessageNumber -ceq '1' -and $data.MessageTotal -ceq '1' -and $data.ScriptBlockId -match '^[0-9a-f-]{36}$'){$selected['4104']=@($selected['4104'])+ $xml} if($id -eq 4103 -and $data.ContainsKey('Payload') -and $data.ContainsKey('ContextInfo') -and $data.Payload.Contains($nonce) -and $data.ContextInfo.Contains($workerPath)){$selected['4103']=@($selected['4103'])+ $xml} }