#!/usr/bin/env node import { readFile, realpath } from "node:fs/promises"; import { parseArgs } from "node:util"; import { analyzeJavaScriptApplication } from "../dist/application/JavaScriptApplicationService.js"; import { PermissionAuthority } from "../dist/application/PermissionAuthority.js"; import { reconcileJavaScriptRuntimeEvidence } from "../dist/application/JavaScriptRuntimeReconciliationService.js"; import { createPermissionPolicy } from "../dist/domain/permissionPolicy.js"; const options = parseArgs({ options: { application: { type: "string" }, cache: { type: "string", multiple: true, default: [] }, assets: { type: "string", multiple: true, default: [] }, "runtime-evidence": { type: "string", multiple: true, default: [] }, mapping: { type: "string", multiple: true, default: [] }, "source-map-read-approved": { type: "boolean", default: false }, }, strict: true, allowPositionals: false, }).values; if (options.application === undefined) throw new Error("--application must name an operator-provided app/ASAR path"); if (options["runtime-evidence"].length === 0) throw new Error( "At least one --runtime-evidence path from inspect_web_page or inspect_electron_page is required", ); const requestedLayers = [ { role: "application", path: options.application }, ...options.cache.map((path) => ({ role: "cache", path })), ...options.assets.map((path) => ({ role: "assets", path })), ]; const canonicalLayers = await Promise.all( requestedLayers.map(async (layer) => ({ ...layer, path: await realpath(layer.path), })), ); const authority = permissionAuthority(canonicalLayers.map(({ path }) => path)); const staticLayers = []; for (const layer of canonicalLayers) { const analyzed = await analyzeJavaScriptApplication(authority, { input_path: layer.path, format: "auto", approved: true, source_map_read_approved: options["source-map-read-approved"], }); if (!analyzed.ok) throw analyzed.error; staticLayers.push({ role: layer.role, analysis: analyzed.value, runtime_mappings: [], }); } for (const encoded of options.mapping) { const mapping = parseMapping(encoded, staticLayers.length); staticLayers[mapping.layer].runtime_mappings.push(mapping.value); } const runtimeObservations = ( await Promise.all(options["runtime-evidence"].map(readEvidenceFile)) ).flat(); const reconciled = reconcileJavaScriptRuntimeEvidence({ static_layers: staticLayers, runtime_observations: runtimeObservations, }); if (!reconciled.ok) throw reconciled.error; const result = reconciled.value.normalized_result; process.stdout.write( `${JSON.stringify({ reconciliation_id: result.reconciliation_id, evidence_id: reconciled.value.evidence_id, graph_id: result.graph.graph_id, layers: result.static_layers.map((layer) => ({ role: layer.role, evidence_id: layer.evidence_id, graph_id: layer.graph_id, root_artifact_sha256: layer.root_artifact_sha256, input_path: layer.input_path, })), runtime_captures: result.runtime_captures.map((capture) => ({ evidence_id: capture.evidence_id, capture_sha256: capture.capture_sha256, target_location: capture.target_location, })), summary: result.summary, coverage: result.coverage, unmatched_reasons: counted( result.reconciliations .filter(({ status }) => status !== "matched") .map(({ reason }) => reason), ), static_load_states: counted( result.static_load_states.map(({ status }) => status), ), verified: true, })}\n`, ); function permissionAuthority(roots) { const scope = { capability: "investigation_input", roots, executables: [], environment_names: [], network: "none", mount: false, }; return new PermissionAuthority( createPermissionPolicy( [scope], [ { ...scope, grant_id: "local-javascript-runtime-verifier", lifetime: "session", operation_identity: null, expires_at: null, }, ], ), ); } async function readEvidenceFile(path) { const bytes = await readFile(await realpath(path)); if (bytes.byteLength > 64 * 1024 * 1024) throw new Error(`Runtime Evidence exceeds 64 MiB: ${path}`); const value = JSON.parse(bytes.toString("utf8")); return Array.isArray(value) ? value : [value]; } function parseMapping(encoded, layerCount) { const parsed = JSON.parse(encoded); if ( typeof parsed !== "object" || parsed === null || !Number.isInteger(parsed.layer) || parsed.layer < 0 || parsed.layer >= layerCount || (parsed.kind !== "file-root" && parsed.kind !== "url-prefix") ) throw new Error( "--mapping must be JSON with a valid layer index and file-root or url-prefix kind", ); const { layer, ...value } = parsed; return { layer, value }; } function counted(values) { return Object.fromEntries( [...new Set(values)] .sort() .map((value) => [ value, values.filter((candidate) => candidate === value).length, ]), ); }