117 lines
4.7 KiB
PowerShell
117 lines
4.7 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Captures one launcher child PID by its unique isolated session-config path.
|
|
|
|
.DESCRIPTION
|
|
Writes a sanitized gate-only sidecar. It never reads the session-config
|
|
contents and records no command line, account, character, or credential.
|
|
#>
|
|
[CmdletBinding(DefaultParameterSetName = 'Path')]
|
|
param(
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Path')]
|
|
[string]$SessionConfigPath,
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Directory')]
|
|
[string]$SessionsDirectory,
|
|
[Parameter(ParameterSetName = 'Directory')]
|
|
[DateTimeOffset]$CreatedAfterUtc = [DateTimeOffset]::MinValue,
|
|
[Parameter(Mandatory = $true)][string]$ReportPath,
|
|
[ValidateRange(1, 60)][int]$WaitSeconds = 10
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($PSVersionTable.PSVersion.Major -lt 7) {
|
|
throw 'Campaign LA PID capture requires PowerShell 7 or newer.'
|
|
}
|
|
. (Join-Path $PSScriptRoot 'CampaignLaProcessCorrelation.ps1')
|
|
|
|
if ($PSCmdlet.ParameterSetName -eq 'Path') {
|
|
if (-not [IO.Path]::IsPathFullyQualified($SessionConfigPath)) {
|
|
throw '-SessionConfigPath must be absolute.'
|
|
}
|
|
$SessionConfigPath = [IO.Path]::GetFullPath($SessionConfigPath)
|
|
if (-not (Test-Path -LiteralPath $SessionConfigPath -PathType Leaf)) {
|
|
throw "Session config does not exist: $SessionConfigPath"
|
|
}
|
|
}
|
|
else {
|
|
if (-not [IO.Path]::IsPathFullyQualified($SessionsDirectory)) {
|
|
throw '-SessionsDirectory must be absolute.'
|
|
}
|
|
$SessionsDirectory = [IO.Path]::TrimEndingDirectorySeparator(
|
|
[IO.Path]::GetFullPath($SessionsDirectory))
|
|
if (-not (Test-Path -LiteralPath $SessionsDirectory -PathType Container)) {
|
|
throw "Sessions directory does not exist: $SessionsDirectory"
|
|
}
|
|
}
|
|
if (-not [IO.Path]::IsPathFullyQualified($ReportPath)) {
|
|
throw '-ReportPath must be absolute.'
|
|
}
|
|
$ReportPath = [IO.Path]::GetFullPath($ReportPath)
|
|
if (Test-Path -LiteralPath $ReportPath) {
|
|
throw '-ReportPath must be fresh.'
|
|
}
|
|
|
|
$deadline = [DateTime]::UtcNow.AddSeconds($WaitSeconds)
|
|
do {
|
|
if ($PSCmdlet.ParameterSetName -eq 'Path') {
|
|
$correlations = @(Get-CampaignLaSessionProcessCorrelations |
|
|
Where-Object {
|
|
$comparison = if ($IsWindows) {
|
|
[StringComparison]::OrdinalIgnoreCase
|
|
} else { [StringComparison]::Ordinal }
|
|
[string]::Equals(
|
|
$_.SessionConfigPath,
|
|
$SessionConfigPath,
|
|
$comparison)
|
|
})
|
|
}
|
|
else {
|
|
$comparison = if ($IsWindows) {
|
|
[StringComparison]::OrdinalIgnoreCase
|
|
} else { [StringComparison]::Ordinal }
|
|
$prefix = $SessionsDirectory + [IO.Path]::DirectorySeparatorChar
|
|
$correlations = @(Get-CampaignLaSessionProcessCorrelations |
|
|
Where-Object {
|
|
$_.SessionConfigPath.StartsWith($prefix, $comparison) -and
|
|
[IO.Path]::GetFileName($_.SessionConfigPath) -ceq 'session.json' -and
|
|
(Test-Path -LiteralPath $_.SessionConfigPath -PathType Leaf) -and
|
|
(Get-Item -LiteralPath $_.SessionConfigPath).LastWriteTimeUtc -ge
|
|
$CreatedAfterUtc.UtcDateTime
|
|
})
|
|
}
|
|
if ($correlations.Count -eq 1) { break }
|
|
if ($correlations.Count -gt 1) {
|
|
throw "More than one process uses the isolated session config."
|
|
}
|
|
Start-Sleep -Milliseconds 100
|
|
} while ([DateTime]::UtcNow -lt $deadline)
|
|
if ($correlations.Count -ne 1) {
|
|
throw 'No live process uses the isolated session config.'
|
|
}
|
|
$SessionConfigPath = [IO.Path]::GetFullPath($correlations[0].SessionConfigPath)
|
|
$processIdentity = [string]$correlations[0].ProcessInstanceIdentity
|
|
$commandFingerprint = [string]$correlations[0].CommandLineFingerprintSha256
|
|
if ($processIdentity -notmatch '^(windows-creation-v1:[0-9]{15,19}|linux-proc-start-v1:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[0-9]+)$' -or
|
|
$commandFingerprint -notmatch '^[0-9a-f]{64}$') {
|
|
throw 'The correlated process instance evidence is malformed.'
|
|
}
|
|
|
|
$directory = Split-Path -Parent $ReportPath
|
|
if (-not [string]::IsNullOrEmpty($directory)) {
|
|
$null = New-Item -ItemType Directory -Force -Path $directory
|
|
}
|
|
$report = [ordered]@{
|
|
schemaVersion = 2
|
|
kind = 'campaign-la-session-process-capture'
|
|
processId = [int]$correlations[0].ProcessId
|
|
processInstanceIdentity = $processIdentity
|
|
sessionId = [IO.Path]::GetFileName(
|
|
[IO.Path]::GetDirectoryName($SessionConfigPath))
|
|
sessionConfigPath = $SessionConfigPath
|
|
commandLineFingerprintSha256 = $commandFingerprint
|
|
capturedUtc = [DateTime]::UtcNow.ToString('O')
|
|
}
|
|
$report | ConvertTo-Json -Depth 3 |
|
|
Set-Content -LiteralPath $ReportPath -Encoding utf8NoBOM
|
|
Write-Host "Campaign LA process capture: $ReportPath"
|