mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-11-02 11:18:18 +01:00
Add fixed native AppLocker Script probe and disposable evidence matrix
This commit is contained in:
1 parent
203fdfc942
commit
8ac4c45653
8 files changed
+577
No files matched your search
@@ -0,0 +1,46 @@
|
||||
name: Native AppLocker Script probe
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
applocker-script-probe:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2022, windows-2025]
|
||||
engine: [powershell, pwsh]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
- name: Fixtures in Windows PowerShell 5.1
|
||||
if: matrix.engine == 'powershell'
|
||||
shell: powershell
|
||||
run: |
|
||||
./tests/AppLockerScriptProbe.Tests.ps1
|
||||
./tests/AppLockerScriptProbe.Cli.Tests.ps1
|
||||
- name: Native probe in Windows PowerShell 5.1
|
||||
if: matrix.engine == 'powershell'
|
||||
shell: powershell
|
||||
run: ./tests/AppLockerScriptProbe.Windows.Tests.ps1 -AllowDisposablePolicyWrite
|
||||
- name: Fixtures in PowerShell 7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: |
|
||||
./tests/AppLockerScriptProbe.Tests.ps1
|
||||
./tests/AppLockerScriptProbe.Cli.Tests.ps1
|
||||
- name: Native probe in PowerShell 7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: ./tests/AppLockerScriptProbe.Windows.Tests.ps1 -AllowDisposablePolicyWrite
|
||||
- name: Retain bounded native evidence and cleanup receipt
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: applocker-script-${{ matrix.os }}-${{ matrix.engine }}
|
||||
path: ${{ runner.temp }}/wela-applocker-script-native-*/**
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -144,6 +144,9 @@
|
||||
[switch]$AllowDnsTraceReset,
|
||||
[string[]]$WecRuntimeId,
|
||||
[ValidateRange(1,512)][int]$WecRuntimeMaximumSources=128,
|
||||
[ValidateSet('Plan','Run')][string]$AppLockerScriptAction = 'Plan',
|
||||
[string]$AppLockerScriptOutputPath,
|
||||
[ValidateRange(1,30)][int]$AppLockerScriptTimeoutSeconds = 15,
|
||||
[ValidateSet('Plan','Run')][string]$AppLockerProbeAction = 'Plan',
|
||||
[string]$AppLockerProbeOutputPath,
|
||||
[ValidateRange(1,30)][int]$AppLockerProbeTimeoutSeconds = 15,
|
||||
@@ -182,6 +185,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
|
||||
. (Join-Path $ScriptRoot "scripts/AdObjectSacl.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/AppLockerProbe.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/AppLockerScriptProbe.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/WmiNamespaceAuditing.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/WmiProbe.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/PowerShellTranscription.ps1")
|
||||
@@ -1989,6 +1993,7 @@ Usage:
|
||||
./WELA.ps1 wec-state -Help # Review enable/disable of one existing subscription
|
||||
./WELA.ps1 wec-update -Help # Review query/description updates on a disabled subscription
|
||||
./WELA.ps1 wmi-probe -Help # Fixed local read and matched namespace Security4662 evidence
|
||||
./WELA.ps1 applocker-script-probe -Help # Collect a fixed native Script8005/8006 event
|
||||
./WELA.ps1 applocker-probe -Help # Collect a fixed native AppLocker EXE event
|
||||
./WELA.ps1 wef-arrival -Help # Verify exact native probe presence on the local collector
|
||||
./WELA.ps1 native-validation -Help # Collect a fixed native 4688 probe without changing policy
|
||||
@@ -2079,6 +2084,8 @@ if ($Cmd -eq 'wec-runtime' -and @($PSBoundParameters.Keys | Where-Object {$_ -no
|
||||
}
|
||||
if ($Cmd -ne 'wmi-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WmiProbe*'}).Count) {throw 'WmiProbe options require wmi-probe.'}
|
||||
if ($Cmd -eq 'wmi-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WmiProbeAction','WmiProbeNamespace','WmiProbeOutputPath','WmiProbeTimeoutSeconds','Help')}).Count) {throw 'wmi-probe accepts only dedicated probe options.'}
|
||||
if ($Cmd -ne 'applocker-script-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'AppLockerScript*'}).Count) {throw 'AppLockerScript options require applocker-script-probe.'}
|
||||
if ($Cmd -eq 'applocker-script-probe' -and ($args.Count -or @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','AppLockerScriptAction','AppLockerScriptOutputPath','AppLockerScriptTimeoutSeconds','Help')}).Count)) {throw 'applocker-script-probe accepts only its dedicated options.'}
|
||||
if ($Cmd -ne 'applocker-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -in @('AppLockerProbeAction','AppLockerProbeOutputPath','AppLockerProbeTimeoutSeconds')}).Count) {throw 'AppLocker probe options require applocker-probe.'}
|
||||
if ($Cmd -eq 'applocker-probe' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','AppLockerProbeAction','AppLockerProbeOutputPath','AppLockerProbeTimeoutSeconds','Help')}).Count) {throw 'applocker-probe accepts only its dedicated options.'}
|
||||
if ($Cmd -ne 'wef-arrival' -and @($PSBoundParameters.Keys | Where-Object {$_ -in @('ArrivalProbePath','ArrivalOutputPath')}).Count) {
|
||||
@@ -2302,6 +2309,12 @@ switch ($Cmd.ToLower()) {
|
||||
$report
|
||||
if($report.ExitCode){exit $report.ExitCode}
|
||||
}
|
||||
'applocker-script-probe' {
|
||||
if ($Help) {Write-Host 'Usage: applocker-script-probe [-AppLockerScriptAction Plan|Run] [-AppLockerScriptOutputPath new-private-directory] [-AppLockerScriptTimeoutSeconds 1..30]. Requires existing Script AuditOnly policy, running AppIDSvc and enabled MSI and Script channel. Fixed native Windows PowerShell5.1 script, no policy changes or Sigma credit. See docs/applocker-script-probe.md.';return}
|
||||
$report=Invoke-WelaAppLockerScriptProbe -Action $AppLockerScriptAction -OutputPath $AppLockerScriptOutputPath -TimeoutSeconds $AppLockerScriptTimeoutSeconds
|
||||
$report
|
||||
if($report.ExitCode){exit $report.ExitCode}
|
||||
}
|
||||
'applocker-probe' {
|
||||
if ($Help) {Write-Host 'Usage: applocker-probe [-AppLockerProbeAction Plan|Run] [-AppLockerProbeOutputPath new-private-directory] [-AppLockerProbeTimeoutSeconds 1..30]. Requires existing EXE audit-only policy, running AppIDSvc and enabled channel. Run launches a fixed native cmd.exe copy and collects one exact AppLocker event. See docs/applocker-probe.md.';return}
|
||||
$report=Invoke-WelaAppLockerProbe -Action $AppLockerProbeAction -OutputPath $AppLockerProbeOutputPath -TimeoutSeconds $AppLockerProbeTimeoutSeconds
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Read-only process-token observations. No privilege or authorization changes.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
namespace Wela.AppLockerScript {
|
||||
public sealed class Group { public string Sid; public uint Attributes; }
|
||||
public sealed class Privilege { public string Luid; public uint Attributes; }
|
||||
public sealed class Token {
|
||||
public string Sid, Name, TokenId, ModifiedId, AuthenticationId, AuthenticationType, ImpersonationLevel, TokenSource;
|
||||
public Group[] Groups; public Privilege[] Privileges;
|
||||
}
|
||||
public static class Native {
|
||||
public const string SourceSha256="__WELA_SOURCE_SHA256__";
|
||||
[DllImport("kernel32.dll",ExactSpelling=true)] static extern void GetSystemTimePreciseAsFileTime(out long value);
|
||||
public static DateTime UtcNow() {long value;GetSystemTimePreciseAsFileTime(out value);return DateTime.FromFileTimeUtc(value);}
|
||||
[StructLayout(LayoutKind.Sequential)] struct Luid {public uint Low; public int High;}
|
||||
[StructLayout(LayoutKind.Sequential)] struct Statistics {public Luid TokenId,AuthenticationId;public long Expiration;public int Type,Level;public uint Charged,Available,Groups,Privileges;public Luid Modified;}
|
||||
[StructLayout(LayoutKind.Sequential)] struct SidAndAttributes {public IntPtr Sid;public uint Attributes;}
|
||||
[StructLayout(LayoutKind.Sequential)] struct TokenGroups {public uint Count;public SidAndAttributes First;}
|
||||
[StructLayout(LayoutKind.Sequential)] struct LuidAndAttributes {public Luid Luid;public uint Attributes;}
|
||||
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
||||
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentThread();
|
||||
[DllImport("kernel32.dll",SetLastError=true)] static extern bool CloseHandle(IntPtr h);
|
||||
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenProcessToken(IntPtr p,uint access,out IntPtr t);
|
||||
[DllImport("advapi32.dll",SetLastError=true)] static extern bool OpenThreadToken(IntPtr p,uint access,bool self,out IntPtr t);
|
||||
[DllImport("advapi32.dll",SetLastError=true)] static extern bool GetTokenInformation(IntPtr t,int cls,IntPtr data,int length,out int needed);
|
||||
static string Hex(Luid id) {return "0x"+(((ulong)(uint)id.High<<32)|id.Low).ToString("x");}
|
||||
static IntPtr Read(IntPtr token,int cls,out int length) {
|
||||
GetTokenInformation(token,cls,IntPtr.Zero,0,out length);
|
||||
if(Marshal.GetLastWin32Error()!=122||length<4||length>65536)throw new InvalidOperationException("Unknown or oversized token information.");
|
||||
IntPtr data=Marshal.AllocHGlobal(length);
|
||||
if(!GetTokenInformation(token,cls,data,length,out length)){int error=Marshal.GetLastWin32Error();Marshal.FreeHGlobal(data);throw new Win32Exception(error);}
|
||||
return data;
|
||||
}
|
||||
static Token ReadToken(IntPtr token,string source) {
|
||||
Token result=new Token();result.TokenSource=source;using(WindowsIdentity identity=new WindowsIdentity(token)){result.Sid=identity.User.Value;result.Name=identity.Name;result.AuthenticationType=identity.AuthenticationType;result.ImpersonationLevel=identity.ImpersonationLevel.ToString();}
|
||||
int length;IntPtr p=Read(token,10,out length);
|
||||
try {if(length<Marshal.SizeOf(typeof(Statistics)))throw new InvalidOperationException("Truncated token statistics.");Statistics stats=(Statistics)Marshal.PtrToStructure(p,typeof(Statistics));if(stats.Type!=1)throw new InvalidOperationException("A primary token is required.");result.AuthenticationId=Hex(stats.AuthenticationId);result.TokenId=Hex(stats.TokenId);result.ModifiedId=Hex(stats.Modified);}finally{Marshal.FreeHGlobal(p);}
|
||||
p=Read(token,2,out length);
|
||||
try {int count=Marshal.ReadInt32(p),offset=(int)Marshal.OffsetOf(typeof(TokenGroups),"First"),size=Marshal.SizeOf(typeof(SidAndAttributes));if(count<0||count>4096||offset+(long)count*size>length)throw new InvalidOperationException("Invalid token groups.");List<Group> groups=new List<Group>();for(int i=0;i<count;i++){SidAndAttributes g=(SidAndAttributes)Marshal.PtrToStructure(IntPtr.Add(p,offset+i*size),typeof(SidAndAttributes));groups.Add(new Group{Sid=new SecurityIdentifier(g.Sid).Value,Attributes=g.Attributes});}groups.Sort((a,b)=>String.CompareOrdinal(a.Sid,b.Sid));result.Groups=groups.ToArray();}finally{Marshal.FreeHGlobal(p);}
|
||||
p=Read(token,3,out length);
|
||||
try {int count=Marshal.ReadInt32(p),size=Marshal.SizeOf(typeof(LuidAndAttributes));if(count<0||count>4096||4+(long)count*size>length)throw new InvalidOperationException("Invalid token privileges.");List<Privilege> privileges=new List<Privilege>();for(int i=0;i<count;i++){LuidAndAttributes v=(LuidAndAttributes)Marshal.PtrToStructure(IntPtr.Add(p,4+i*size),typeof(LuidAndAttributes));privileges.Add(new Privilege{Luid=Hex(v.Luid),Attributes=v.Attributes});}privileges.Sort((a,b)=>String.CompareOrdinal(a.Luid,b.Luid));result.Privileges=privileges.ToArray();}finally{Marshal.FreeHGlobal(p);}
|
||||
return result;
|
||||
}
|
||||
public static Token Child(IntPtr handle) {
|
||||
IntPtr token=IntPtr.Zero;
|
||||
if(!OpenProcessToken(handle,8,out token))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try {if(IsTokenRestricted(token))throw new InvalidOperationException("Restricted child token unsupported.");return ReadToken(token,"ChildProcess");}finally{CloseHandle(token);}
|
||||
}
|
||||
[DllImport("advapi32.dll")] static extern bool IsTokenRestricted(IntPtr token);
|
||||
public static Token Snapshot() {
|
||||
IntPtr thread=IntPtr.Zero,process=IntPtr.Zero;
|
||||
if(OpenThreadToken(GetCurrentThread(),8,true,out thread)){CloseHandle(thread);throw new InvalidOperationException("Impersonated callers are unsupported.");}
|
||||
int error=Marshal.GetLastWin32Error();if(error!=1008)throw new Win32Exception(error);
|
||||
if(!OpenProcessToken(GetCurrentProcess(),8,out process))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try {if(IsTokenRestricted(process))throw new InvalidOperationException("Restricted caller unsupported.");return ReadToken(process,"Process");}finally{CloseHandle(process);}
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] struct FileInformation {
|
||||
public uint Attributes;public System.Runtime.InteropServices.ComTypes.FILETIME Creation,Access,Write;
|
||||
public uint Volume,SizeHigh,SizeLow,Links,IndexHigh,IndexLow;
|
||||
}
|
||||
[DllImport("kernel32.dll",SetLastError=true)] static extern bool GetFileInformationByHandle(IntPtr file,out FileInformation info);
|
||||
public static string FileId(IntPtr handle) {
|
||||
FileInformation info;if(!GetFileInformationByHandle(handle,out info))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
if((info.Attributes&0x410)!=0)throw new InvalidOperationException("One ordinary non-reparse file identity is required.");
|
||||
return info.Volume.ToString("x8")+":"+info.IndexHigh.ToString("x8")+info.IndexLow.ToString("x8");
|
||||
}
|
||||
public static async System.Threading.Tasks.Task<string> ReadLineBoundedAsync(System.IO.StreamReader reader,int limit) {
|
||||
char[] buffer=new char[1];System.Text.StringBuilder text=new System.Text.StringBuilder();
|
||||
while(true){int count=await reader.ReadAsync(buffer,0,1).ConfigureAwait(false);if(count==0)throw new InvalidOperationException("Owned child ended before its ready marker.");if(buffer[0]=='\n')return text.ToString().TrimEnd('\r');if(text.Length>=limit)throw new InvalidOperationException("Owned child line exceeds the bound.");text.Append(buffer[0]);}
|
||||
}
|
||||
public static async System.Threading.Tasks.Task<string> ReadBoundedAsync(System.IO.StreamReader reader,int limit) {
|
||||
char[] buffer=new char[256];System.Text.StringBuilder text=new System.Text.StringBuilder();
|
||||
while(true){int count=await reader.ReadAsync(buffer,0,buffer.Length).ConfigureAwait(false);if(count==0)return text.ToString();if(text.Length+count>limit)throw new InvalidOperationException("Owned child output exceeds the bound.");text.Append(buffer,0,count);}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
# One fixed native Windows PowerShell script. Never prepares policy/services/channels.
|
||||
function Get-WelaAppLockerScriptKey { param($Value) ConvertTo-Json -InputObject $Value -Depth 30 -Compress }
|
||||
function Get-WelaAppLockerScriptSources {
|
||||
$sources=[ordered]@{}
|
||||
foreach($path in @('WELA.ps1','scripts/AppLockerScriptProbe.ps1','scripts/AppLockerScriptNative.cs','scripts/AppLockerScriptWorker.ps1','scripts/AppLockerReadiness.ps1','scripts/WefArrival.ps1')) {
|
||||
$sources[$path]=(Get-FileHash -LiteralPath (Join-Path $script:ScriptRoot $path) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
|
||||
}
|
||||
[pscustomobject]$sources
|
||||
}
|
||||
function Initialize-WelaAppLockerScriptNative {
|
||||
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'A native 64-bit Windows process is required.'}
|
||||
$bytes=[IO.File]::ReadAllBytes((Join-Path $script:ScriptRoot 'scripts/AppLockerScriptNative.cs'))
|
||||
$hash=Get-WelaArrivalHash $bytes
|
||||
if(-not ('Wela.AppLockerScript.Native' -as [type])) {
|
||||
$text=[Text.UTF8Encoding]::new($false,$true).GetString($bytes).TrimStart([char]0xfeff)
|
||||
if(([regex]::Matches($text,'__WELA_SOURCE_SHA256__')).Count -ne 1){throw 'Unexpected native source fingerprint placeholder.'}
|
||||
Add-Type -TypeDefinition $text.Replace('__WELA_SOURCE_SHA256__',$hash) -ErrorAction Stop
|
||||
}
|
||||
if([Wela.AppLockerScript.Native]::SourceSha256 -cne $hash){throw 'Loaded helper differs from current source; start a fresh PowerShell process.'}
|
||||
}
|
||||
function Get-WelaAppLockerScriptReader { [Wela.AppLockerScript.Native]::Snapshot() }
|
||||
function Get-WelaAppLockerScriptUtcNow { [Wela.AppLockerScript.Native]::UtcNow() }
|
||||
function Get-WelaAppLockerScriptExecutionPolicy {
|
||||
$values=[ordered]@{InheritedProcessValue=$env:PSExecutionPolicyPreference;Machine=@();User=@()}
|
||||
foreach($scope in @('Machine','User')) {
|
||||
$base=if($scope -eq 'Machine'){[Microsoft.Win32.Registry]::LocalMachine}else{[Microsoft.Win32.Registry]::CurrentUser}
|
||||
foreach($path in @('SOFTWARE\Policies\Microsoft\Windows\PowerShell','SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell')) {
|
||||
$key=$base.OpenSubKey($path,$false)
|
||||
try {
|
||||
foreach($name in @('EnableScripts','ExecutionPolicy')) {
|
||||
$present=$null -ne $key -and $name -cin @($key.GetValueNames())
|
||||
$values[$scope]+=[pscustomobject]@{Path=$path;Name=$name;Present=[bool]$present;Kind=$(if($present){[string]$key.GetValueKind($name)}else{$null});Value=$(if($present){$key.GetValue($name,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}else{$null})}
|
||||
}
|
||||
}finally{if($key){$key.Dispose()}}
|
||||
}
|
||||
}
|
||||
[pscustomobject]$values
|
||||
}
|
||||
function Get-WelaAppLockerScriptState {
|
||||
$hostState=Get-WelaAppLockerHost
|
||||
$computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
|
||||
$version=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
|
||||
$machine=Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop
|
||||
$channel=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Microsoft-Windows-AppLocker/MSI and Script')
|
||||
try {$log=[ordered]@{Name=$channel.LogName;Enabled=$channel.IsEnabled;SecurityDescriptor=$channel.SecurityDescriptor;MaximumSize=$channel.MaximumSizeInBytes;Mode=[string]$channel.LogMode;LogFilePath=$channel.LogFilePath}}finally{$channel.Dispose()}
|
||||
$source=Resolve-WelaArrivalPath (Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell\v1.0\powershell.exe')
|
||||
[pscustomobject][ordered]@{Host=$hostState;MachineGuid=$machine.MachineGuid;UBR=$version.UBR;Computer=[Environment]::MachineName;Domain=[string]$computer.Domain;LocalPolicy=(Get-WelaAppLockerPolicySnapshot Local);EffectivePolicy=(Get-WelaAppLockerPolicySnapshot Effective);Management=(Get-WelaAppLockerManagement);Service=(Get-WelaAppLockerService);Channel=$log;ExecutionPolicy=(Get-WelaAppLockerScriptExecutionPolicy);Source=$source;SourceHash=(Get-FileHash -LiteralPath $source -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
|
||||
}
|
||||
function Get-WelaAppLockerScriptStateKey {
|
||||
param($State)
|
||||
if($State.Host.Status -cne 'Candidate' -or $State.Host.Is64BitProcess -isnot [bool] -or -not $State.Host.Is64BitProcess -or $State.Host.ProductType -notin @(1,3) -or ($State.Host.ProductType -eq 1 -and $State.Host.Build -notin @(22000,22621,22631,26100,26200)) -or ($State.Host.ProductType -eq 3 -and $State.Host.Build -notin @(20348,26100))){throw 'A reviewed native Windows 11 or Server 2022/2025 member host is required; DCs are excluded.'}
|
||||
foreach($policy in @($State.LocalPolicy,$State.EffectivePolicy)){if($policy.Status -cne 'Observed' -or $policy.Policy.HasUnknownPolicyData -isnot [bool] -or $policy.Policy.HasUnknownPolicyData){throw 'Local and effective GP policy must be readable and understood.'}}
|
||||
$collection=@($State.EffectivePolicy.Policy.Collections|Where-Object Type -CEQ 'Script')
|
||||
if($collection.Count -ne 1 -or $collection[0].EnforcementMode -cne 'AuditOnly' -or $collection[0].RuleCount -lt 1){throw 'An existing nonempty effective Script AuditOnly collection is required.'}
|
||||
if($State.Service.Status -cne 'Observed' -or $State.Service.State -cne 'Running'){throw 'AppIDSvc must already be running.'}
|
||||
if($State.Channel.Enabled -isnot [bool] -or -not $State.Channel.Enabled -or $State.Channel.Name -cne 'Microsoft-Windows-AppLocker/MSI and Script' -or -not $State.Channel.SecurityDescriptor){throw 'The native MSI and Script channel must already be enabled and readable.'}
|
||||
if($State.Management.Status -cne 'Observed' -or $State.SourceHash -cnotmatch '^[a-f0-9]{64}$' -or -not $State.MachineGuid -or -not $State.Computer){throw 'Incomplete management, machine or source observation.'}
|
||||
Get-WelaAppLockerScriptKey $State
|
||||
}
|
||||
function Get-WelaAppLockerScriptAuthorizationKey {
|
||||
param($Token)
|
||||
if($Token.Sid -cnotmatch '^S-1-\d+(-\d+)+$' -or $Token.AuthenticationId -cnotmatch '^0x[0-9a-f]+$' -or $null -eq $Token.Groups -or $null -eq $Token.Privileges){throw 'Incomplete actual token evidence.'}
|
||||
Get-WelaAppLockerScriptKey ([ordered]@{Sid=$Token.Sid;AuthenticationId=$Token.AuthenticationId;Groups=$Token.Groups;Privileges=$Token.Privileges})
|
||||
}
|
||||
function New-WelaAppLockerScriptText {
|
||||
param([string]$Template,[string]$Nonce)
|
||||
if($Nonce -cnotmatch '^[a-f0-9]{32}$' -or ([regex]::Matches($Template,'__WELA_SCRIPT_NONCE__')).Count -ne 3){throw 'Unexpected fixed worker template or nonce.'}
|
||||
$Template.Replace('__WELA_SCRIPT_NONCE__',$Nonce)
|
||||
}
|
||||
function Read-WelaAppLockerScriptBoundary {
|
||||
$reader=$null;$record=$null
|
||||
try {
|
||||
$query=[Diagnostics.Eventing.Reader.EventLogQuery]::new('Microsoft-Windows-AppLocker/MSI and Script',[Diagnostics.Eventing.Reader.PathType]::LogName,'*');$query.ReverseDirection=$true;$query.TolerateQueryErrors=$false
|
||||
$reader=[Diagnostics.Eventing.Reader.EventLogReader]::new($query);$reader.BatchSize=1
|
||||
$record=$reader.ReadEvent([TimeSpan]::FromSeconds(5))
|
||||
$status=@($reader.LogStatus)
|
||||
if($status.Count -ne 1 -or $status[0].LogName -cne 'Microsoft-Windows-AppLocker/MSI and Script' -or $status[0].StatusCode -ne 0){throw 'Incomplete native channel query status.'}
|
||||
if($null -eq $record){return [long]0}
|
||||
if($record.LogName -cne $status[0].LogName -or $record.RecordId -le 0){throw 'Invalid native record boundary.'}
|
||||
[long]$record.RecordId
|
||||
}finally{if($record){$record.Dispose()};if($reader){$reader.Dispose()}}
|
||||
}
|
||||
function Start-WelaAppLockerScriptProcess {
|
||||
param([string]$Root,$State,$Reader,[string]$SourcesKey)
|
||||
$nonce=[guid]::NewGuid().ToString('N');$path=Join-Path $Root ('wela-script-'+$nonce+'.ps1')
|
||||
$template=[IO.File]::ReadAllText((Join-Path $script:ScriptRoot 'scripts/AppLockerScriptWorker.ps1'))
|
||||
$scriptBytes=[Text.UTF8Encoding]::new($false).GetBytes((New-WelaAppLockerScriptText $template $nonce))
|
||||
$artifact=Write-WelaArrivalArtifact $Root ([IO.Path]::GetFileName($path)) ([Text.UTF8Encoding]::new($false).GetString($scriptBytes))
|
||||
$source=$null;$scriptFile=$null;$process=$null;$started=$false
|
||||
try {
|
||||
$source=[IO.File]::Open($State.Source,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
|
||||
$scriptFile=[IO.File]::Open($path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
|
||||
$sourceId=[Wela.AppLockerScript.Native]::FileId($source.SafeFileHandle.DangerousGetHandle());$scriptId=[Wela.AppLockerScript.Native]::FileId($scriptFile.SafeFileHandle.DangerousGetHandle())
|
||||
if((Get-FileHash -LiteralPath $State.Source -Algorithm SHA256).Hash.ToLowerInvariant() -cne $State.SourceHash -or (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'The held native source or generated script differs before launch.'}
|
||||
if((Get-WelaAppLockerScriptKey (Get-WelaAppLockerScriptSources)) -cne $SourcesKey){throw 'Source changed before script launch.'}
|
||||
$readerKey=Get-WelaAppLockerScriptKey $Reader
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Caller token changed before launch.'}
|
||||
$info=[Diagnostics.ProcessStartInfo]::new();$info.FileName=$State.Source;$info.Arguments='-NoLogo -NoProfile -NonInteractive -File "'+$path+'"';$info.UseShellExecute=$false;$info.CreateNoWindow=$true;$info.RedirectStandardInput=$true;$info.RedirectStandardOutput=$true;$info.RedirectStandardError=$true;$info.WorkingDirectory=$Root
|
||||
$process=[Diagnostics.Process]::new();$process.StartInfo=$info
|
||||
$start=(Get-WelaAppLockerScriptUtcNow)
|
||||
$started=$process.Start();if(-not $started){throw 'The fixed script process did not start.'}
|
||||
$stderr=[Wela.AppLockerScript.Native]::ReadBoundedAsync($process.StandardError,4096)
|
||||
$ready=[Wela.AppLockerScript.Native]::ReadLineBoundedAsync($process.StandardOutput,256)
|
||||
if(-not $ready.Wait(30000)){throw 'The fixed script did not reach its ready marker within thirty seconds.'}
|
||||
$line=$ready.GetAwaiter().GetResult()
|
||||
if($line -cnotmatch ('^WELA_SCRIPT_READY_'+$nonce+'\|(FullLanguage|ConstrainedLanguage)\|5\.1\.[0-9.]+$')){throw ('Unexpected fixed script ready marker: '+$line)}
|
||||
$child=[Wela.AppLockerScript.Native]::Child($process.Handle)
|
||||
if((Get-WelaAppLockerScriptAuthorizationKey $child) -cne (Get-WelaAppLockerScriptAuthorizationKey $Reader)){throw 'The actual child primary/logon authorization differs from the caller.'}
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Caller token changed while the fixed child started.'}
|
||||
$stdout=[Wela.AppLockerScript.Native]::ReadBoundedAsync($process.StandardOutput,4096)
|
||||
$process.StandardInput.WriteLine('WELA_SCRIPT_GO_'+$nonce);$process.StandardInput.Close()
|
||||
if(-not $process.WaitForExit(10000)){throw 'The fixed child did not complete within ten seconds of release.'}
|
||||
if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($stdout,$stderr),5000)){throw 'The fixed child output did not complete within five seconds.'}
|
||||
$out=$stdout.GetAwaiter().GetResult();$err=$stderr.GetAwaiter().GetResult();$end=(Get-WelaAppLockerScriptUtcNow)
|
||||
if($process.ExitCode -ne 0 -or $out.TrimEnd("`r","`n") -cne ('WELA_SCRIPT_COMPLETE_'+$nonce) -or $err){throw ('The fixed script did not complete correctly. Exit='+$process.ExitCode+' Error='+$err)}
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey -or [Wela.AppLockerScript.Native]::FileId($source.SafeFileHandle.DangerousGetHandle()) -cne $sourceId -or [Wela.AppLockerScript.Native]::FileId($scriptFile.SafeFileHandle.DangerousGetHandle()) -cne $scriptId){throw 'Reader or held file identity changed during script execution.'}
|
||||
[pscustomobject][ordered]@{ProcessId=$process.Id;UserSid=$Reader.Sid;ChildToken=$child;NativePowerShell=$State.Source;NativePowerShellSha256=$State.SourceHash;NativePowerShellFileId=$sourceId;ScriptPath=$path;ScriptSha256=$artifact.Sha256;ScriptFileId=$scriptId;ScriptArtifact=$artifact;Nonce=$nonce;Arguments=$info.Arguments;StartedUtc=$start.ToString('o');CompletedUtc=$end.ToString('o');Clock='GetSystemTimePreciseAsFileTime';Ready=$line;Marker=$out.TrimEnd("`r","`n");ExitCode=$process.ExitCode}
|
||||
}finally{
|
||||
if($process){try{if($started -and -not $process.HasExited){$process.Kill();if(-not $process.WaitForExit(5000)){throw 'Owned script process termination is unconfirmed.'}}}finally{$process.Dispose()}}
|
||||
if($scriptFile){$scriptFile.Dispose()};if($source){$source.Dispose()}
|
||||
}
|
||||
}
|
||||
function Read-WelaAppLockerScriptEvents {
|
||||
param([long]$Boundary)
|
||||
$query="*[System[Provider[@Name='Microsoft-Windows-AppLocker'] and (EventID=8005 or EventID=8006) and EventRecordID>$Boundary]]"
|
||||
$reader=$null;$record=$null;$xml=@();$bytes=0
|
||||
try {
|
||||
$nativeQuery=[Diagnostics.Eventing.Reader.EventLogQuery]::new('Microsoft-Windows-AppLocker/MSI and Script',[Diagnostics.Eventing.Reader.PathType]::LogName,$query);$nativeQuery.ReverseDirection=$false;$nativeQuery.TolerateQueryErrors=$false
|
||||
$reader=[Diagnostics.Eventing.Reader.EventLogReader]::new($nativeQuery);$reader.BatchSize=16
|
||||
while($null -ne ($record=$reader.ReadEvent([TimeSpan]::FromSeconds(2)))) {
|
||||
try{$text=$record.ToXml();$bytes+=[Text.Encoding]::UTF8.GetByteCount($text);if($xml.Count -ge 255 -or $bytes -gt 1048576){throw 'Native script query reached its 256-event/one-MiB cap.'};$xml+=$text}finally{$record.Dispose();$record=$null}
|
||||
}
|
||||
$status=@($reader.LogStatus);if($status.Count -ne 1 -or $status[0].LogName -cne 'Microsoft-Windows-AppLocker/MSI and Script' -or $status[0].StatusCode -ne 0){throw 'Incomplete native script query status.'}
|
||||
[pscustomobject]@{Xml=$xml;Query=$query;Bytes=$bytes;Complete=$true}
|
||||
}finally{if($record){$record.Dispose()};if($reader){$reader.Dispose()}}
|
||||
}
|
||||
|
||||
function Test-WelaAppLockerScriptEvent {
|
||||
param([string]$Xml,$Process,$State,[long]$Boundary)
|
||||
$reader=$null
|
||||
try {
|
||||
$settings=New-Object Xml.XmlReaderSettings;$settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit;$settings.XmlResolver=$null;$settings.MaxCharactersInDocument=4194304
|
||||
$reader=[Xml.XmlReader]::Create([IO.StringReader]::new($Xml),$settings);$doc=New-Object Xml.XmlDocument;$doc.XmlResolver=$null;$doc.Load($reader)
|
||||
$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('e','http://schemas.microsoft.com/win/2004/08/events/event');$ns.AddNamespace('a','http://schemas.microsoft.com/schemas/event/Microsoft.Windows/1.0.0.0')
|
||||
if ($doc.DocumentElement.LocalName -cne 'Event' -or $doc.DocumentElement.NamespaceURI -cne $ns.LookupNamespace('e') -or $doc.SelectNodes('/e:Event/e:System',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:UserData/a:RuleAndFileData',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:EventData',$ns).Count) {return $false}
|
||||
$system=@{};foreach ($name in @('Provider','EventID','Version','EventRecordID','Channel','Computer','TimeCreated')) {$nodes=$doc.SelectNodes("/e:Event/e:System/e:$name",$ns);if($nodes.Count -ne 1){return $false};$system[$name]=$nodes[0]}
|
||||
if ($system.Provider.GetAttribute('Name') -cne 'Microsoft-Windows-AppLocker' -or $system.Provider.GetAttribute('Guid').Trim('{}') -ine 'cbda4dbf-8d5d-4f69-9578-be14aa540d22' -or $system.EventID.InnerText -cnotin @('8005','8006') -or $system.Version.InnerText -cne '0' -or $system.EventRecordID.InnerText -notmatch '^[1-9][0-9]*$' -or $system.Channel.InnerText -cne 'Microsoft-Windows-AppLocker/MSI and Script') {return $false}
|
||||
$computers=@($State.Computer);if($State.Host.PartOfDomain){$computers+=$State.Computer+'.'+$State.Domain};if($system.Computer.InnerText -notin $computers){return $false}
|
||||
$time=ConvertTo-WelaArrivalUtc $system.TimeCreated.GetAttribute('SystemTime')
|
||||
if($time.UtcDateTime -lt ([DateTimeOffset]::Parse($Process.StartedUtc)).UtcDateTime -or $time.UtcDateTime -gt (ConvertTo-WelaArrivalUtc $Process.CompletedUtc).UtcDateTime -or [long]$system.EventRecordID.InnerText -le $Boundary){return $false}
|
||||
$data=@{};foreach($node in $doc.SelectSingleNode('/e:Event/e:UserData/a:RuleAndFileData',$ns).ChildNodes){if($node.NodeType -eq 'Whitespace'){continue};if($node.NodeType -ne 'Element' -or $node.NamespaceURI -cne $ns.LookupNamespace('a') -or $data.ContainsKey($node.LocalName) -or @($node.ChildNodes|Where-Object NodeType -eq Element).Count){return $false};$data[$node.LocalName]=$node.InnerText}
|
||||
if($data.PolicyName -cne 'SCRIPT' -or $data.TargetUser -cne $Process.UserSid -or $data.TargetProcessId -notmatch '^[1-9][0-9]*$' -or [long]$data.TargetProcessId -ne $Process.ProcessId){return $false}
|
||||
# AppLocker may render the exact Windows directory through this documented path variable.
|
||||
$eventPath=$data.FilePath
|
||||
if($eventPath -imatch '^%OSDRIVE%\\'){$eventPath=[IO.Path]::GetPathRoot($State.Source).TrimEnd('\')+$eventPath.Substring(9)}
|
||||
return $eventPath -ieq $Process.ScriptPath
|
||||
} catch {return $false} finally {if($reader){$reader.Dispose()}}
|
||||
}
|
||||
function Invoke-WelaAppLockerScriptProbe {
|
||||
param([ValidateSet('Plan','Run')][string]$Action='Plan',[string]$OutputPath,[ValidateRange(1,30)][int]$TimeoutSeconds=15)
|
||||
if(($Action -eq 'Run') -ne (-not [string]::IsNullOrWhiteSpace($OutputPath))){throw 'Run requires a new AppLockerScriptOutputPath; Plan does not write files.'}
|
||||
Initialize-WelaAppLockerScriptNative
|
||||
$sources=Get-WelaAppLockerScriptSources;$sourceKey=Get-WelaAppLockerScriptKey $sources
|
||||
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaAppLockerScriptProbe';Action=$Action;Status='Unverified';ExitCode=1;RecordedUtc=(Get-WelaAppLockerScriptUtcNow).ToString('o');Sources=$sources;Before=$null;After=$null;ReaderBefore=$null;ReaderAfter=$null;ReaderInterval='After output/context preparation, through child execution and actual event queries; final metadata is checked separately';Boundary=$null;Process=$null;EventId=$null;Decision=$null;Artifacts=@();Diagnostic='';OutputPath=$null;PolicyChanges=0;ReadyRuleCredit=0;CspPolicyState='Unknown';Scope='One fixed native Windows PowerShell5.1 Script-collection event. Other engines, collections, forwarding and Sigma/backend validation are not tested. Sysmon excluded.'}
|
||||
try {
|
||||
$before=Get-WelaAppLockerScriptState;$report.Before=$before;$key=Get-WelaAppLockerScriptStateKey $before
|
||||
if($Action -eq 'Plan'){$report.ReaderBefore=(Get-WelaAppLockerScriptReader);$report.Status='PrerequisitesObserved';$report.ExitCode=0;return $report}
|
||||
$report.OutputPath=New-WelaArrivalOutput -Path $OutputPath -SourcePath $script:ScriptRoot
|
||||
if((Get-WelaAppLockerScriptStateKey (Get-WelaAppLockerScriptState)) -cne $key){throw 'Context changed during output preparation.'}
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'before.json' ($before|ConvertTo-Json -Depth 24)
|
||||
$reader=(Get-WelaAppLockerScriptReader);$report.ReaderBefore=$reader;$readerKey=Get-WelaAppLockerScriptKey $reader
|
||||
$report.Boundary=Read-WelaAppLockerScriptBoundary
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Reader changed during the actual boundary query.'}
|
||||
$process=Start-WelaAppLockerScriptProcess -Root $report.OutputPath -State $before -Reader $reader -SourcesKey $sourceKey;$report.Process=$process
|
||||
$report.Artifacts+= $process.ScriptArtifact
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'process.json' ($process|ConvertTo-Json -Depth 16)
|
||||
$timer=[Diagnostics.Stopwatch]::StartNew();$matches=@();$batch=$null
|
||||
do {
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Reader changed before script event query.'}
|
||||
$batch=Read-WelaAppLockerScriptEvents $report.Boundary
|
||||
if($batch.Complete -isnot [bool] -or -not $batch.Complete){throw 'Script query completeness is unknown.'}
|
||||
if((Get-WelaAppLockerScriptKey ((Get-WelaAppLockerScriptReader))) -cne $readerKey){throw 'Reader changed during script event query.'}
|
||||
$matches=@($batch.Xml|Where-Object{Test-WelaAppLockerScriptEvent $_ $process $before $report.Boundary})
|
||||
if($matches.Count -gt 1){throw 'Multiple exact script records make the result ambiguous.'}
|
||||
if($matches.Count -eq 1){break}
|
||||
Start-Sleep -Milliseconds 250
|
||||
}while($timer.Elapsed.TotalSeconds -lt $TimeoutSeconds)
|
||||
$report.ReaderAfter=(Get-WelaAppLockerScriptReader)
|
||||
if((Get-WelaAppLockerScriptKey $report.ReaderAfter) -cne $readerKey){throw 'Reader changed before completion of the actual query interval.'}
|
||||
if($matches.Count -ne 1){
|
||||
# Retain only bounded candidates bearing the owned unique script name.
|
||||
$owned=@($batch.Xml|Where-Object{$_ -like ('*wela-script-'+$process.Nonce+'.ps1*')}|Select-Object -First 4)
|
||||
for($i=0;$i -lt $owned.Count;$i++){$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath ('candidate-'+$i+'.xml') $owned[$i]}
|
||||
throw 'No exact native Script8005/8006 event arrived within the timeout.'
|
||||
}
|
||||
$report.After=Get-WelaAppLockerScriptState
|
||||
if((Get-WelaAppLockerScriptStateKey $report.After) -cne $key -or (Get-WelaAppLockerScriptKey (Get-WelaAppLockerScriptSources)) -cne $sourceKey -or (Get-FileHash -LiteralPath $process.ScriptPath -Algorithm SHA256).Hash.ToLowerInvariant() -cne $process.ScriptSha256){throw 'Host, policy, service, channel, execution-policy observation or implementation changed.'}
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'after.json' ($report.After|ConvertTo-Json -Depth 24)
|
||||
$report.Artifacts+=Write-WelaArrivalArtifact $report.OutputPath 'event.xml' $matches[0]
|
||||
$event=[xml]$matches[0];$report.EventId=[int]$event.Event.System.EventID
|
||||
$report.Decision=if($report.EventId -eq 8005){'Allowed'}else{'AllowedWouldBlockIfEnforced'}
|
||||
$report.Status='NativeScriptEventObserved';$report.ExitCode=0
|
||||
}catch{$report.Diagnostic=$_.Exception.Message}
|
||||
if($report.OutputPath){$json=$report|ConvertTo-Json -Depth 32;if([Text.Encoding]::UTF8.GetByteCount($json) -gt 2097152){throw 'The script probe report exceeded its two-MiB bound.'};$null=Write-WelaArrivalArtifact $report.OutputPath 'manifest.json' $json}
|
||||
$report
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Fixed locally generated script; no external inputs or configuration writes.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::Out.WriteLine(('WELA_SCRIPT_READY___WELA_SCRIPT_NONCE__|' + $ExecutionContext.SessionState.LanguageMode + '|' + $PSVersionTable.PSVersion))
|
||||
$release = [Console]::In.ReadLine()
|
||||
if ($release -cne 'WELA_SCRIPT_GO___WELA_SCRIPT_NONCE__') { exit 17 }
|
||||
[Console]::Out.WriteLine('WELA_SCRIPT_COMPLETE___WELA_SCRIPT_NONCE__')
|
||||
exit 0
|
||||
@@ -0,0 +1,25 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
$engine=Join-Path $PSHOME $(if($PSEdition -eq 'Core'){if($env:OS -eq 'Windows_NT'){'pwsh.exe'}else{'pwsh'}}else{'powershell.exe'})
|
||||
$count=0
|
||||
function Check-Command([string]$Arguments,[int]$ExpectedExit,[string]$Pattern) {
|
||||
$info=[Diagnostics.ProcessStartInfo]::new();$info.FileName=$engine;$info.Arguments='-NoProfile -File "'+(Join-Path $repo 'WELA.ps1')+'" '+$Arguments;$info.UseShellExecute=$false;$info.CreateNoWindow=$true;$info.RedirectStandardOutput=$true;$info.RedirectStandardError=$true
|
||||
$process=[Diagnostics.Process]::new();$process.StartInfo=$info;$started=$false
|
||||
try {
|
||||
$started=$process.Start();$out=$process.StandardOutput.ReadToEndAsync();$err=$process.StandardError.ReadToEndAsync()
|
||||
if(-not $process.WaitForExit(30000)){throw 'CLI timeout'}
|
||||
if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($out,$err),5000)){throw 'CLI output timeout'}
|
||||
$text=$out.GetAwaiter().GetResult()+$err.GetAwaiter().GetResult()
|
||||
if(($process.ExitCode -eq 0) -ne ($ExpectedExit -eq 0) -or $text -notmatch $Pattern){throw "CLI mismatch: $Arguments ; Exit=$($process.ExitCode) ; $text"};$script:count++
|
||||
}finally{if($started -and -not $process.HasExited){$process.Kill();$null=$process.WaitForExit(5000)};$process.Dispose()}
|
||||
}
|
||||
Check-Command 'applocker-script-probe -Help' 0 'existing Script AuditOnly'
|
||||
Check-Command 'help' 0 'applocker-script-probe'
|
||||
Check-Command 'version -AppLockerScriptAction Run' 1 'AppLockerScript options require'
|
||||
Check-Command 'applocker-script-probe -AppLockerScriptAction Run -WhatIf' 1 'only its dedicated'
|
||||
Check-Command 'applocker-script-probe -AppLockerScriptAction Run -DryRun' 1 'only its dedicated'
|
||||
Check-Command 'applocker-script-probe -Auto' 1 'only its dedicated'
|
||||
Check-Command 'applocker-script-probe -AppLockerProbeAction Run' 1 'only its dedicated'
|
||||
Check-Command 'applocker-script-probe -AppLockerScriptAction Run' 1 'Run requires'
|
||||
Check-Command 'applocker-script-probe -AppLockerScriptAction Plan -AppLockerScriptOutputPath ignored' 1 'Run requires'
|
||||
Write-Host "AppLocker Script public CLI fixtures passed: $count checks."
|
||||
@@ -0,0 +1,67 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
. "$repo/scripts/AppLockerReadiness.ps1"
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/AppLockerScriptProbe.ps1"
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Reject([scriptblock]$Action,[string]$Pattern){$message='';try{&$Action|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern; got $message"}
|
||||
$policy='<AppLockerPolicy Version="1"><RuleCollection Type="Script" EnforcementMode="AuditOnly"><FilePathRule Id="12345678-1234-1234-1234-123456789abc" Name="Windows" UserOrGroupSid="S-1-1-0" Action="Allow"><Conditions><FilePathCondition Path="%WINDIR%\*" /></Conditions></FilePathRule></RuleCollection></AppLockerPolicy>'
|
||||
$state=[pscustomobject]@{Host=[pscustomobject]@{Status='Candidate';Is64BitProcess=$true;PartOfDomain=$false;ProductType=3;Build=20348};MachineGuid='11111111-1111-1111-1111-111111111111';Management=[pscustomobject]@{Status='Observed'};Computer='TEST';Domain='WORKGROUP';Reader=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1000'};EffectivePolicy=[pscustomobject]@{Status='Observed';Policy=(ConvertFrom-WelaAppLockerXml $policy)};Service=[pscustomobject]@{Status='Observed';State='Running';StartMode='Manual'};Channel=[pscustomobject]@{Name='Microsoft-Windows-AppLocker/MSI and Script';Enabled=$true;SecurityDescriptor='O:SYG:SYD:(A;;0x1;;;SY)'};Source='C:\Windows\System32\cmd.ps1';SourceHash=('a'*64)}
|
||||
$state|Add-Member NoteProperty LocalPolicy ($state.EffectivePolicy|ConvertTo-Json -Depth 20|ConvertFrom-Json)
|
||||
$null=Get-WelaAppLockerScriptStateKey $state;Assert $true 'Valid audit-only prereqs'
|
||||
foreach($mode in @('Enabled','NotConfigured')){$state.EffectivePolicy.Policy=ConvertFrom-WelaAppLockerXml ($policy.Replace('AuditOnly',$mode));Reject {Get-WelaAppLockerScriptStateKey $state} 'AuditOnly'}
|
||||
$state.EffectivePolicy.Policy=ConvertFrom-WelaAppLockerXml $policy
|
||||
$state.Service.State='Stopped';Reject {Get-WelaAppLockerScriptStateKey $state} 'already be running';$state.Service.State='Running'
|
||||
$state.Channel.Enabled=$false;Reject {Get-WelaAppLockerScriptStateKey $state} 'enabled';$state.Channel.Enabled=$true
|
||||
$process=[pscustomobject]@{ScriptPath='C:\Temp\wela-owned.ps1';ProcessId=1234;UserSid=$state.Reader.Sid;StartedUtc='2026-09-01T00:00:00.0000000Z';CompletedUtc='2026-09-01T00:00:02.0000000Z'}
|
||||
$event='<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"><System><Provider Name="Microsoft-Windows-AppLocker" Guid="{cbda4dbf-8d5d-4f69-9578-be14aa540d22}"/><EventID>8006</EventID><Version>0</Version><EventRecordID>42</EventRecordID><TimeCreated SystemTime="2026-09-01T00:00:01.0000000Z"/><Channel>Microsoft-Windows-AppLocker/MSI and Script</Channel><Computer>TEST</Computer></System><UserData><RuleAndFileData xmlns="http://schemas.microsoft.com/schemas/event/Microsoft.Windows/1.0.0.0"><PolicyName>SCRIPT</PolicyName><TargetUser>S-1-5-21-1-2-3-1000</TargetUser><TargetProcessId>1234</TargetProcessId><FilePath>C:\Temp\wela-owned.ps1</FilePath></RuleAndFileData></UserData></Event>'
|
||||
$end=[long]41
|
||||
Assert (Test-WelaAppLockerScriptEvent $event $process $state $end) 'Exact fixture must match'
|
||||
Assert (Test-WelaAppLockerScriptEvent ($event.Replace('8006','8005')) $process $state $end) 'Allowed event matches but has distinct EventId'
|
||||
$mutations=@(@('8006','8007'),@('1234','1235'),@('S-1-5-21-1-2-3-1000','S-1-5-21-1-2-3-1001'),@('C:\Temp\wela-owned.ps1','C:\Temp\other.ps1'),@('<PolicyName>SCRIPT','<PolicyName>DLL'),@('<Computer>TEST','<Computer>OTHER'),@('cbda4dbf','abda4dbf'),@('<Version>0','<Version>1'),@('00:00:01.0000000Z','00:00:03.0000000Z'),@('</System>','<EventID>8006</EventID></System>'),@('</RuleAndFileData>','<TargetUser>S-1-1-0</TargetUser></RuleAndFileData>'))
|
||||
foreach($pair in $mutations){$bad=$event.Replace($pair[0],$pair[1]);Assert ($bad -cne $event) 'Mutation changed fixture';Assert (-not(Test-WelaAppLockerScriptEvent $bad $process $state $end)) ('Reject '+$pair[0])}
|
||||
Assert (-not(Test-WelaAppLockerScriptEvent ('<!DOCTYPE Event [<!ENTITY x SYSTEM "file:///etc/passwd">]>'+$event) $process $state $end)) 'DTD rejected'
|
||||
Reject {Invoke-WelaAppLockerScriptProbe -Action Run} 'requires'
|
||||
Reject {Invoke-WelaAppLockerScriptProbe -Action Plan -OutputPath ignored} 'requires'
|
||||
|
||||
Assert (-not(Test-WelaAppLockerScriptEvent $event $process $state 42)) 'Previously observed record is rejected'
|
||||
Assert (-not(Test-WelaAppLockerScriptEvent ($event.Replace('00:00:01.0000000Z','00:00:02.0000001Z')) $process $state $end)) 'A 100ns late record is rejected without clock padding'
|
||||
Assert (Test-WelaAppLockerScriptEvent ($event.Replace('00:00:01.0000000Z','00:00:00.0000000Z')) $process $state $end) 'Exact inclusive start is accepted'
|
||||
Assert (Test-WelaAppLockerScriptEvent ($event.Replace('00:00:01.0000000Z','00:00:02.0000000Z')) $process $state $end) 'Exact inclusive completion is accepted'
|
||||
$worker=[IO.File]::ReadAllText("$repo/scripts/AppLockerScriptWorker.ps1")
|
||||
$text=New-WelaAppLockerScriptText $worker ('a'*32)
|
||||
Assert ($text -notlike '*__WELA_SCRIPT_NONCE__*') 'All three fixed nonce placeholders replaced'
|
||||
Reject {New-WelaAppLockerScriptText $worker ('a'*31+"'" )} 'nonce'
|
||||
Reject {New-WelaAppLockerScriptText ($worker+'__WELA_SCRIPT_NONCE__') ('a'*32)} 'template'
|
||||
$tokens=$null;$errors=$null;$null=[Management.Automation.Language.Parser]::ParseInput($text,[ref]$tokens,[ref]$errors)
|
||||
Assert (-not $errors.Count) 'Generated worker parses'
|
||||
# Use the production orchestration with mocked native read/launch boundaries.
|
||||
# These fixtures never stand in for actual native success; the Windows matrix does that.
|
||||
$script:ScriptRoot=$repo;$script:scenario='ok';$script:reads=0;$script:state=$state;$script:event=$event
|
||||
$script:token=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1000';AuthenticationId='0x123';Groups=@();Privileges=@();TokenId='0x456';ModifiedId='0x789'}
|
||||
function Initialize-WelaAppLockerScriptNative {}
|
||||
function Get-WelaAppLockerScriptReader {if($script:scenario -eq 'reader-drift' -and $script:reads -gt 2){$script:token.ModifiedId='0xabc'};$script:token|ConvertTo-Json -Depth 8|ConvertFrom-Json}
|
||||
function Get-WelaAppLockerScriptUtcNow {([DateTimeOffset]::Parse('2026-09-01T00:00:00Z')).UtcDateTime}
|
||||
function Get-WelaAppLockerScriptState {$script:reads++;if($script:scenario -eq 'drift' -and $script:reads -gt 2){$script:state.Service.StartMode='Auto'};$script:state|ConvertTo-Json -Depth 20|ConvertFrom-Json}
|
||||
function Read-WelaAppLockerScriptBoundary {41}
|
||||
function Start-WelaAppLockerScriptProcess {
|
||||
param($Root,$State,$Reader,$SourcesKey)
|
||||
if($script:scenario -eq 'reader-drift'){$script:token.ModifiedId='0xabc'}
|
||||
$artifact=Write-WelaArrivalArtifact $Root 'fixed.ps1' 'fixed'
|
||||
[pscustomobject]@{ScriptPath=(Join-Path $Root 'fixed.ps1');ScriptSha256=$artifact.Sha256;ScriptArtifact=$artifact;Nonce=('a'*32)}
|
||||
}
|
||||
function Read-WelaAppLockerScriptEvents {param($Boundary);if($script:scenario -eq 'denied'){throw [UnauthorizedAccessException]::new('Native query denied')};[pscustomobject]@{Xml=if($script:scenario -eq 'duplicate'){@($script:event,$script:event)}elseif($script:scenario -eq 'absent'){@()}else{@($script:event)};Complete=($script:scenario -ne 'cap')}}
|
||||
function Test-WelaAppLockerScriptEvent {$true}
|
||||
$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-applocker-script-test-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
|
||||
try {
|
||||
foreach($scenario in @('ok','duplicate','drift','cap','denied','absent','reader-drift')){
|
||||
$script:scenario=$scenario;$script:reads=0;$state.Service.StartMode='Manual';$script:token.ModifiedId='0x789'
|
||||
$result=Invoke-WelaAppLockerScriptProbe Run (Join-Path $root $scenario) 1
|
||||
Assert ($result.ReadyRuleCredit -eq 0 -and $result.PolicyChanges -eq 0) 'No readiness or configuration credit'
|
||||
Assert (($result.ExitCode -eq 0) -eq ($scenario -eq 'ok')) "Expected outcome $scenario : $($result.Diagnostic)"
|
||||
Assert (($result.Status -ceq 'NativeScriptEventObserved') -eq ($scenario -eq 'ok')) 'Only complete success receives observed status'
|
||||
Assert (Test-Path (Join-Path $result.OutputPath 'manifest.json')) 'Success/failure manifest retained'
|
||||
}
|
||||
}finally{Remove-Item -LiteralPath $root -Recurse -Force}
|
||||
Write-Host "AppLocker Script fixtures passed: $count assertions."
|
||||
@@ -0,0 +1,134 @@
|
||||
param([switch]$AllowDisposablePolicyWrite)
|
||||
$ErrorActionPreference='Stop'
|
||||
if($env:OS -ne 'Windows_NT'){Write-Host 'Skipped: Windows required.';exit 0}
|
||||
if(-not $AllowDisposablePolicyWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit disposable GitHub-hosted policy-write opt-in required.'}
|
||||
function Refresh-DisposableComputerPolicy {
|
||||
$info=New-Object Diagnostics.ProcessStartInfo
|
||||
$info.FileName=Join-Path ([Environment]::SystemDirectory) 'gpupdate.exe';$info.Arguments='/target:computer /force /wait:30';$info.UseShellExecute=$false
|
||||
$process=[Diagnostics.Process]::Start($info)
|
||||
try {if(-not $process.WaitForExit(60000)){$process.Kill();throw 'Disposable computer policy refresh exceeded 60 seconds.'};if($process.ExitCode -ne 0){throw ('Disposable computer policy refresh failed: '+$process.ExitCode)}} finally {$process.Dispose()}
|
||||
}
|
||||
function Stop-DisposablePolicyConverter {
|
||||
$task=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop
|
||||
if($task.State -in @('Running','Queued')) {Stop-ScheduledTask -InputObject $task -ErrorAction Stop}
|
||||
$deadline=[DateTime]::UtcNow.AddSeconds(15)
|
||||
do {$task=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop;if($task.State -in @('Ready','Disabled')){return};Start-Sleep -Milliseconds 200}while([DateTime]::UtcNow -lt $deadline)
|
||||
throw 'The verified borrowed PolicyConverter task did not become idle.'
|
||||
}
|
||||
function Run-DisposablePolicyConverter {
|
||||
# The Task Scheduler CIM provider can retain stale LastRunTime on Server2022.
|
||||
# Follow the actual COM Run instance and native completion state instead.
|
||||
$scheduler=$null;$folder=$null;$registered=$null;$instance=$null;$running=$null;$definition=$null;$settings=$null
|
||||
try {
|
||||
$scheduler=New-Object -ComObject 'Schedule.Service';$scheduler.Connect()
|
||||
$folder=$scheduler.GetFolder('\Microsoft\Windows\AppID');$registered=$folder.GetTask('PolicyConverter')
|
||||
$definition=$registered.Definition;$settings=$definition.Settings
|
||||
if(-not $settings.AllowDemandStart){throw 'The verified PolicyConverter task does not allow an on-demand invocation.'}
|
||||
$running=$registered.GetInstances(0)
|
||||
if($running.Count -ne 0 -or $registered.State -ne 3){throw 'The verified borrowed PolicyConverter task must be idle before invocation.'}
|
||||
$null=[Runtime.InteropServices.Marshal]::FinalReleaseComObject($running);$running=$null
|
||||
$instance=$registered.Run($null)
|
||||
if($null -eq $instance -or [string]::IsNullOrWhiteSpace($instance.InstanceGuid)){throw 'Native PolicyConverter did not return a task instance identity.'}
|
||||
$instanceId=[string]$instance.InstanceGuid;$deadline=[DateTime]::UtcNow.AddSeconds(30)
|
||||
do {
|
||||
$running=$registered.GetInstances(0)
|
||||
try {$idle=$running.Count -eq 0 -and $registered.State -eq 3}finally{$null=[Runtime.InteropServices.Marshal]::FinalReleaseComObject($running);$running=$null}
|
||||
if($idle){if($registered.LastTaskResult -ne 0){throw ('Native policy conversion failed: '+$registered.LastTaskResult)};Write-Host ('Native PolicyConverter instance completed: '+$instanceId);return}
|
||||
Start-Sleep -Milliseconds 200
|
||||
}while([DateTime]::UtcNow -lt $deadline)
|
||||
Stop-DisposablePolicyConverter
|
||||
throw 'The owned native PolicyConverter instance did not complete within thirty seconds.'
|
||||
}finally{foreach($item in @($running,$instance,$settings,$definition,$registered,$folder,$scheduler)){if($null -ne $item -and [Runtime.InteropServices.Marshal]::IsComObject($item)){$null=[Runtime.InteropServices.Marshal]::FinalReleaseComObject($item)}}}
|
||||
}
|
||||
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
. "$repo/scripts/Configuration.ps1"
|
||||
. "$repo/scripts/AppLockerReadiness.ps1"
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/AppLockerScriptProbe.ps1"
|
||||
$script:ScriptRoot=$repo
|
||||
$root=Join-Path $env:RUNNER_TEMP ('wela-applocker-script-native-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
|
||||
Write-Host ('Native fixture process session: '+[Diagnostics.Process]::GetCurrentProcess().SessionId)
|
||||
$converter=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop
|
||||
$converterBefore=Export-ScheduledTask -InputObject $converter -ErrorAction Stop
|
||||
$converterDisabled=$converter.State -eq 'Disabled';$converterChanged=$false
|
||||
$actions=@($converter.Actions)
|
||||
if($converter.State -notin @('Disabled','Ready') -or $actions.Count -ne 1 -or [Environment]::ExpandEnvironmentVariables($actions[0].Execute).Trim('"') -ine (Join-Path ([Environment]::SystemDirectory) 'appidpolicyconverter.exe') -or $actions[0].Arguments){throw ('Only the unchanged native PolicyConverter action is permitted: '+($actions|ConvertTo-Json -Depth 8))}
|
||||
$before=Get-WelaAppLockerReadiness
|
||||
if($before.Host.PartOfDomain -or $before.Management.Status -ne 'Observed' -or $before.LocalPolicy.Status -ne 'Observed' -or $before.EffectiveGpPolicy.Status -ne 'Observed' -or $before.LocalPolicy.Policy.TotalRules -ne 0 -or $before.EffectiveGpPolicy.Policy.TotalRules -ne 0 -or $before.LocalPolicy.Policy.HasUnknownPolicyData -or $before.EffectiveGpPolicy.Policy.HasUnknownPolicyData){Write-Host ($before | ConvertTo-Json -Depth 16);throw 'Disposable test requires empty, understood local/effective policies on a non-domain disposable host.'}
|
||||
$backup=Join-Path $root 'policy-before.xml';[IO.File]::WriteAllText($backup,$before.LocalPolicy.Policy.Xml)
|
||||
[IO.File]::WriteAllText((Join-Path $root 'prerequisites-before.json'),($before | ConvertTo-Json -Depth 16))
|
||||
$fixture='<AppLockerPolicy Version="1"><RuleCollection Type="Script" EnforcementMode="AuditOnly"><FilePathRule Id="12345678-1234-1234-1234-123456789abc" Name="Disposable Windows path only" Description="Owned native event fixture" UserOrGroupSid="S-1-1-0" Action="Allow"><Conditions><FilePathCondition Path="%WINDIR%\*" /></Conditions></FilePathRule></RuleCollection></AppLockerPolicy>'
|
||||
$policyPath=Join-Path $root 'fixture.xml';[IO.File]::WriteAllText($policyPath,$fixture)
|
||||
$log=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Microsoft-Windows-AppLocker/MSI and Script');$enabled=$log.IsEnabled;$touched=$false;$cleanup=@();$primary=$null
|
||||
try {
|
||||
$touched=$true
|
||||
# Test-only preparation under explicit disposable-host and empty-GP gates.
|
||||
# Hosted images contain enrollment/provider keys: preserve them and CSP Unknown.
|
||||
# The production importer must continue to reject those observations.
|
||||
$preparedUtc=[DateTime]::UtcNow
|
||||
Set-AppLockerPolicy -XmlPolicy $policyPath -ErrorAction Stop
|
||||
if($before.Service.StartMode -eq 'Disabled'){throw 'Test will not change protected AppIDSvc startup mode.'}
|
||||
if($before.Service.State -ne 'Running'){Start-Service AppIDSvc -ErrorAction Stop}
|
||||
$log.IsEnabled=$true;$log.SaveChanges()
|
||||
if($converterDisabled){$converterChanged=$true;$null=Enable-ScheduledTask -InputObject $converter -ErrorAction Stop}
|
||||
Refresh-DisposableComputerPolicy
|
||||
Run-DisposablePolicyConverter
|
||||
$applied=$false;$applyDeadline=[DateTime]::UtcNow.AddSeconds(30)
|
||||
do {
|
||||
$records=@()
|
||||
try {try{$records=@(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-AppLocker/EXE and DLL';Id=8001;StartTime=$preparedUtc} -MaxEvents 10 -ErrorAction Stop)}catch{if($_.FullyQualifiedErrorId -notlike 'NoMatchingEventsFound*'){throw}};$applied=$records.Count -gt 0} finally {foreach($record in $records){$record.Dispose()}}
|
||||
if($applied){break};Start-Sleep -Milliseconds 250
|
||||
} while([DateTime]::UtcNow -lt $applyDeadline)
|
||||
if(-not $applied){throw 'No native 8001 policy-applied event after disposable GP refresh.'}
|
||||
Write-Host 'Native 8001 policy-applied evidence observed after disposable GP refresh.'
|
||||
# Wait for actual effective audit-only policy, without treating elapsed time as success.
|
||||
$deadline=[DateTime]::UtcNow.AddSeconds(30)
|
||||
do {$state=Get-WelaAppLockerScriptState;$ready=$false;try{$null=Get-WelaAppLockerScriptStateKey $state;$ready=$true}catch{};if($ready){break};Start-Sleep -Milliseconds 500}while([DateTime]::UtcNow -lt $deadline)
|
||||
foreach($decision in @('WouldBlock','Allowed')) {
|
||||
if($decision -eq 'Allowed'){
|
||||
[IO.File]::WriteAllText($policyPath,$fixture.Replace('%WINDIR%\*','*'))
|
||||
Set-AppLockerPolicy -XmlPolicy $policyPath -ErrorAction Stop
|
||||
Refresh-DisposableComputerPolicy;Run-DisposablePolicyConverter
|
||||
$expected=ConvertFrom-WelaAppLockerXml ([IO.File]::ReadAllText($policyPath))
|
||||
$actual=Get-WelaAppLockerPolicySnapshot Effective
|
||||
if($actual.Status -ne 'Observed' -or (Get-WelaAppLockerXmlKey $actual.Policy.Xml) -cne (Get-WelaAppLockerXmlKey $expected.Xml)){throw 'Actual allowed Script policy differs from the disposable fixture.'}
|
||||
}
|
||||
$probe=& "$repo/WELA.ps1" applocker-script-probe -AppLockerScriptAction Run -AppLockerScriptOutputPath (Join-Path $root ('evidence-'+$decision)) -AppLockerScriptTimeoutSeconds 30
|
||||
$expectedId=if($decision -eq 'Allowed'){8005}else{8006}
|
||||
if($probe.ExitCode -or $probe.Status -cne 'NativeScriptEventObserved' -or $probe.EventId -ne $expectedId){throw ($probe|ConvertTo-Json -Depth 32)}
|
||||
if($probe.ReadyRuleCredit -ne 0 -or $probe.PolicyChanges -ne 0){throw 'Unsupported policy/credit claim.'}
|
||||
foreach($artifact in $probe.Artifacts){if((Get-FileHash (Join-Path $probe.OutputPath $artifact.Name)).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'Artifact hash mismatch'}}
|
||||
Write-Host "Native AppLocker $expectedId observed via public CLI under PowerShell $($PSVersionTable.PSVersion), build $($probe.Before.Host.Build). Zero Sigma credit."
|
||||
}
|
||||
|
||||
} catch {
|
||||
$primary=$_;Write-Host $_
|
||||
Get-ChildItem -LiteralPath $root -Recurse -Filter 'candidate-*.xml'|ForEach-Object {Write-Host ([IO.File]::ReadAllText($_.FullName))}
|
||||
# Read-only diagnostic independent of the production XPath filter and parser.
|
||||
try {
|
||||
$recent=@(Get-WinEvent -LogName 'Microsoft-Windows-AppLocker/MSI and Script' -MaxEvents 12 -ErrorAction Stop)
|
||||
try {foreach($record in $recent){Write-Host ('Recent native channel XML: '+$record.ToXml())}} finally {foreach($record in $recent){$record.Dispose()}}
|
||||
} catch {Write-Host ('Recent native channel read: '+$_.Exception.Message)}
|
||||
Get-CimInstance Win32_SystemDriver -Filter "Name='AppID'" | Select-Object Name,State,StartMode | ConvertTo-Json | Write-Host
|
||||
try {Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -ErrorAction Stop | Select-Object TaskName,State | ConvertTo-Json | Write-Host}catch{Write-Host ('AppID task read: '+$_.Exception.Message)}
|
||||
try {$nativeLog=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('Microsoft-Windows-AppLocker/MSI and Script');try{$nativeLog | Select-Object IsEnabled,LogType,ProviderLevel,ProviderKeywords,LogIsolation | ConvertTo-Json | Write-Host}finally{$nativeLog.Dispose()}}catch{Write-Host ('Channel metadata read: '+$_.Exception.Message)}
|
||||
Write-Host ((Get-WelaAppLockerPolicySnapshot Effective) | ConvertTo-Json -Depth 12)
|
||||
}
|
||||
finally {
|
||||
if($touched){
|
||||
try {Stop-DisposablePolicyConverter}catch{$cleanup+=$_.Exception.Message}
|
||||
try {Set-AppLockerPolicy -XmlPolicy $backup -ErrorAction Stop;Refresh-DisposableComputerPolicy;if($converterChanged -or -not $converterDisabled){Run-DisposablePolicyConverter};$restored=Get-WelaAppLockerPolicySnapshot Local;if($restored.Status -ne 'Observed' -or (Get-WelaAppLockerXmlKey $restored.Policy.Xml) -cne (Get-WelaAppLockerXmlKey $before.LocalPolicy.Policy.Xml)){throw 'Local policy restoration differs'};$effectiveRestored=Get-WelaAppLockerPolicySnapshot Effective;if($effectiveRestored.Status -ne 'Observed' -or (Get-WelaAppLockerXmlKey $effectiveRestored.Policy.Xml) -cne (Get-WelaAppLockerXmlKey $before.EffectiveGpPolicy.Policy.Xml)){throw 'Effective GP policy restoration differs'}}catch{$cleanup+=$_.Exception.Message}
|
||||
try {Stop-DisposablePolicyConverter;if($converterChanged){$null=Disable-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop};$taskAfter=Get-ScheduledTask -TaskPath '\Microsoft\Windows\AppID\' -TaskName 'PolicyConverter' -ErrorAction Stop;if($taskAfter.State -ne $(if($converterDisabled){'Disabled'}else{'Ready'}) -or (Export-ScheduledTask -InputObject $taskAfter -ErrorAction Stop) -cne $converterBefore){throw 'Native PolicyConverter task definition was not restored'}}catch{$cleanup+=$_.Exception.Message}
|
||||
try {$log.IsEnabled=$enabled;$log.SaveChanges();$verify=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new($log.LogName);try{if($verify.IsEnabled -ne $enabled){throw 'Channel restoration differs'}}finally{$verify.Dispose()}}catch{$cleanup+=$_.Exception.Message}
|
||||
if($before.Service.State -ne 'Running') {try {Stop-Service AppIDSvc -ErrorAction Stop}catch{Write-Host 'Protected AppIDSvc could not stop; startup mode was untouched. The disposable hosted VM is discarded after this job.'}}
|
||||
$afterService=Get-WelaAppLockerService;if($afterService.StartMode -ne $before.Service.StartMode){$cleanup+='AppIDSvc startup mode changed'}
|
||||
}
|
||||
$log.Dispose()
|
||||
}
|
||||
if($cleanup.Count){throw ('Native cleanup failed: '+($cleanup -join '; '))}
|
||||
if($primary){throw $primary}
|
||||
$cleanupReceipt=[pscustomobject]@{Head=$env:GITHUB_SHA;Engine=[string]$PSVersionTable.PSVersion;PolicyRestored=$true;ChannelRestored=$true;TaskRestored=$true;ServiceBefore=$before.Service;ServiceAfter=(Get-WelaAppLockerService);ServiceStateRestored=($before.Service.State -ceq (Get-WelaAppLockerService).State);ServiceStartupPreserved=($before.Service.StartMode -ceq (Get-WelaAppLockerService).StartMode);ProtectedServiceBoundary='If AppIDSvc refuses Stop, its running state is left for disposable VM teardown; no full service-state rollback claim.'}
|
||||
[IO.File]::WriteAllText((Join-Path $root 'cleanup.json'),($cleanupReceipt|ConvertTo-Json -Depth 8))
|
||||
Write-Host 'Original local/effective GP policy, channel enablement and converter task restored; service startup mode preserved.'
|
||||
$global:LASTEXITCODE=0
|
||||
Reference in new issue
Block a user