Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { statSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const RUNTIME_HOST_DIAGNOSTIC_MAX_BYTES = 32 * 1024;
|
|
const WINDOWS_SECURITY_TEXT_MAX_CHARS = 4 * 1024;
|
|
const DIAGNOSTIC_TEXT_MAX_CHARS = 2 * 1024;
|
|
const WINDOWS_PATH_SECURITY_TIMEOUT_MS = 30_000;
|
|
const WINDOWS_DIAGNOSTIC_PATHS_ENV = 'MAKA_RUNTIME_HOST_DIAGNOSTIC_PATHS';
|
|
const WINDOWS_PATH_SECURITY_SCRIPT = `
|
|
$ErrorActionPreference = 'Stop'
|
|
$paths = ConvertFrom-Json -InputObject $env:${WINDOWS_DIAGNOSTIC_PATHS_ENV}
|
|
$results = @()
|
|
foreach ($path in @($paths)) {
|
|
try {
|
|
$acl = Get-Acl -LiteralPath $path
|
|
$results += [ordered]@{
|
|
state = 'present'
|
|
owner = $acl.Owner
|
|
sddl = $acl.Sddl
|
|
}
|
|
} catch {
|
|
$exception = $_.Exception
|
|
$nativeErrorCode = $null
|
|
if ($null -ne $exception.PSObject.Properties['NativeErrorCode']) {
|
|
$nativeErrorCode = $exception.NativeErrorCode
|
|
}
|
|
$results += [ordered]@{
|
|
state = 'unavailable'
|
|
error = [ordered]@{
|
|
name = $exception.GetType().FullName
|
|
hresult = $exception.HResult
|
|
nativeErrorCode = $nativeErrorCode
|
|
message = $exception.Message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$response = [ordered]@{ results = @($results) }
|
|
[Console]::Out.Write((ConvertTo-Json -InputObject $response -Compress -Depth 5))
|
|
`;
|
|
|
|
const STORAGE_AUTHORITY_MODULE = 'node_modules/@maka/storage/dist/root-authority.js';
|
|
const REGISTRATION_MODULE = 'node_modules/@maka/runtime-host/dist/control/registration.js';
|
|
const STARTUP_DIAGNOSTIC_MODULE =
|
|
'node_modules/@maka/runtime-host/dist/control/startup-diagnostic.js';
|
|
|
|
export async function collectRuntimeHostFailureDiagnostic(packageRoot, rootPath, options = {}) {
|
|
const platform = options.platform ?? process.platform;
|
|
const environment = options.environment ?? process.env;
|
|
const loadInstalled = options.loadInstalled ?? importInstalled;
|
|
const readPathSecurityBatch =
|
|
options.readPathSecurityBatch ??
|
|
(platform === 'win32'
|
|
? (paths) => readWindowsPathSecurityBatch(paths, environment)
|
|
: undefined);
|
|
const diagnostic = {
|
|
schemaVersion: 1,
|
|
platform,
|
|
architecture: options.architecture ?? process.arch,
|
|
runner: compactObject({
|
|
imageOS: boundedOptionalString(environment.ImageOS),
|
|
imageVersion: boundedOptionalString(environment.ImageVersion),
|
|
name: boundedOptionalString(environment.RUNNER_NAME),
|
|
environment: boundedOptionalString(environment.RUNNER_ENVIRONMENT),
|
|
}),
|
|
paths: [],
|
|
};
|
|
|
|
addPathEvidence(diagnostic.paths, 'root', rootPath);
|
|
|
|
const finish = async () => {
|
|
if (readPathSecurityBatch) {
|
|
await addPathSecurityEvidence(diagnostic.paths, readPathSecurityBatch);
|
|
}
|
|
return diagnostic;
|
|
};
|
|
|
|
let authority;
|
|
try {
|
|
authority = await loadInstalled(packageRoot, STORAGE_AUTHORITY_MODULE);
|
|
} catch (error) {
|
|
diagnostic.storageAuthority = { state: 'unavailable', error: summarizeDiagnosticError(error) };
|
|
return finish();
|
|
}
|
|
|
|
addPathEvidence(
|
|
diagnostic.paths,
|
|
'root_marker',
|
|
join(rootPath, authority.STORAGE_ROOT_MARKER_FILE),
|
|
);
|
|
|
|
let capability;
|
|
try {
|
|
capability = await authority.discoverMarkedStorageRoot({ path: rootPath });
|
|
diagnostic.storageAuthority = { state: 'valid', rootId: capability.rootId };
|
|
} catch (error) {
|
|
diagnostic.storageAuthority = { state: 'invalid', error: summarizeDiagnosticError(error) };
|
|
}
|
|
|
|
let controlRoot;
|
|
try {
|
|
controlRoot = authority.resolveRootControlNamespace();
|
|
addPathEvidence(diagnostic.paths, 'control_root', controlRoot);
|
|
} catch (error) {
|
|
diagnostic.controlNamespace = {
|
|
state: 'unavailable',
|
|
error: summarizeDiagnosticError(error),
|
|
};
|
|
}
|
|
if (!capability || !controlRoot) return finish();
|
|
|
|
const controlDirectory = join(controlRoot, capability.rootId);
|
|
addPathEvidence(diagnostic.paths, 'control_directory', controlDirectory);
|
|
|
|
try {
|
|
const registrationAuthority = await loadInstalled(packageRoot, REGISTRATION_MODULE);
|
|
const registrationPath = join(
|
|
controlDirectory,
|
|
registrationAuthority.RUNTIME_HOST_REGISTRATION_FILE,
|
|
);
|
|
addPathEvidence(diagnostic.paths, 'registration', registrationPath);
|
|
const registration = await registrationAuthority.readHostRegistration(controlDirectory);
|
|
diagnostic.registration = registration
|
|
? summarizeRegistration(registration, capability.rootId)
|
|
: { state: 'absent' };
|
|
} catch (error) {
|
|
diagnostic.registration = { state: 'unavailable', error: summarizeDiagnosticError(error) };
|
|
}
|
|
|
|
try {
|
|
const startupAuthority = await loadInstalled(packageRoot, STARTUP_DIAGNOSTIC_MODULE);
|
|
const startupPath = join(
|
|
controlDirectory,
|
|
startupAuthority.RUNTIME_HOST_STARTUP_DIAGNOSTIC_FILE,
|
|
);
|
|
addPathEvidence(diagnostic.paths, 'startup_diagnostic', startupPath);
|
|
const startup = await startupAuthority.readCandidateStartupDiagnostic(capability.rootId);
|
|
diagnostic.startup = startup ? { state: 'present', diagnostic: startup } : { state: 'absent' };
|
|
} catch (error) {
|
|
diagnostic.startup = { state: 'unavailable', error: summarizeDiagnosticError(error) };
|
|
}
|
|
|
|
return finish();
|
|
}
|
|
|
|
export function renderRuntimeHostFailureDiagnostic(diagnostic) {
|
|
return truncateUtf8Text(
|
|
JSON.stringify(diagnostic),
|
|
RUNTIME_HOST_DIAGNOSTIC_MAX_BYTES,
|
|
'<diagnostic truncated>',
|
|
);
|
|
}
|
|
|
|
export async function retireCollectedRuntimeHostStartupDiagnostic(
|
|
packageRoot,
|
|
diagnostic,
|
|
options = {},
|
|
) {
|
|
const startupAttemptId = diagnostic.startup?.diagnostic?.startupAttemptId;
|
|
const rootId = diagnostic.storageAuthority?.rootId;
|
|
if (typeof rootId !== 'string' || typeof startupAttemptId !== 'string') return false;
|
|
const loadInstalled = options.loadInstalled ?? importInstalled;
|
|
const startupAuthority = await loadInstalled(packageRoot, STARTUP_DIAGNOSTIC_MODULE);
|
|
return startupAuthority.clearSelectedCandidateStartupDiagnostic(rootId, startupAttemptId);
|
|
}
|
|
|
|
function addPathEvidence(paths, role, path) {
|
|
paths.push(inspectDiagnosticPath(role, path));
|
|
}
|
|
|
|
async function addPathSecurityEvidence(paths, readPathSecurityBatch) {
|
|
const evidence = paths.filter(
|
|
(entry) =>
|
|
entry.state === 'present' &&
|
|
(entry.role === 'root' ||
|
|
entry.role === 'control_root' ||
|
|
entry.role === 'control_directory'),
|
|
);
|
|
if (evidence.length === 0) return;
|
|
try {
|
|
const results = await readPathSecurityBatch(evidence.map(({ path }) => path));
|
|
if (!Array.isArray(results) || results.length !== evidence.length) {
|
|
throw new Error('Windows path security probe returned an invalid result count');
|
|
}
|
|
for (let index = 0; index < evidence.length; index += 1) {
|
|
evidence[index].security = results[index];
|
|
}
|
|
} catch (error) {
|
|
const unavailable = { state: 'unavailable', error: summarizeDiagnosticError(error) };
|
|
for (const entry of evidence) entry.security = unavailable;
|
|
}
|
|
}
|
|
|
|
function inspectDiagnosticPath(role, path) {
|
|
try {
|
|
const pathStat = statSync(path);
|
|
return {
|
|
role,
|
|
path,
|
|
state: 'present',
|
|
kind: pathStat.isDirectory() ? 'directory' : pathStat.isFile() ? 'file' : 'other',
|
|
size: pathStat.size,
|
|
mode: pathStat.mode,
|
|
};
|
|
} catch (error) {
|
|
return { role, path, state: 'unavailable', error: summarizeDiagnosticError(error) };
|
|
}
|
|
}
|
|
|
|
function readWindowsPathSecurityBatch(paths, environment) {
|
|
const systemRoot = environment.SystemRoot;
|
|
if (!systemRoot) throw new Error('SystemRoot is unavailable');
|
|
const output = execFileSync(
|
|
join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
|
|
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', WINDOWS_PATH_SECURITY_SCRIPT],
|
|
{
|
|
encoding: 'utf8',
|
|
env: { ...environment, [WINDOWS_DIAGNOSTIC_PATHS_ENV]: JSON.stringify(paths) },
|
|
timeout: WINDOWS_PATH_SECURITY_TIMEOUT_MS,
|
|
windowsHide: true,
|
|
},
|
|
);
|
|
const parsed = JSON.parse(output);
|
|
if (
|
|
typeof parsed !== 'object' ||
|
|
parsed === null ||
|
|
!Array.isArray(parsed.results) ||
|
|
parsed.results.length !== paths.length
|
|
) {
|
|
throw new Error('Windows path security probe returned an invalid response');
|
|
}
|
|
return parsed.results.map(decodeWindowsPathSecurity);
|
|
}
|
|
|
|
function decodeWindowsPathSecurity(value) {
|
|
if (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
value.state === 'present' &&
|
|
typeof value.owner === 'string' &&
|
|
typeof value.sddl === 'string'
|
|
) {
|
|
return {
|
|
state: 'present',
|
|
owner: value.owner.slice(0, WINDOWS_SECURITY_TEXT_MAX_CHARS),
|
|
sddl: value.sddl.slice(0, WINDOWS_SECURITY_TEXT_MAX_CHARS),
|
|
};
|
|
}
|
|
if (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
value.state === 'unavailable' &&
|
|
typeof value.error === 'object' &&
|
|
value.error !== null &&
|
|
typeof value.error.message === 'string'
|
|
) {
|
|
return {
|
|
state: 'unavailable',
|
|
error: compactObject({
|
|
name:
|
|
typeof value.error.name === 'string'
|
|
? boundedDiagnosticString(value.error.name)
|
|
: undefined,
|
|
hresult: Number.isSafeInteger(value.error.hresult) ? value.error.hresult : undefined,
|
|
nativeErrorCode: Number.isSafeInteger(value.error.nativeErrorCode)
|
|
? value.error.nativeErrorCode
|
|
: undefined,
|
|
message: boundedDiagnosticString(value.error.message),
|
|
}),
|
|
};
|
|
}
|
|
throw new Error('Windows path security probe returned an invalid path result');
|
|
}
|
|
|
|
function summarizeRegistration(registration, expectedRootId) {
|
|
return compactObject({
|
|
state: 'present',
|
|
schemaVersion: registration.schemaVersion,
|
|
rootIdMatches: registration.rootId === expectedRootId,
|
|
hostEpoch: registration.hostEpoch,
|
|
lifecycleState: registration.state,
|
|
lifecycleMode: registration.lifecycleMode,
|
|
pid: registration.pid,
|
|
endpointKind:
|
|
typeof registration.endpoint === 'string' && registration.endpoint.startsWith('\\\\.\\pipe\\')
|
|
? 'windows_named_pipe'
|
|
: 'other',
|
|
});
|
|
}
|
|
|
|
function summarizeDiagnosticError(error, depth = 0) {
|
|
if (!(error instanceof Error)) return { message: boundedDiagnosticString(String(error)) };
|
|
return compactObject({
|
|
name: boundedDiagnosticString(error.name),
|
|
code: typeof error.code === 'string' || typeof error.code === 'number' ? error.code : undefined,
|
|
message: boundedDiagnosticString(error.message),
|
|
cause:
|
|
depth < 3 && error.cause !== undefined
|
|
? summarizeDiagnosticError(error.cause, depth + 1)
|
|
: undefined,
|
|
});
|
|
}
|
|
|
|
function compactObject(value) {
|
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
}
|
|
|
|
function boundedOptionalString(value) {
|
|
return typeof value === 'string' ? boundedDiagnosticString(value) : undefined;
|
|
}
|
|
|
|
function boundedDiagnosticString(value) {
|
|
return value.slice(0, DIAGNOSTIC_TEXT_MAX_CHARS);
|
|
}
|
|
|
|
function truncateUtf8Text(value, maximumBytes, marker) {
|
|
const encoded = Buffer.from(value, 'utf8');
|
|
if (encoded.byteLength <= maximumBytes) return value;
|
|
const markerBytes = Buffer.byteLength(marker, 'utf8');
|
|
const prefix = encoded
|
|
.subarray(0, Math.max(0, maximumBytes - markerBytes))
|
|
.toString('utf8')
|
|
.replace(/\uFFFD$/u, '');
|
|
return `${prefix}${marker}`;
|
|
}
|
|
|
|
function importInstalled(packageRoot, relativePath) {
|
|
return import(pathToFileURL(join(packageRoot, relativePath)).href);
|
|
}
|