# Optional read-only process inventory. Never launches, closes or controls a GUI. #Requires -Version 7.0 $ErrorActionPreference = 'Stop' $trialPath = Join-Path $PSScriptRoot 'trial' $resultPath = Join-Path $trialPath 'loaded-modules.json' if (Test-Path -LiteralPath $resultPath) { throw 'Preserve existing module receipt.' } $launch = Get-Content -LiteralPath (Join-Path $trialPath 'launch.json') -Raw | ConvertFrom-Json $identity = Get-Content -LiteralPath (Join-Path $trialPath 'process-identity.json') -Raw | ConvertFrom-Json $before = Get-Content -LiteralPath (Join-Path $trialPath 'package-before.json') -Raw | ConvertFrom-Json $receipt = [ordered]@{ atUtc = [DateTime]::UtcNow.ToString('o'); pid = $launch.pid; status = 'PENDING'; modules = @(); scope = 'Point-in-time loaded modules of the exact launched process; no GUI action or universal runtime dependency claim.' } try { $guiProcess = Get-Process -Id $launch.pid -ErrorAction Stop if (-not [string]::Equals($guiProcess.Path, $identity.path, [StringComparison]::OrdinalIgnoreCase) -or $guiProcess.StartTime.ToUniversalTime().ToFileTimeUtc() -ne $identity.creation_filetime) { throw 'PID no longer identifies the original launch.' } $receipt.creation_filetime = $identity.creation_filetime $systemRoot = [IO.Path]::GetFullPath($launch.environment.SystemRoot).TrimEnd('\') + '\' $packageRoot = [IO.Path]::GetFullPath($launch.package).TrimEnd('\') + '\' $items = foreach ($module in $guiProcess.Modules) { $full = [IO.Path]::GetFullPath($module.FileName) $inWindows = $full.StartsWith($systemRoot, [StringComparison]::OrdinalIgnoreCase) $inPackage = $full.StartsWith($packageRoot, [StringComparison]::OrdinalIgnoreCase) $hash = (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() if ($inPackage) { $relative = $full.Substring($packageRoot.Length).Replace('\', '/') $recorded = $before.PSObject.Properties[$relative] if (-not $recorded -or $recorded.Value -cne $hash) { throw "Package module differs: $relative" } } [ordered]@{ name = $module.ModuleName; path = $full; sha256 = $hash; windowsPath = $inWindows; packagePath = $inPackage } } $receipt.modules = @($items) if (-not $receipt.modules.Count) { throw 'No modules observed.' } $unexpected = @($items | Where-Object { -not $_.windowsPath -and -not $_.packagePath }) $receipt.unexpected = $unexpected if ($unexpected.Count) { throw 'Unexpected non-Windows, non-package module; inspect receipt.' } $receipt.status = 'PASS' } catch { $receipt.status = 'FAIL' $receipt.error = $_.ToString() } finally { [IO.File]::WriteAllText($resultPath, ($receipt | ConvertTo-Json -Depth 10) + "`n", [Text.UTF8Encoding]::new($false)) } if ($receipt.status -ne 'PASS') { throw $receipt.error }