# Scoped machine policy for the built-in Windows PowerShell 5.1 engine. function ConvertTo-WelaPsLoggingKey { param($Value) ConvertTo-Json -InputObject $Value -Depth 24 -Compress } function Assert-WelaPsLoggingSelection { param([string]$Action,[string[]]$Control,[string[]]$ModuleName) if ($null -eq $Control) {$Control=@()}; if ($null -eq $ModuleName) {$ModuleName=@()} if ($Action -ne 'Audit' -and -not $Control.Count) { throw 'Plan and Configure require explicit PowerShellLoggingControl selection.' } if (@($Control | ForEach-Object {$_.ToLowerInvariant()} | Select-Object -Unique).Count -ne $Control.Count -or @($Control | Where-Object {$_ -notin @('ScriptBlock','Module')}).Count) { throw 'Select unique ScriptBlock and/or Module controls.' } if ($ModuleName.Count -and $Control -notcontains 'Module') { throw 'Module names require explicit Module selection.' } if ($Action -ne 'Audit' -and $Control -contains 'Module' -and -not $ModuleName.Count) { throw 'Module selection requires explicit PowerShellLoggingModuleName values; use * only after reviewing all-module scope.' } if ($ModuleName.Count -gt 32 -or @($ModuleName | ForEach-Object {$_.ToLowerInvariant()} | Select-Object -Unique).Count -ne $ModuleName.Count) { throw 'Select at most 32 unique module names.' } foreach ($name in $ModuleName) { if ($name -cne '*' -and $name -cnotmatch '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$') { throw 'Use literal module names or the explicitly selected * all-module value; paths and other wildcard patterns are refused.' } } } function Get-WelaPsLoggingTree { param([ValidateSet('LocalMachine','CurrentUser')][string]$Hive,[ValidateSet('Registry64','Registry32')][string]$View,[string]$Root) $base=$null;$rows=New-Object 'System.Collections.Generic.List[object]';$queue=New-Object 'System.Collections.Generic.Queue[string]';$queue.Enqueue('') try { $base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::$Hive,[Microsoft.Win32.RegistryView]::$View) while ($queue.Count) { if ($rows.Count -ge 64) { throw 'PowerShell policy tree exceeds 64 keys; no partial snapshot is accepted.' } $relative=$queue.Dequeue();$path=$Root;if ($relative) {$path+='\'+$relative};$key=$null try { $key=$base.OpenSubKey($path,$false) if (-not $key) { if ($relative) {throw 'Policy key disappeared during enumeration.'};return [pscustomobject]@{Exists=$false;Keys=@()} } $names=@($key.GetValueNames()|Sort-Object);$children=@($key.GetSubKeyNames()|Sort-Object) if ($names.Count -gt 128 -or $children.Count -gt 64) {throw 'PowerShell policy key inventory is too large.'} $values=@(foreach ($name in $names) {[pscustomobject][ordered]@{Name=$name;Type=$key.GetValueKind($name).ToString();Value=$key.GetValue($name,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}}) $acl=if ($PSVersionTable.PSVersion.Major -ge 6) {[Microsoft.Win32.RegistryAclExtensions]::GetAccessControl($key)}else{$key.GetAccessControl()} $rows.Add([pscustomobject][ordered]@{Path=$relative;Values=$values;Children=$children;Access=$acl.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]'Owner,Group,Access')}) foreach ($child in $children) {$next=if($relative){$relative+'\'+$child}else{$child};if($next.Split('\').Count -gt 8){throw 'PowerShell policy tree exceeds eight levels.'};$queue.Enqueue($next)} } finally {if($key){$key.Dispose()}} } $result=[pscustomobject]@{Exists=$true;Keys=@($rows.ToArray()|Sort-Object Path)} if ((ConvertTo-WelaPsLoggingKey $result).Length -gt 1048576) {throw 'PowerShell policy snapshot exceeds one Mi character bound.'} return $result } finally {if($base){$base.Dispose()}} } function Get-WelaPsLoggingSources { $root=Split-Path $PSScriptRoot -Parent @(foreach ($name in @('WELA.ps1','scripts/PowerShellLogging.ps1','scripts/Configuration.ps1')) {[pscustomobject]@{Path=$name;Sha256=(Get-FileHash -LiteralPath (Join-Path $root $name) -Algorithm SHA256).Hash.ToLowerInvariant()}}) } 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.'} $build=[int]$os.BuildNumber;$product=[int]$os.ProductType;$role=[int]$computer.DomainRole;$joined=$computer.PartOfDomain $coherent=($product -eq 1 -and (($role -eq 0 -and -not $joined) -or ($role -eq 1 -and $joined))) -or ($product -eq 3 -and (($role -eq 2 -and -not $joined) -or ($role -eq 3 -and $joined))) -or ($product -eq 2 -and $role -in @(4,5) -and $joined) if (-not $coherent -or [string]::IsNullOrWhiteSpace($computer.Name)) {throw 'Native host role observations conflict.'} if (-not (($product -eq 1 -and $build -in @(22000,22621,22631,26100,26200)) -or ($product -in @(2,3) -and $build -in @(20348,26100)))) {throw 'Windows role/build is outside reviewed scope.'} $engine=Get-WelaRegistryState 'HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine' PowerShellVersion if (-not $engine.ValueExists -or $engine.Type -cne 'String' -or $engine.Value -notmatch '^5\.1(?:\.|$)') {throw 'Installed Windows PowerShell 5.1 is not confirmed.'} $exe=Join-Path $env:windir 'System32\WindowsPowerShell\v1.0\powershell.exe' $engineHash=(Get-FileHash -LiteralPath $exe -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant() if (-not (Get-WelaRegistryState 'HKLM:\SOFTWARE\Policies\Microsoft\Windows' '__WelaObservationOnly').KeyExists) {throw 'Existing Microsoft Windows policy parent key is required; unrecorded ancestors will not be created.'} $windowsRoot='SOFTWARE\Policies\Microsoft\Windows\PowerShell';$coreRoot='SOFTWARE\Policies\Microsoft\PowerShellCore' $machine=Get-WelaPsLoggingTree LocalMachine Registry64 $windowsRoot;$user=Get-WelaPsLoggingTree CurrentUser Registry64 $windowsRoot if ((ConvertTo-WelaPsLoggingKey $machine) -cne (ConvertTo-WelaPsLoggingKey (Get-WelaPsLoggingTree LocalMachine Registry32 $windowsRoot)) -or (ConvertTo-WelaPsLoggingKey $user) -cne (ConvertTo-WelaPsLoggingKey (Get-WelaPsLoggingTree CurrentUser Registry32 $windowsRoot))) {throw 'Shared Windows PowerShell policy views disagree.'} $coreMachine=Get-WelaPsLoggingTree LocalMachine Registry64 $coreRoot;$coreUser=Get-WelaPsLoggingTree CurrentUser Registry64 $coreRoot $protected=Get-WelaPsLoggingTree LocalMachine Registry64 'SOFTWARE\Policies\Microsoft\Windows\EventLog\ProtectedEventLogging' $channel=$null 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]@{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) $keys=@($Tree.Keys|Where-Object Path -ieq $Path) if ($keys.Count -gt 1) {throw 'Ambiguous policy key.'} if ($keys.Count -eq 1) {$rows=@($keys[0].Values|Where-Object Name -ieq $Name);if($rows.Count -gt 1){throw 'Ambiguous policy value.'};if($rows.Count){return $rows[0]}} return $null } function Get-WelaPsLoggingDefinitions { param([string[]]$Control,[string[]]$ModuleName) # Add selected module entries before enabling module logging. No existing name is removed. if ($Control -contains 'Module') { foreach ($name in @($ModuleName|Sort-Object)) {[pscustomobject]@{Control='Module';Path='ModuleLogging\ModuleNames';Name=$name;Type='String';Value=$name}} [pscustomobject]@{Control='Module';Path='ModuleLogging';Name='EnableModuleLogging';Type='DWord';Value=1} } if ($Control -contains 'ScriptBlock') {[pscustomobject]@{Control='ScriptBlock';Path='ScriptBlockLogging';Name='EnableScriptBlockLogging';Type='DWord';Value=1}} } function Test-WelaPsLoggingValue {param($Snapshot,$Definition) $value=Get-WelaPsLoggingValue $Snapshot.Machine $Definition.Path $Definition.Name;return $null -ne $value -and $value.Type -ceq $Definition.Type -and (ConvertTo-WelaPsLoggingKey $value.Value) -ceq (ConvertTo-WelaPsLoggingKey $Definition.Value)} function Assert-WelaPsLoggingCapacity { param($Snapshot,[array]$Definitions) # Project predictable inventory growth before writing; new inherited access descriptors # still require native readback. Projection never mutates the captured original state. # JSON round-trips can reinterpret literal ISO-date registry strings as DateTime. $tree=[pscustomobject]@{Exists=$Snapshot.Machine.Exists;Keys=@()};$rows=@{} foreach($row in $Snapshot.Machine.Keys){ $values=@(foreach($entry in $row.Values){$data=$entry.Value;if($data -is [Array]){$data=$data.Clone()};[pscustomobject]@{Name=$entry.Name;Type=$entry.Type;Value=$data}}) $rows[$row.Path]=[pscustomobject]@{Path=$row.Path;Values=$values;Children=@($row.Children);Access=$row.Access} } foreach($definition in $Definitions){ $paths=@('');$path='';foreach($part in $definition.Path.Split('\')){$path=if($path){$path+'\'+$part}else{$part};$paths+=$path} foreach($path in $paths){ if(-not $rows.ContainsKey($path)){$rows[$path]=[pscustomobject]@{Path=$path;Values=@();Children=@();Access=''}} if($path){$separator=$path.LastIndexOf('\');$parent=if($separator -ge 0){$path.Substring(0,$separator)}else{''};$leaf=if($separator -ge 0){$path.Substring($separator+1)}else{$path};$rows[$parent].Children=@(@($rows[$parent].Children)+$leaf|Sort-Object -Unique)} } $row=$rows[$definition.Path];$row.Values=@($row.Values|Where-Object Name -ine $definition.Name)+[pscustomobject]@{Name=$definition.Name;Type=$definition.Type;Value=$definition.Value} if($row.Values.Count -gt 128){throw 'Selected changes exceed the 128-value policy inventory capacity; no write is safe.'} } if($rows.Count -gt 64){throw 'Selected changes exceed the 64-key policy inventory capacity; no write is safe.'} $tree.Exists=($rows.Count -gt 0);$tree.Keys=@($rows.Values|Sort-Object Path) if((ConvertTo-WelaPsLoggingKey $tree).Length -gt 1048576){throw 'Selected changes exceed the policy snapshot character capacity; no write is safe.'} } function Assert-WelaPsLoggingKnown { param($Snapshot,[array]$Definitions) foreach ($definition in $Definitions) { $value=Get-WelaPsLoggingValue $Snapshot.Machine $definition.Path $definition.Name if ($value -and ($value.Type -cne $definition.Type -or ($definition.Type -eq 'DWord' -and $value.Value -notin @(0,1)) -or ($definition.Type -eq 'String' -and $value.Value -cne $definition.Value))) {throw "Selected policy value has an unknown type/value or a name collision: $($definition.Path)/$($definition.Name)."} } Assert-WelaPsLoggingCapacity $Snapshot $Definitions if (@($Definitions|Where-Object Control -eq Module).Count) { foreach($key in @($Snapshot.Machine.Keys|Where-Object Path -ieq 'ModuleLogging\ModuleNames')) {foreach($value in $key.Values) {if($value.Type -cne 'String' -or [string]::IsNullOrWhiteSpace($value.Value)){throw 'Existing module-name policy contains an unsupported type/empty value; preserve and review it.'}}} } } function Set-WelaPsLoggingValue { param($Definition) if ($Definition.Path -notin @('ModuleLogging','ModuleLogging\ModuleNames','ScriptBlockLogging')) {throw 'Unsupported policy destination.'} if (($Definition.Path -eq 'ModuleLogging' -and ($Definition.Name -cne 'EnableModuleLogging' -or $Definition.Type -cne 'DWord' -or $Definition.Value -ne 1)) -or ($Definition.Path -eq 'ScriptBlockLogging' -and ($Definition.Name -cne 'EnableScriptBlockLogging' -or $Definition.Type -cne 'DWord' -or $Definition.Value -ne 1))) {throw 'Unsupported logging DWORD mutation.'} if ($Definition.Path -eq 'ModuleLogging\ModuleNames') {Assert-WelaPsLoggingSelection Configure @('Module') @($Definition.Name);if($Definition.Type -cne 'String' -or $Definition.Value -cne $Definition.Name){throw 'Unsupported module-name mutation.'}} $base=$null;$key=$null try {$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64);$key=$base.CreateSubKey('SOFTWARE\Policies\Microsoft\Windows\PowerShell\'+$Definition.Path);$key.SetValue($Definition.Name,$Definition.Value,[Microsoft.Win32.RegistryValueKind]::$($Definition.Type));$key.Flush()} finally{if($key){$key.Dispose()};if($base){$base.Dispose()}} } function Assert-WelaPsLoggingTransition { param($Before,$After,$Definition) foreach($property in $Before.PSObject.Properties.Name|Where-Object {$_ -cne 'Machine'}) {if((ConvertTo-WelaPsLoggingKey $Before.$property) -cne (ConvertTo-WelaPsLoggingKey $After.$property)){throw "Unselected state changed: $property"}} if (-not (Test-WelaPsLoggingValue $After $Definition)) {throw 'Selected policy value did not match native readback.'} $allowed=@('');$parts=$Definition.Path.Split('\');$part='';foreach($segment in $parts){$part=if($part){$part+'\'+$segment}else{$segment};$allowed+=$part} $old=@{};foreach($row in $Before.Machine.Keys){$old[$row.Path]=$row} $new=@{};foreach($row in $After.Machine.Keys){$new[$row.Path]=$row} foreach($path in $old.Keys){if(-not $new.ContainsKey($path)){throw 'An original policy key disappeared.'};if($new[$path].Access -cne $old[$path].Access){throw 'An existing policy key access descriptor changed.'}} foreach($path in $new.Keys){ if(-not $old.ContainsKey($path) -and $path -notin $allowed){throw 'An unrequested policy key appeared.'} $expected=@();if($old.ContainsKey($path)){$expected=@($old[$path].Values|Where-Object {-not ($path -ieq $Definition.Path -and $_.Name -ieq $Definition.Name)})} $observed=@($new[$path].Values|Where-Object {-not ($path -ieq $Definition.Path -and $_.Name -ieq $Definition.Name)}) if((ConvertTo-WelaPsLoggingKey $expected) -cne (ConvertTo-WelaPsLoggingKey $observed)){throw 'Unrelated policy values changed.'} $expectedChildren=@();if($old.ContainsKey($path)){$expectedChildren=@($old[$path].Children)} foreach($possible in $allowed){if(-not $possible){continue};$separator=$possible.LastIndexOf('\');$parent=if($separator -ge 0){$possible.Substring(0,$separator)}else{''};$leaf=if($separator -ge 0){$possible.Substring($separator+1)}else{$possible};if($parent -ieq $path){$expectedChildren+= $leaf}} if((ConvertTo-WelaPsLoggingKey @($expectedChildren|Sort-Object -Unique)) -cne (ConvertTo-WelaPsLoggingKey @($new[$path].Children|Sort-Object -Unique))){throw 'Unrelated policy subkeys changed.'} } } function Invoke-WelaPowerShellLogging { param([ValidateSet('Audit','Plan','Configure')][string]$Action='Audit',[string[]]$Control=@(),[string[]]$ModuleName=@(),[switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath) Assert-WelaPsLoggingSelection $Action $Control $ModuleName if($Action -ne 'Configure' -and ($Auto -or $DryRun -or $BackupPath)){throw 'Consent, dry-run and backup options require PowerShellLoggingAction Configure.'} if($ResultsPath){$ResultsPath=$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ResultsPath);if(Test-Path -LiteralPath $ResultsPath){throw 'ResultsPath must name a new file.'};if(-not (Test-Path -LiteralPath ([IO.Path]::GetDirectoryName($ResultsPath)) -PathType Container)){throw 'ResultsPath parent must already exist.'}} $definitions=@(Get-WelaPsLoggingDefinitions $Control $ModuleName);$before=$null;$diagnostic='';$known=$false try {$before=Get-WelaPsLoggingSnapshot;Assert-WelaPsLoggingKnown $before $definitions;$known=$true}catch{$diagnostic=$_.Exception.Message} $plan=[pscustomobject]@{Selection=@($Control);ModuleNames=@($ModuleName);Before=$before;Controls=@(foreach($definition in $definitions){[pscustomobject]@{Definition=$definition;Status=$(if(-not $known){'Unknown'}elseif(Test-WelaPsLoggingValue $before $definition){'AlreadyCompliant'}else{'ChangeRequired'})}});Status=$(if($known){'Observed'}else{'Unknown'});Diagnostic=$diagnostic;Provenance=@('https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_group_policy_settings?view=powershell-5.1','https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-powershellexecutionpolicy');Meaning='Explicit Windows PowerShell 5.1 machine-policy selection. Existing module names remain active when Module logging is enabled; no claim of a complete Microsoft/CIS/ASD baseline.'} if($Action -eq 'Configure') { if(-not $known){$report=[pscustomobject]@{ExitCode=1;Scope='windows-powershell-event-logging-policy-only';Results=@();Diagnostic=$diagnostic}} else { $context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath $shared=@{Expected=$before;Definitions=$definitions;Failed=$false;StopReason=$null} foreach($definition in $definitions){ if($shared.StopReason){$context.Results.Add([pscustomobject]@{Id=('PowerShellLogging/'+$definition.Path+'/'+$definition.Name);Kind='Registry';Target=@{Path=$definition.Path;Name=$definition.Name};Desired=@{Type=$definition.Type;Value=$definition.Value};Before=$null;After=$null;Status='Skipped';Diagnostic=$shared.StopReason});continue} $state=@{Shared=$shared;Definition=$definition} $read={param($s) if($s.Shared.Failed){throw 'An earlier operation failed; remaining operations are stopped.'};$snapshot=Get-WelaPsLoggingSnapshot;if((ConvertTo-WelaPsLoggingKey $snapshot) -cne (ConvertTo-WelaPsLoggingKey $s.Shared.Expected)){throw 'Policy, host, channel, engine or source changed from the reviewed state.'};Assert-WelaPsLoggingKnown $snapshot $s.Shared.Definitions;return $snapshot} $test={param($snapshot,$s) Test-WelaPsLoggingValue $snapshot $s.Definition} $apply={param($s) try {$fresh=Get-WelaPsLoggingSnapshot;if((ConvertTo-WelaPsLoggingKey $fresh) -cne (ConvertTo-WelaPsLoggingKey $s.Shared.Expected)){throw 'Pre-write state drifted after journal/approval; no write attempted.'};Set-WelaPsLoggingValue $s.Definition;$after=Get-WelaPsLoggingSnapshot;Assert-WelaPsLoggingTransition $fresh $after $s.Definition;$s.Shared.Expected=$after;'Only the named Windows PowerShell policy value was changed and read back.'}catch{$s.Shared.Failed=$true;throw} } Invoke-WelaConfigurationControl -Context $context -Id ('PowerShellLogging/'+$definition.Path+'/'+$definition.Name) -Kind Registry -Target @{Hive='LocalMachine';View='Registry64';Path=('SOFTWARE\Policies\Microsoft\Windows\PowerShell\'+$definition.Path);Name=$definition.Name} -Desired @{Type=$definition.Type;Value=$definition.Value} -Read $read -Compliant $test -Apply $apply -CallbackState $state -Description 'Enable the explicitly selected event-logging policy; existing module names are preserved.' if($context.Results[$context.Results.Count-1].Status -eq 'Failed'){$shared.Failed=$true;$shared.StopReason='Not attempted because an earlier selected operation failed.'} if($context.Results[$context.Results.Count-1].Status -eq 'Skipped' -and -not $DryRun){$shared.StopReason='Not attempted because an earlier selected operation was declined.'} } $report=Complete-WelaConfiguration -Context $context -Scope 'windows-powershell-event-logging-policy-only' -SuccessMessage 'Selected local machine policy values verified; fresh-session events and policy persistence remain separate.' } }else{$report=[pscustomobject]@{ExitCode=$(if($known){0}else{1});Scope='windows-powershell-event-logging-policy-only'}} $report|Add-Member NoteProperty Action $Action;$report|Add-Member NoteProperty Plan $plan $report|Add-Member NoteProperty EventGeneration 'Unverified; run a separately reviewed new Windows PowerShell session and retain native XML.' $report|Add-Member NoteProperty PowerShell7Sessions 'Not assessed. Separate PowerShell Core policy/configuration and Windows-policy fallback are preserved; fallback users can inherit changed Windows settings.' $report|Add-Member NoteProperty PolicyAuthority 'Local registry observations only; GPO/MDM persistence and current winning authority are not established.' $report|Add-Member NoteProperty ReadyRuleCredit 0 if($ResultsPath){$bytes=[Text.UTF8Encoding]::new($false).GetBytes(($report|ConvertTo-Json -Depth 28));$file=[IO.File]::Open($ResultsPath,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None);try{$file.Write($bytes,0,$bytes.Length)}finally{$file.Dispose()}} return $report }