fix(launcher): harden LA11 gate evidence

This commit is contained in:
Erik 2026-08-15 01:45:15 +02:00
parent accd01a008
commit 9f9c116792
7 changed files with 514 additions and 89 deletions

View file

@ -28,6 +28,7 @@ if ([string]::IsNullOrWhiteSpace($pwsh)) {
}
$validator = Join-Path $Repository 'tools/test-campaign-la-session-status.ps1'
$capture = Join-Path $Repository 'tools/capture-campaign-la-session-process.ps1'
. (Join-Path $Repository 'tools/CampaignLaProcessCorrelation.ps1')
function Write-Profile([string]$Path, [string]$Secret) {
$document = [ordered]@{
@ -86,16 +87,14 @@ function Invoke-Validator(
[string]$Status,
[string]$Profile,
[string]$Report,
[int]$ExpectedProcessId,
[bool]$ShouldPass,
[string]$SessionConfig = '') {
[string]$ProcessCapture,
[bool]$ShouldPass) {
$arguments = [Collections.Generic.List[string]]::new()
foreach ($value in @(
'-NoProfile', '-File', $validator,
'-StatusFile', $Status,
'-Mode', 'gui',
'-ExpectedProcessId', $ExpectedProcessId.ToString(
[Globalization.CultureInfo]::InvariantCulture),
'-ProcessCapturePath', $ProcessCapture,
'-CredentialProfilePath', $Profile,
'-ExpectedPlugin', 'smoke',
'-AllowPluginFailure',
@ -103,10 +102,6 @@ function Invoke-Validator(
'-ReportPath', $Report)) {
$arguments.Add($value)
}
if (-not [string]::IsNullOrWhiteSpace($SessionConfig)) {
$arguments.Add('-SessionConfigPath')
$arguments.Add($SessionConfig)
}
$start = [Diagnostics.ProcessStartInfo]::new($pwsh)
$start.UseShellExecute = $false
$start.CreateNoWindow = $true
@ -127,6 +122,30 @@ function Invoke-Validator(
}
}
function Write-ProcessCapture(
[string]$Path,
[int]$ProcessId,
[string]$ProcessInstanceIdentity,
[string]$SessionConfigPath,
[string]$CommandFingerprint = ('a' * 64)) {
$sessionId = [IO.Path]::GetFileName(
[IO.Path]::GetDirectoryName($SessionConfigPath))
$document = [ordered]@{
schemaVersion = 2
kind = 'campaign-la-session-process-capture'
processId = $ProcessId
processInstanceIdentity = $ProcessInstanceIdentity
sessionId = $sessionId
sessionConfigPath = [IO.Path]::GetFullPath($SessionConfigPath)
commandLineFingerprintSha256 = $CommandFingerprint
capturedUtc = [DateTime]::UtcNow.ToString('O')
}
[IO.File]::WriteAllText(
$Path,
($document | ConvertTo-Json -Depth 4),
[Text.UTF8Encoding]::new($false))
}
$quickInfo = [Diagnostics.ProcessStartInfo]::new($pwsh)
$quickInfo.UseShellExecute = $false
$quickInfo.ArgumentList.Add('-NoProfile')
@ -138,13 +157,33 @@ $goneProcessId = $quick.Id
$quick.WaitForExit()
$quick.Dispose()
$sessionRoot = Join-Path $OutputDirectory 'fixture-session'
$null = New-Item -ItemType Directory -Path $sessionRoot
$sessionConfig = Join-Path $sessionRoot 'session.json'
[IO.File]::WriteAllText($sessionConfig, '{}', [Text.UTF8Encoding]::new($false))
$syntheticIdentity = if ($IsWindows) {
'windows-creation-v1:638000000000000000'
} else { 'linux-proc-start-v1:00000000-0000-0000-0000-000000000001:1' }
$goneCapture = Join-Path $OutputDirectory 'gone-process.capture.json'
Write-ProcessCapture `
$goneCapture $goneProcessId $syntheticIdentity $sessionConfig
$profile = Join-Path $OutputDirectory 'launcher-profiles.json'
Write-Profile $profile 'la11-positive-secret-7E477A2D'
$positiveStatus = Join-Path $OutputDirectory 'positive.jsonl'
Write-Events $positiveStatus (New-GuiEvents)
Invoke-Validator `
$positiveStatus $profile (Join-Path $OutputDirectory 'positive.validation.json') `
$goneProcessId $true
$goneCapture $true
$malformedCapture = Join-Path $OutputDirectory 'malformed-process.capture.json'
[IO.File]::WriteAllText(
$malformedCapture,
'{"schemaVersion":2}',
[Text.UTF8Encoding]::new($false))
Invoke-Validator `
$positiveStatus $profile (Join-Path $OutputDirectory 'malformed.validation.json') `
$malformedCapture $false
foreach ($reason in @('transport', 'reconnect', 'other')) {
$events = @(New-GuiEvents)
@ -152,7 +191,7 @@ foreach ($reason in @('transport', 'reconnect', 'other')) {
$path = Join-Path $OutputDirectory "reason-$reason.jsonl"
$report = Join-Path $OutputDirectory "reason-$reason.validation.json"
Write-Events $path $events
Invoke-Validator $path $profile $report $goneProcessId $false
Invoke-Validator $path $profile $report $goneCapture $false
$result = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json
if (-not ($result.failures -match 'disconnected reason')) {
throw "Disconnected reason '$reason' was not rejected by its exact assertion."
@ -186,7 +225,7 @@ foreach ($case in $secretCases) {
$path = Join-Path $OutputDirectory "secret-$case.jsonl"
$report = Join-Path $OutputDirectory "secret-$case.validation.json"
Write-Events $path $events
Invoke-Validator $path $caseProfile $report $goneProcessId $false
Invoke-Validator $path $caseProfile $report $goneCapture $false
$result = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json
if (-not ($result.failures -match 'credential value')) {
throw "Credential echo case '$case' was not rejected by recursive scanning."
@ -212,12 +251,14 @@ if ($IsLinux) {
[IO.File]::GetUnixFileMode($sourceHost))
}
$sessionConfig = Join-Path $OutputDirectory 'session.json'
[IO.File]::WriteAllText($sessionConfig, '{}', [Text.UTF8Encoding]::new($false))
$targetReady = Join-Path $OutputDirectory 'target.ready'
$targetRelease = Join-Path $OutputDirectory 'target.release'
$unrelatedReady = Join-Path $OutputDirectory 'unrelated.ready'
$unrelatedRelease = Join-Path $OutputDirectory 'unrelated.release'
$unrelatedSessionRoot = Join-Path $OutputDirectory 'unrelated-session'
$null = New-Item -ItemType Directory -Path $unrelatedSessionRoot
$unrelatedConfig = Join-Path $unrelatedSessionRoot 'session.json'
[IO.File]::WriteAllText($unrelatedConfig, '{}', [Text.UTF8Encoding]::new($false))
function Start-Fixture([string[]]$Arguments) {
$start = [Diagnostics.ProcessStartInfo]::new($sameNameHost)
@ -230,7 +271,7 @@ function Start-Fixture([string[]]$Arguments) {
$target = Start-Fixture @(
'hold-campaign-la-process', '--config', $sessionConfig, $targetReady, $targetRelease)
$unrelated = Start-Fixture @(
'hold-update-lease', 'session', (Join-Path $OutputDirectory 'unrelated-data'),
'hold-campaign-la-process', '--config', $unrelatedConfig,
$unrelatedReady, $unrelatedRelease)
if ($null -eq $target -or $null -eq $unrelated) {
throw 'Could not start process-correlation fixtures.'
@ -252,16 +293,19 @@ try {
-SessionConfigPath $sessionConfig -ReportPath $captureReport
if ($LASTEXITCODE -ne 0) { throw 'Process capture failed.' }
$captured = Get-Content -LiteralPath $captureReport -Raw | ConvertFrom-Json
if ([int]$captured.processId -ne $target.Id) {
throw 'Process capture did not return the exact correlated PID.'
if ([int]$captured.processId -ne $target.Id -or
[string]$captured.sessionConfigPath -cne $sessionConfig -or
[string]$captured.commandLineFingerprintSha256 -cnotmatch '^[0-9a-f]{64}$') {
throw 'Process capture did not return exact sanitized instance evidence.'
}
$liveReport = Join-Path $OutputDirectory 'live-pid.validation.json'
Invoke-Validator `
$positiveStatus $profile $liveReport $target.Id $false $sessionConfig
$positiveStatus $profile $liveReport $captureReport $false
$liveResult = Get-Content -LiteralPath $liveReport -Raw | ConvertFrom-Json
if (-not ($liveResult.failures -match 'remains alive')) {
throw 'A live exact child PID was not rejected by the terminal validator.'
if (-not ($liveResult.failures -match 'process instance.*remains alive') -or
$liveResult.capturedProcessInstanceExited) {
throw 'A live exact child instance was not rejected by the terminal validator.'
}
Set-Content -LiteralPath $targetRelease -Value 'release' -NoNewline
@ -269,7 +313,31 @@ try {
Invoke-Validator `
$positiveStatus $profile `
(Join-Path $OutputDirectory 'unrelated-same-name.validation.json') `
$target.Id $true $sessionConfig
$captureReport $true
$reusedCapture = Join-Path $OutputDirectory 'reused-pid.capture.json'
$capturedIdentity = [string]$captured.processInstanceIdentity
$identitySeparator = $capturedIdentity.LastIndexOf(':')
$capturedStartValue = [uint64]::Parse(
$capturedIdentity.Substring($identitySeparator + 1),
[Globalization.CultureInfo]::InvariantCulture)
$reusedPriorIdentity = $capturedIdentity.Substring(0, $identitySeparator + 1) `
+ ($capturedStartValue + 1).ToString(
[Globalization.CultureInfo]::InvariantCulture)
Write-ProcessCapture `
$reusedCapture `
$unrelated.Id `
$reusedPriorIdentity `
$sessionConfig `
([string]$captured.commandLineFingerprintSha256)
$reusedReport = Join-Path $OutputDirectory 'reused-pid.validation.json'
Invoke-Validator $positiveStatus $profile $reusedReport $reusedCapture $true
$reusedResult = Get-Content -LiteralPath $reusedReport -Raw | ConvertFrom-Json
if (-not $reusedResult.capturedPidReused -or
-not $reusedResult.capturedProcessInstanceExited -or
-not $reusedResult.sessionConfigProcessExited) {
throw 'A reused PID was not distinguished from the exited captured instance.'
}
}
finally {
Set-Content -LiteralPath $targetRelease -Value 'release' -NoNewline
@ -287,7 +355,10 @@ $summary = [ordered]@{
disconnectedReasonNegatives = 3
credentialStringFieldNegatives = $secretCases.Count
exactPidCapture = $true
livePidRejected = $true
stableProcessInstanceCapture = $true
liveProcessInstanceRejected = $true
malformedProcessCaptureRejected = $true
injectedPidReuseIgnored = $true
unrelatedSameNameIgnored = $true
platform = if ($IsWindows) { 'windows' } else { 'linux' }
}