[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SmokeDll, [Parameter(Mandatory = $true)] [string]$SceneRoot, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedNativeSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedInteropSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedSmokeSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedPlayoutSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedCoreSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedDepsSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedRuntimeConfigSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedWindowsSdkSha256, [Parameter(Mandatory = $true)] [ValidatePattern('^[0-9A-Fa-f]{64}$')] [string]$ApprovedWinRtSha256, [string]$ExpectedPgmWindowTitle = 'PGM', [ValidateRange(1, 65535)] [int]$Port = 30001, [string]$EvidenceDirectory, [switch]$IUnderstandPgmWillRenderCuts ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $acknowledgement = '--i-understand-this-will-render-current-pgm-tornado-cuts-5001-then-5006-and-take-them-out' $expectedCuts = @{ '5001.t2s' = '99CE3B689A42D8C42BEB09A86FA10C2D7C1AEF4F50D324D81276C1A1E4C4D8A7' '5006.t2s' = '25CD0AE931F51E4E3B84CE3E6FD21A40DB85464F157A23CC3511D63B336D8757' } if (-not $IUnderstandPgmWillRenderCuts) { throw 'The explicit PGM render acknowledgement switch is required.' } $playoutOverrides = @(Get-ChildItem Env: | Where-Object { $_.Name -like 'MBN_STOCK_PLAYOUT_*' }) if ($playoutOverrides.Count -ne 0) { throw 'MBN_STOCK_PLAYOUT environment overrides must be absent.' } $runtimeInjectionOverrides = @(Get-ChildItem Env: | Where-Object { $_.Name -in @( 'DOTNET_STARTUP_HOOKS', 'DOTNET_ADDITIONAL_DEPS', 'DOTNET_SHARED_STORE', 'DOTNET_HOST_PATH', 'CORECLR_ENABLE_PROFILING', 'COR_ENABLE_PROFILING') -or $_.Name -like 'DOTNET_ROOT*' -or $_.Name -like 'CORECLR_*PROFILER*' -or $_.Name -like 'COR_*PROFILER*' }) if ($runtimeInjectionOverrides.Count -ne 0) { throw '.NET startup-hook, additional-dependency, or profiler overrides must be absent.' } $SmokeDll = [IO.Path]::GetFullPath($SmokeDll) $SceneRoot = [IO.Path]::GetFullPath($SceneRoot) if (-not (Test-Path -LiteralPath $SmokeDll -PathType Leaf)) { throw 'The playout smoke assembly does not exist.' } if ([IO.Path]::GetFileName($SmokeDll) -cne 'MBN_STOCK_WEBVIEW.PlayoutSmoke.dll') { throw 'The expected playout smoke assembly name is required.' } $runnerDirectory = [IO.Path]::GetDirectoryName($SmokeDll) $runnerFiles = [ordered]@{ $SmokeDll = $ApprovedSmokeSha256 (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.Playout.dll') = $ApprovedPlayoutSha256 (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.Core.dll') = $ApprovedCoreSha256 (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.PlayoutSmoke.deps.json') = $ApprovedDepsSha256 (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.PlayoutSmoke.runtimeconfig.json') = $ApprovedRuntimeConfigSha256 (Join-Path $runnerDirectory 'Microsoft.Windows.SDK.NET.dll') = $ApprovedWindowsSdkSha256 (Join-Path $runnerDirectory 'WinRT.Runtime.dll') = $ApprovedWinRtSha256 } foreach ($entry in $expectedCuts.GetEnumerator()) { $path = Join-Path $SceneRoot $entry.Key if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Required cut file is missing: $($entry.Key)" } $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash if ($actual -cne $entry.Value) { throw "Required cut file hash does not match: $($entry.Key)" } } $tornado = @(Get-Process -Name 'Tornado2*' -ErrorAction SilentlyContinue) if ($tornado.Count -ne 1 -or $tornado[0].MainWindowTitle -cne $ExpectedPgmWindowTitle) { throw 'Exactly one Tornado2 process with the expected PGM title is required.' } $listener = @(Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction Stop) if ($listener.Count -ne 1 -or $listener[0].OwningProcess -ne $tornado[0].Id) { throw 'The exact Tornado2 process must own the requested KTAP listener.' } if ([string]::IsNullOrWhiteSpace($EvidenceDirectory)) { $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' $EvidenceDirectory = Join-Path $env:TEMP "MBN_STOCK_WEBVIEW_PGM_EVIDENCE_$stamp" } $EvidenceDirectory = [IO.Path]::GetFullPath($EvidenceDirectory) if (Test-Path -LiteralPath $EvidenceDirectory) { throw 'The evidence directory must not already exist.' } [void](New-Item -ItemType Directory -Path $EvidenceDirectory) Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName System.Windows.Forms Add-Type @' using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; public static class PgmEvidenceWindows { private const int GwlExStyle = -20; private const long WsExTopmost = 0x00000008L; public const int SwRestore = 9; public static readonly IntPtr HwndTopmost = new IntPtr(-1); public static readonly IntPtr HwndNotTopmost = new IntPtr(-2); public const uint SwpNoSize = 0x0001; public const uint SwpNoMove = 0x0002; public const uint SwpNoActivate = 0x0010; public const uint SwpShowWindow = 0x0040; [StructLayout(LayoutKind.Sequential)] public struct Point { public int X; public int Y; } [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left; public int Top; public int Right; public int Bottom; } [StructLayout(LayoutKind.Sequential)] public struct WindowPlacement { public int Length; public int Flags; public int ShowCommand; public Point MinimumPosition; public Point MaximumPosition; public Rect NormalPosition; } private delegate bool EnumWindowsCallback(IntPtr window, IntPtr parameter); [DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsCallback callback, IntPtr parameter); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowText(IntPtr window, StringBuilder text, int count); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId); [DllImport("user32.dll")] public static extern bool GetWindowPlacement(IntPtr window, ref WindowPlacement placement); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr window, out Rect rectangle); [DllImport("user32.dll")] public static extern bool SetWindowPlacement(IntPtr window, ref WindowPlacement placement); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr window, int command); [DllImport("user32.dll")] public static extern bool SetWindowPos( IntPtr window, IntPtr insertAfter, int x, int y, int width, int height, uint flags); [DllImport("user32.dll", EntryPoint = "GetWindowLong")] private static extern int GetWindowLong32(IntPtr window, int index); [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] private static extern IntPtr GetWindowLongPtr64(IntPtr window, int index); public static IntPtr FindTopLevelWindow(uint processId, string exactTitle) { IntPtr match = IntPtr.Zero; EnumWindows((window, parameter) => { uint owner; GetWindowThreadProcessId(window, out owner); if (owner != processId) { return true; } var title = new StringBuilder(512); GetWindowText(window, title, title.Capacity); if (String.Equals(title.ToString(), exactTitle, StringComparison.Ordinal)) { match = window; return false; } return true; }, IntPtr.Zero); return match; } public static bool IsTopmost(IntPtr window) { var style = IntPtr.Size == 8 ? GetWindowLongPtr64(window, GwlExStyle).ToInt64() : GetWindowLong32(window, GwlExStyle); return (style & WsExTopmost) != 0; } public static string QuoteArgument(string argument) { if (argument == null) { throw new ArgumentNullException("argument"); } if (argument.Length > 0 && argument.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) < 0) { return argument; } var commandLine = new StringBuilder(); commandLine.Append('"'); var backslashes = 0; foreach (var character in argument) { if (character == '\\') { backslashes++; continue; } if (character == '"') { commandLine.Append('\\', backslashes * 2 + 1); commandLine.Append('"'); backslashes = 0; continue; } commandLine.Append('\\', backslashes); backslashes = 0; commandLine.Append(character); } commandLine.Append('\\', backslashes * 2); commandLine.Append('"'); return commandLine.ToString(); } } '@ function Get-Placement([IntPtr]$Handle) { $placement = New-Object PgmEvidenceWindows+WindowPlacement $placement.Length = [Runtime.InteropServices.Marshal]::SizeOf($placement) if (-not [PgmEvidenceWindows]::GetWindowPlacement($Handle, [ref]$placement)) { throw 'Unable to read Tornado window placement.' } return $placement } function Open-ApprovedFileLease( [string]$Path, [string]$ApprovedSha256) { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Required runner file is missing: $([IO.Path]::GetFileName($Path))" } $stream = [IO.File]::Open( $Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) try { $sha = [Security.Cryptography.SHA256]::Create() try { $bytes = $sha.ComputeHash($stream) } finally { $sha.Dispose() } $actual = -join ($bytes | ForEach-Object { $_.ToString('X2') }) if ($actual -cne $ApprovedSha256.ToUpperInvariant()) { throw "Runner file hash does not match: $([IO.Path]::GetFileName($Path))" } return [pscustomobject]@{ Path = $Path Sha256 = $actual Stream = $stream } } catch { $stream.Dispose() throw } } function Save-Frame([int]$Index, [long]$ElapsedMilliseconds) { $name = 'frame_{0:D4}_{1:D7}ms.png' -f $Index, $ElapsedMilliseconds $path = Join-Path $EvidenceDirectory $name $bitmap = New-Object Drawing.Bitmap ` $script:captureWidth, ` $script:captureHeight, ` ([Drawing.Imaging.PixelFormat]::Format24bppRgb) try { $graphics = [Drawing.Graphics]::FromImage($bitmap) try { $graphics.Clear([Drawing.Color]::Black) $graphics.CopyFromScreen( $script:networkCaptureRect.X, $script:networkCaptureRect.Y, 0, 0, $script:networkCaptureRect.Size) $graphics.CopyFromScreen( $script:pgmCaptureRect.X, $script:pgmCaptureRect.Y, $script:networkCaptureRect.Width + 20, 0, $script:pgmCaptureRect.Size) } finally { $graphics.Dispose() } $bitmap.Save($path, [Drawing.Imaging.ImageFormat]::Png) } finally { $bitmap.Dispose() } return [pscustomobject]@{ file = $name elapsedMilliseconds = $ElapsedMilliseconds sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash } } $pgmHandle = [IntPtr]$tornado[0].MainWindowHandle $networkTitle = -join ([char[]]@( 0xB124, 0xD2B8, 0xC6CC, 0xD06C, 0x20, 0xBAA8, 0xB2C8, 0xD130, 0xB9C1, 0x20, 0xCC3D)) $networkHandle = [PgmEvidenceWindows]::FindTopLevelWindow( [uint32]$tornado[0].Id, $networkTitle) if ($pgmHandle -eq [IntPtr]::Zero -or $networkHandle -eq [IntPtr]::Zero) { throw 'Both the PGM and Network Monitoring windows must already be open.' } $script:networkCaptureRect = New-Object Drawing.Rectangle 20, 20, 1183, 648 $script:pgmCaptureRect = New-Object Drawing.Rectangle 1760, 70, 720, 543 $script:captureWidth = $script:networkCaptureRect.Width + 20 + $script:pgmCaptureRect.Width $script:captureHeight = [Math]::Max( $script:networkCaptureRect.Height, $script:pgmCaptureRect.Height) $primaryBounds = [Windows.Forms.Screen]::PrimaryScreen.Bounds if (-not $primaryBounds.Contains($script:networkCaptureRect) -or -not $primaryBounds.Contains($script:pgmCaptureRect)) { throw 'The primary display cannot contain both evidence windows.' } function Assert-WindowRectangle( [IntPtr]$Handle, [Drawing.Rectangle]$Expected) { $actual = New-Object PgmEvidenceWindows+Rect if (-not [PgmEvidenceWindows]::GetWindowRect($Handle, [ref]$actual) -or $actual.Left -ne $Expected.Left -or $actual.Top -ne $Expected.Top -or ($actual.Right - $actual.Left) -ne $Expected.Width -or ($actual.Bottom - $actual.Top) -ne $Expected.Height) { throw 'A Tornado evidence window was not placed at the verified capture rectangle.' } } $pgmPlacement = Get-Placement $pgmHandle $networkPlacement = Get-Placement $networkHandle $pgmWasTopmost = [PgmEvidenceWindows]::IsTopmost($pgmHandle) $networkWasTopmost = [PgmEvidenceWindows]::IsTopmost($networkHandle) $dotnet = 'C:\Program Files\dotnet\dotnet.exe' if (-not (Test-Path -LiteralPath $dotnet -PathType Leaf)) { throw 'The trusted 64-bit dotnet host was not found.' } $dotnetSignature = Get-AuthenticodeSignature -LiteralPath $dotnet if ($dotnetSignature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or $null -eq $dotnetSignature.SignerCertificate -or $dotnetSignature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') { throw 'The dotnet host signature is not trusted.' } $runnerLeases = [Collections.Generic.List[object]]::new() try { foreach ($runnerFile in $runnerFiles.GetEnumerator()) { $runnerLeases.Add((Open-ApprovedFileLease ` ([string]$runnerFile.Key) ` ([string]$runnerFile.Value))) } } catch { foreach ($lease in $runnerLeases) { $lease.Stream.Dispose() } throw } $frames = [Collections.Generic.List[object]]::new() $output = [Collections.Generic.List[object]]::new() $captureErrors = [Collections.Generic.List[string]]::new() $restorationErrors = [Collections.Generic.List[string]]::new() $process = $null $exitCode = $null $evidenceExitCode = $null $result = $null $stopwatch = [Diagnostics.Stopwatch]::StartNew() try { [void][PgmEvidenceWindows]::ShowWindow($networkHandle, [PgmEvidenceWindows]::SwRestore) [void][PgmEvidenceWindows]::ShowWindow($pgmHandle, [PgmEvidenceWindows]::SwRestore) if (-not [PgmEvidenceWindows]::SetWindowPos( $networkHandle, [PgmEvidenceWindows]::HwndTopmost, $script:networkCaptureRect.X, $script:networkCaptureRect.Y, $script:networkCaptureRect.Width, $script:networkCaptureRect.Height, [PgmEvidenceWindows]::SwpNoActivate -bor [PgmEvidenceWindows]::SwpShowWindow)) { throw 'Unable to place the Network Monitoring window for evidence capture.' } if (-not [PgmEvidenceWindows]::SetWindowPos( $pgmHandle, [PgmEvidenceWindows]::HwndTopmost, $script:pgmCaptureRect.X, $script:pgmCaptureRect.Y, $script:pgmCaptureRect.Width, $script:pgmCaptureRect.Height, [PgmEvidenceWindows]::SwpNoActivate -bor [PgmEvidenceWindows]::SwpShowWindow)) { throw 'Unable to place the PGM window for evidence capture.' } Assert-WindowRectangle $networkHandle $script:networkCaptureRect Assert-WindowRectangle $pgmHandle $script:pgmCaptureRect Start-Sleep -Milliseconds 750 Assert-WindowRectangle $networkHandle $script:networkCaptureRect Assert-WindowRectangle $pgmHandle $script:pgmCaptureRect for ($index = 0; $index -lt 4; $index++) { $frames.Add((Save-Frame $index $stopwatch.ElapsedMilliseconds)) Start-Sleep -Milliseconds 500 } $start = [Diagnostics.ProcessStartInfo]::new() $start.FileName = $dotnet $start.UseShellExecute = $false $start.CreateNoWindow = $true $start.RedirectStandardOutput = $true $start.RedirectStandardError = $true $runnerArguments = @( $SmokeDll, '--pgm-cuts-sequence', $acknowledgement, '--host', '127.0.0.1', '--port', $Port.ToString([Globalization.CultureInfo]::InvariantCulture), '--expected-pgm-window-title', $ExpectedPgmWindowTitle, '--scene-root', $SceneRoot, '--observe-ms', '5000') $start.Arguments = ($runnerArguments | ForEach-Object { [PgmEvidenceWindows]::QuoteArgument([string]$_) }) -join ' ' $start.EnvironmentVariables['MBN_STOCK_K3D_NATIVE_SHA256'] = $ApprovedNativeSha256.ToUpperInvariant() $start.EnvironmentVariables['MBN_STOCK_K3D_INTEROP_SHA256'] = $ApprovedInteropSha256.ToUpperInvariant() $process = [Diagnostics.Process]::new() $process.StartInfo = $start if (-not $process.Start()) { throw 'Unable to launch the PGM sequence runner.' } $processStartElapsed = $stopwatch.ElapsedMilliseconds $frameIndex = 4 $captureEnabled = $true try { $stdoutRead = $process.StandardOutput.ReadLineAsync() $stderrRead = $process.StandardError.ReadLineAsync() $nextFrameAt = $stopwatch.ElapsedMilliseconds $stdoutEnded = $false $stderrEnded = $false while (-not $process.HasExited -or -not $stdoutEnded -or -not $stderrEnded) { if (-not $stdoutEnded -and $stdoutRead.IsCompleted) { try { $line = $stdoutRead.GetAwaiter().GetResult() } catch { $captureErrors.Add('stdout-read-failed') $stdoutEnded = $true $line = $null } if ($null -eq $line) { $stdoutEnded = $true } else { $output.Add([pscustomobject]@{ stream = 'stdout' elapsedMilliseconds = $stopwatch.ElapsedMilliseconds line = $line }) $stdoutRead = $process.StandardOutput.ReadLineAsync() } } if (-not $stderrEnded -and $stderrRead.IsCompleted) { try { $line = $stderrRead.GetAwaiter().GetResult() } catch { $captureErrors.Add('stderr-read-failed') $stderrEnded = $true $line = $null } if ($null -eq $line) { $stderrEnded = $true } else { $output.Add([pscustomobject]@{ stream = 'stderr' elapsedMilliseconds = $stopwatch.ElapsedMilliseconds line = $line }) $stderrRead = $process.StandardError.ReadLineAsync() } } if ($captureEnabled -and $stopwatch.ElapsedMilliseconds -ge $nextFrameAt) { try { $frames.Add((Save-Frame $frameIndex $stopwatch.ElapsedMilliseconds)) $frameIndex++ } catch { $captureErrors.Add('frame-capture-failed') $captureEnabled = $false } $nextFrameAt += 500 } Start-Sleep -Milliseconds 20 } } catch { $captureErrors.Add('supervision-loop-failed') while (-not $process.HasExited) { Start-Sleep -Milliseconds 100 } } $process.WaitForExit() $exitCode = $process.ExitCode Start-Sleep -Milliseconds 500 if ($captureEnabled) { try { $frames.Add((Save-Frame $frameIndex $stopwatch.ElapsedMilliseconds)) } catch { $captureErrors.Add('final-frame-capture-failed') } } $validationErrors = [Collections.Generic.List[string]]::new() $stdoutEntries = @($output | Where-Object { $_.stream -eq 'stdout' }) $markers = [Collections.Generic.List[object]]::new() $lastMarkerIndex = -1 for ($index = 0; $index -lt $stdoutEntries.Count; $index++) { $entry = $stdoutEntries[$index] if ($entry.line -notmatch '"event"\s*:\s*"observation-start"') { continue } try { $marker = $entry.line | ConvertFrom-Json -ErrorAction Stop $markers.Add([pscustomobject]@{ stage = [string]$marker.stage elapsedMilliseconds = [long]$entry.elapsedMilliseconds }) $lastMarkerIndex = $index } catch { $validationErrors.Add('observation-marker-json-invalid') } } $markerStages = @($markers | ForEach-Object { $_.stage }) $expectedMarkerStages = @('first', 'next', 'take-out') if ($markerStages.Count -ne $expectedMarkerStages.Count -or ($markerStages -join '|') -cne ($expectedMarkerStages -join '|')) { $validationErrors.Add('observation-marker-order-invalid') } $terminalEntries = if ($lastMarkerIndex + 1 -lt $stdoutEntries.Count) { @($stdoutEntries[($lastMarkerIndex + 1)..($stdoutEntries.Count - 1)]) } else { @() } $terminal = $null if ($terminalEntries.Count -ne 0) { try { $terminal = ($terminalEntries.line -join [Environment]::NewLine) | ConvertFrom-Json -ErrorAction Stop } catch { $validationErrors.Add('terminal-report-json-invalid') } } $observationIntervals = @() if ($markers.Count -eq 3 -and $terminalEntries.Count -ne 0) { $observationIntervals = @( ([long]$markers[1].elapsedMilliseconds - [long]$markers[0].elapsedMilliseconds), ([long]$markers[2].elapsedMilliseconds - [long]$markers[1].elapsedMilliseconds), ([long]$terminalEntries[0].elapsedMilliseconds - [long]$markers[2].elapsedMilliseconds) ) if (@($observationIntervals | Where-Object { $_ -lt 4900 }).Count -ne 0) { $validationErrors.Add('observation-duration-too-short') } } else { $validationErrors.Add('observation-duration-unavailable') } if ($null -eq $terminal -or $terminal.command -ne 'pgm-cuts-sequence') { $validationErrors.Add('terminal-report-missing-or-invalid') } else { $actualCommandStages = @($terminal.commands | ForEach-Object { '{0}:{1}' -f $_.stage, $_.code }) $expectedCommandStages = @( 'connect:Success', 'prepare-first:Success', 'take-in:Success', 'next:Success', 'take-out:Success', 'disconnect:Success') if ($actualCommandStages.Count -ne $expectedCommandStages.Count -or ($actualCommandStages -join '|') -cne ($expectedCommandStages -join '|')) { $validationErrors.Add('terminal-command-order-invalid') } if ($terminal.completed -ne $true -or $terminal.outcomeUnknown -ne $false -or $terminal.renderCommandAttempted -ne $true -or $terminal.disconnectAttempted -ne $true -or $terminal.quarantineAttempted -ne $false) { $validationErrors.Add('terminal-safety-state-invalid') } } if ($exitCode -ne 0) { $validationErrors.Add('runner-exit-code-nonzero') } if ($frames.Count -lt 30) { $validationErrors.Add('insufficient-frame-count') } if ($captureErrors.Count -ne 0) { $validationErrors.Add('evidence-capture-incomplete') } $terminalValidated = $validationErrors.Count -eq 0 $evidenceExitCode = if ($exitCode -ne 0) { $exitCode } elseif ( -not $terminalValidated) { 6 } else { 0 } $manifest = [ordered]@{ schemaVersion = 1 createdAtUtc = [DateTimeOffset]::UtcNow.ToString('O') processStartElapsedMilliseconds = $processStartElapsed runnerExitCode = $exitCode evidenceExitCode = $evidenceExitCode terminalValidated = $terminalValidated runnerFilesSha256 = @($runnerLeases | ForEach-Object { [ordered]@{ file = [IO.Path]::GetFileName($_.Path) sha256 = $_.Sha256 } }) cutSha256 = $expectedCuts frames = $frames captureErrors = $captureErrors validationErrors = $validationErrors observationIntervalsMilliseconds = $observationIntervals runnerOutput = $output } $manifestPath = Join-Path $EvidenceDirectory 'manifest.json' [IO.File]::WriteAllText( $manifestPath, ($manifest | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false)) $manifestHash = (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash $result = [pscustomobject]@{ EvidenceDirectory = $EvidenceDirectory RunnerExitCode = $exitCode TerminalValidated = $terminalValidated FrameCount = $frames.Count ManifestSha256 = $manifestHash } } finally { if ($null -ne $process) { $process.Dispose() } foreach ($lease in $runnerLeases) { $lease.Stream.Dispose() } if (-not [PgmEvidenceWindows]::SetWindowPos( $networkHandle, $(if ($networkWasTopmost) { [PgmEvidenceWindows]::HwndTopmost } else { [PgmEvidenceWindows]::HwndNotTopmost }), 0, 0, 0, 0, [PgmEvidenceWindows]::SwpNoActivate -bor [PgmEvidenceWindows]::SwpNoMove -bor [PgmEvidenceWindows]::SwpNoSize)) { $restorationErrors.Add('network-topmost-restore-failed') } if (-not [PgmEvidenceWindows]::SetWindowPos( $pgmHandle, $(if ($pgmWasTopmost) { [PgmEvidenceWindows]::HwndTopmost } else { [PgmEvidenceWindows]::HwndNotTopmost }), 0, 0, 0, 0, [PgmEvidenceWindows]::SwpNoActivate -bor [PgmEvidenceWindows]::SwpNoMove -bor [PgmEvidenceWindows]::SwpNoSize)) { $restorationErrors.Add('pgm-topmost-restore-failed') } if (-not [PgmEvidenceWindows]::SetWindowPlacement( $networkHandle, [ref]$networkPlacement)) { $restorationErrors.Add('network-placement-restore-failed') } if (-not [PgmEvidenceWindows]::SetWindowPlacement( $pgmHandle, [ref]$pgmPlacement)) { $restorationErrors.Add('pgm-placement-restore-failed') } } if ($null -ne $result) { $result | Add-Member -NotePropertyName RestorationSucceeded ` -NotePropertyValue ($restorationErrors.Count -eq 0) } if ($restorationErrors.Count -ne 0) { Write-Warning ($restorationErrors -join ',') } $result if ($evidenceExitCode -ne 0) { exit $evidenceExitCode }