fix(launcher): harden LA11 gate evidence
This commit is contained in:
parent
accd01a008
commit
9f9c116792
7 changed files with 514 additions and 89 deletions
|
|
@ -14,10 +14,8 @@ param(
|
|||
[Parameter(Mandatory = $true)][string]$StatusFile,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('probe', 'guiSelect', 'gui', 'headless')][string]$Mode,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateRange(1, 2147483647)][int]$ExpectedProcessId,
|
||||
[Parameter(Mandatory = $true)][string]$ProcessCapturePath,
|
||||
[Parameter(Mandatory = $true)][string]$CredentialProfilePath,
|
||||
[string]$SessionConfigPath,
|
||||
[string]$ExpectedSessionId,
|
||||
[string[]]$ExpectedPlugin = @(),
|
||||
[switch]$ExpectNoEnteredWorld,
|
||||
|
|
@ -42,6 +40,87 @@ if (-not [IO.Path]::IsPathFullyQualified($StatusFile)) {
|
|||
if (-not (Test-Path -LiteralPath $StatusFile -PathType Leaf)) {
|
||||
throw "Status file does not exist: $StatusFile"
|
||||
}
|
||||
if (-not [IO.Path]::IsPathFullyQualified($ProcessCapturePath)) {
|
||||
throw '-ProcessCapturePath must be absolute.'
|
||||
}
|
||||
$ProcessCapturePath = [IO.Path]::GetFullPath($ProcessCapturePath)
|
||||
if (-not (Test-Path -LiteralPath $ProcessCapturePath -PathType Leaf)) {
|
||||
throw "Process capture does not exist: $ProcessCapturePath"
|
||||
}
|
||||
$captureItem = Get-Item -LiteralPath $ProcessCapturePath -Force
|
||||
if (($captureItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
||||
throw 'Process capture must not be a reparse point.'
|
||||
}
|
||||
$captureDocument = [Text.Json.JsonDocument]::Parse(
|
||||
[IO.File]::ReadAllText($ProcessCapturePath))
|
||||
try {
|
||||
$captureRoot = $captureDocument.RootElement
|
||||
if ($captureRoot.ValueKind -ne [Text.Json.JsonValueKind]::Object) {
|
||||
throw 'Process capture root must be an object.'
|
||||
}
|
||||
$captureNames = @($captureRoot.EnumerateObject() | ForEach-Object { $_.Name })
|
||||
$expectedCaptureNames = @(
|
||||
'schemaVersion', 'kind', 'processId', 'processInstanceIdentity',
|
||||
'sessionId', 'sessionConfigPath', 'commandLineFingerprintSha256',
|
||||
'capturedUtc')
|
||||
if ([string]::Join("`n", $captureNames) -cne
|
||||
[string]::Join("`n", $expectedCaptureNames)) {
|
||||
throw 'Process capture fields/order do not match schema v2.'
|
||||
}
|
||||
if ($captureRoot.GetProperty('schemaVersion').GetInt32() -ne 2 -or
|
||||
$captureRoot.GetProperty('kind').GetString() -cne
|
||||
'campaign-la-session-process-capture') {
|
||||
throw 'Process capture schema/kind is invalid.'
|
||||
}
|
||||
$capturedProcessId = $captureRoot.GetProperty('processId').GetInt32()
|
||||
if ($capturedProcessId -le 0) { throw 'Process capture PID is invalid.' }
|
||||
$capturedProcessIdentity = $captureRoot.GetProperty(
|
||||
'processInstanceIdentity').GetString()
|
||||
$expectedIdentityPattern = if ($IsWindows) {
|
||||
'^windows-creation-v1:[0-9]{15,19}$'
|
||||
} else { '^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]+$' }
|
||||
if ($capturedProcessIdentity -notmatch $expectedIdentityPattern) {
|
||||
throw 'Process capture instance identity is invalid for this platform.'
|
||||
}
|
||||
$capturedSessionId = $captureRoot.GetProperty('sessionId').GetString()
|
||||
if ([string]::IsNullOrWhiteSpace($capturedSessionId) -or
|
||||
$capturedSessionId.IndexOfAny([IO.Path]::GetInvalidFileNameChars()) -ge 0) {
|
||||
throw 'Process capture session id is invalid.'
|
||||
}
|
||||
$capturedSessionConfigPath = $captureRoot.GetProperty(
|
||||
'sessionConfigPath').GetString()
|
||||
if (-not [IO.Path]::IsPathFullyQualified($capturedSessionConfigPath)) {
|
||||
throw 'Process capture session-config path is not absolute.'
|
||||
}
|
||||
$normalizedCapturedConfigPath = [IO.Path]::GetFullPath(
|
||||
$capturedSessionConfigPath)
|
||||
if ($capturedSessionConfigPath -cne $normalizedCapturedConfigPath -or
|
||||
[IO.Path]::GetFileName($capturedSessionConfigPath) -cne 'session.json' -or
|
||||
[IO.Path]::GetFileName([IO.Path]::GetDirectoryName(
|
||||
$capturedSessionConfigPath)) -cne $capturedSessionId) {
|
||||
throw 'Process capture session-config path is not the exact normalized session path.'
|
||||
}
|
||||
$capturedCommandFingerprint = $captureRoot.GetProperty(
|
||||
'commandLineFingerprintSha256').GetString()
|
||||
if ($capturedCommandFingerprint -cnotmatch '^[0-9a-f]{64}$') {
|
||||
throw 'Process capture command-line fingerprint is invalid.'
|
||||
}
|
||||
$capturedUtcText = $captureRoot.GetProperty('capturedUtc').GetString()
|
||||
$capturedUtc = [DateTimeOffset]::MinValue
|
||||
if (-not [DateTimeOffset]::TryParseExact(
|
||||
$capturedUtcText,
|
||||
'O',
|
||||
[Globalization.CultureInfo]::InvariantCulture,
|
||||
[Globalization.DateTimeStyles]::RoundtripKind,
|
||||
[ref]$capturedUtc) -or $capturedUtc.Offset -ne [TimeSpan]::Zero) {
|
||||
throw 'Process capture timestamp is not exact UTC round-trip form.'
|
||||
}
|
||||
}
|
||||
finally { $captureDocument.Dispose() }
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExpectedSessionId) -and
|
||||
$ExpectedSessionId -cne $capturedSessionId) {
|
||||
throw 'Process capture session id does not match -ExpectedSessionId.'
|
||||
}
|
||||
if (-not [IO.Path]::IsPathFullyQualified($CredentialProfilePath)) {
|
||||
throw '-CredentialProfilePath must be absolute.'
|
||||
}
|
||||
|
|
@ -82,12 +161,6 @@ elseif ($IsWindows) {
|
|||
}
|
||||
}
|
||||
else { throw 'Campaign LA status validation supports Windows and Linux only.' }
|
||||
if (-not [string]::IsNullOrWhiteSpace($SessionConfigPath)) {
|
||||
if (-not [IO.Path]::IsPathFullyQualified($SessionConfigPath)) {
|
||||
throw '-SessionConfigPath must be absolute.'
|
||||
}
|
||||
$SessionConfigPath = [IO.Path]::GetFullPath($SessionConfigPath)
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($ReportPath)) {
|
||||
$ReportPath = "$StatusFile.validation.json"
|
||||
}
|
||||
|
|
@ -402,27 +475,29 @@ for ($index = 0; $index -lt $eventNames.Count; $index++) {
|
|||
}
|
||||
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($ProcessExitWaitSeconds)
|
||||
$capturedState = $null
|
||||
do {
|
||||
$expectedProcess = Get-Process -Id $ExpectedProcessId -ErrorAction SilentlyContinue
|
||||
if ($null -eq $expectedProcess) { break }
|
||||
$currentProcessIdentity = Get-CampaignLaProcessInstanceIdentity `
|
||||
-ProcessId $capturedProcessId
|
||||
$correlations = @(Get-CampaignLaSessionProcessCorrelations)
|
||||
$capturedState = Test-CampaignLaCapturedProcessState `
|
||||
-ProcessId $capturedProcessId `
|
||||
-ProcessInstanceIdentity $capturedProcessIdentity `
|
||||
-SessionConfigPath $capturedSessionConfigPath `
|
||||
-CurrentProcessInstanceIdentity $currentProcessIdentity `
|
||||
-Correlations $correlations
|
||||
if (-not $capturedState.SameInstanceAlive -and
|
||||
-not $capturedState.ExactConfigPathAlive) {
|
||||
break
|
||||
}
|
||||
Start-Sleep -Milliseconds 100
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
if ($null -ne $expectedProcess) {
|
||||
$failures.Add("expected launcher child PID $ExpectedProcessId remains alive")
|
||||
if ($capturedState.SameInstanceAlive) {
|
||||
$failures.Add(
|
||||
"captured launcher child process instance PID $capturedProcessId remains alive")
|
||||
}
|
||||
|
||||
$pathCorrelationChecked = -not [string]::IsNullOrWhiteSpace($SessionConfigPath)
|
||||
if ($pathCorrelationChecked) {
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($ProcessExitWaitSeconds)
|
||||
do {
|
||||
$correlated = @(Get-CampaignLaCorrelatedProcessIds $SessionConfigPath)
|
||||
if ($correlated.Count -eq 0) { break }
|
||||
Start-Sleep -Milliseconds 100
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
if ($correlated.Count -gt 0) {
|
||||
$failures.Add(
|
||||
"session-config-correlated launcher child PID(s) remain: $($correlated -join ',')")
|
||||
}
|
||||
if ($capturedState.ExactConfigPathAlive) {
|
||||
$failures.Add('a launcher child remains correlated to the exact session-config path')
|
||||
}
|
||||
|
||||
$reportDirectory = Split-Path -Parent $ReportPath
|
||||
|
|
@ -442,9 +517,12 @@ $report = [ordered]@{
|
|||
eventNames = @($eventNames)
|
||||
loadedPluginCount = $loadedPlugins.Count
|
||||
terminalObserved = $terminalSeen
|
||||
expectedProcessId = $ExpectedProcessId
|
||||
processExited = ($null -eq $expectedProcess)
|
||||
sessionConfigCorrelationChecked = $pathCorrelationChecked
|
||||
capturedProcessId = $capturedProcessId
|
||||
capturedProcessInstanceExited = (-not $capturedState.SameInstanceAlive)
|
||||
capturedPidReused = [bool]$capturedState.PidReused
|
||||
sessionConfigCorrelationChecked = $true
|
||||
sessionConfigProcessExited = (-not $capturedState.ExactConfigPathAlive)
|
||||
processCaptureSha256 = (Get-FileHash -LiteralPath $ProcessCapturePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
credentialPermissionsValidated = $true
|
||||
forbiddenCredentialValueCount = $forbiddenValues.Count
|
||||
failures = @($failures)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue