2026-10-21 21:18:22 -07:00
|
|
|
import {
|
|
|
|
|
DEFAULT_ASAR_HEADER_ADMISSION,
|
|
|
|
|
type AsarHeaderAdmission,
|
|
|
|
|
} from "../../config/asarHeaderAdmission.js";
|
2026-10-24 03:18:32 +08:00
|
|
|
import { constants, type Stats } from "node:fs";
|
2026-10-22 16:54:14 +08:00
|
|
|
import {
|
|
|
|
|
buildJavaScriptRouteInventorySteps,
|
|
|
|
|
type JavaScriptRouteInventory,
|
|
|
|
|
} from "../../domain/javascript/javascriptRouteInventory.js";
|
2026-10-09 09:13:17 +00:00
|
|
|
import { access, lstat, realpath } from "node:fs/promises";
|
2026-10-12 02:39:52 +08:00
|
|
|
import { setImmediate } from "node:timers/promises";
|
2026-07-16 05:29:40 +08:00
|
|
|
|
2026-10-07 17:57:33 +08:00
|
|
|
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
2026-10-09 09:13:17 +00:00
|
|
|
import {
|
|
|
|
|
AnalysisAccessDeniedError,
|
|
|
|
|
AnalysisInputError,
|
|
|
|
|
AnalysisUnsupportedTargetError,
|
|
|
|
|
} from "../../domain/analysisErrorCore.js";
|
2026-10-07 17:57:33 +08:00
|
|
|
import { createJavaScriptArtifactReader as createReader } from "../../artifacts/javascript/JavaScriptArtifactReader.js";
|
2026-10-07 17:16:58 +08:00
|
|
|
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
|
|
|
|
|
import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
|
2026-10-12 04:07:14 +08:00
|
|
|
import type { JavaScriptSourceSelection } from "../../domain/javascript/javascriptSourceSelection.js";
|
2026-10-24 15:02:26 +08:00
|
|
|
import {
|
|
|
|
|
parseOwnedJavaScriptApplicationAnalysisSteps,
|
|
|
|
|
type AnalyzeJavaScriptApplicationInput,
|
|
|
|
|
type ElectronBoundarySummary,
|
|
|
|
|
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
2026-10-11 07:22:50 +08:00
|
|
|
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
2026-10-24 04:00:10 +08:00
|
|
|
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
2026-10-12 02:39:52 +08:00
|
|
|
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
2026-10-10 02:00:08 +08:00
|
|
|
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
2026-10-11 04:41:49 +08:00
|
|
|
import type { ArtifactInventoryPartialObservation } from "../../domain/artifactPartialObservation.js";
|
2026-10-11 07:22:50 +08:00
|
|
|
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
|
|
|
|
import { analyzeAndAdoptJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
|
2026-10-17 12:33:41 +05:00
|
|
|
import {
|
|
|
|
|
isSkippedJavaScriptAnalysis,
|
|
|
|
|
javascriptArtifactAnalysisScopes,
|
|
|
|
|
} from "./JavaScriptArtifactAnalysisTypes.js";
|
2026-10-11 07:22:50 +08:00
|
|
|
import { createJavaScriptSourceAnalysis } from "../../artifacts/javascript/JavaScriptSourceAnalysis.js";
|
|
|
|
|
import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "../../domain/javascript/javascriptAnalysisResourceControls.js";
|
|
|
|
|
import { createOwnedJavaScriptApplicationEvidenceCooperatively } from "./JavaScriptApplicationEvidence.js";
|
2026-10-07 17:57:33 +08:00
|
|
|
import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js";
|
2026-10-09 02:40:21 +08:00
|
|
|
import { buildImmutableJavaScriptArtifactGraphSteps } from "./JavaScriptArtifactGraphBuilder.js";
|
2026-10-11 04:41:49 +08:00
|
|
|
import { scanCanonicalArtifactInventoryInScope } from "../../artifacts/inventory/scanCanonical.js";
|
2026-10-24 02:48:30 +08:00
|
|
|
import { createJavaScriptNextAppContext } from "./JavaScriptNextAppContext.js";
|
2026-07-16 06:45:34 +08:00
|
|
|
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
|
2026-10-08 16:15:35 +08:00
|
|
|
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
|
2026-10-08 03:19:37 +08:00
|
|
|
import type { ProgressReporter } from "../ProgressReporter.js";
|
2026-10-09 02:40:21 +08:00
|
|
|
import {
|
|
|
|
|
checkpointJavaScriptAnalysis,
|
|
|
|
|
completeJavaScriptAnalysisSteps,
|
|
|
|
|
} from "./JavaScriptAnalysisControl.js";
|
2026-07-16 05:29:40 +08:00
|
|
|
|
|
|
|
|
/** Application-layer result retaining local diagnostics outside the canonical graph. */
|
|
|
|
|
export interface JavaScriptArtifactReconstructionResult {
|
2026-10-22 16:54:14 +08:00
|
|
|
readonly route_inventory?: JavaScriptRouteInventory;
|
2026-07-16 05:29:40 +08:00
|
|
|
readonly input_path: string;
|
|
|
|
|
readonly format: "asar" | "directory";
|
2026-07-16 06:45:34 +08:00
|
|
|
readonly root_artifact_sha256: string;
|
2026-07-16 05:29:40 +08:00
|
|
|
readonly inventory_manifest_id: string;
|
|
|
|
|
readonly inventory_graph_sha256: string;
|
2026-10-10 02:00:08 +08:00
|
|
|
readonly integrity_contradictions: ArtifactInventorySnapshot["integrity_contradictions"];
|
2026-10-12 04:07:14 +08:00
|
|
|
readonly source_selection?: JavaScriptSourceSelection;
|
2026-07-16 05:29:40 +08:00
|
|
|
readonly graph: JavaScriptApplicationGraph;
|
2026-07-22 23:07:51 +00:00
|
|
|
readonly semantic_graph: JavaScriptSemanticGraph;
|
2026-07-16 06:45:34 +08:00
|
|
|
readonly electron_summary: ElectronBoundarySummary;
|
2026-07-16 05:29:40 +08:00
|
|
|
readonly statistics: {
|
|
|
|
|
readonly relevant_files: number;
|
|
|
|
|
readonly nested_asar_containers: number;
|
|
|
|
|
readonly text_bytes_read: number;
|
|
|
|
|
readonly invalid_utf8_files: number;
|
|
|
|
|
readonly parsed_javascript_files: number;
|
2026-10-17 12:33:41 +05:00
|
|
|
readonly skipped_javascript_files: number;
|
2026-07-16 05:29:40 +08:00
|
|
|
readonly visited_ast_nodes: number;
|
|
|
|
|
readonly findings: number;
|
|
|
|
|
readonly modules: number;
|
|
|
|
|
readonly parse_failures: number;
|
|
|
|
|
};
|
|
|
|
|
readonly limitations: readonly string[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 15:02:26 +08:00
|
|
|
/** Reconstruct a parsed local ASAR/directory request without executing code. */
|
2026-07-16 05:29:40 +08:00
|
|
|
export const reconstructJavaScriptArtifact = async (
|
2026-10-24 15:02:26 +08:00
|
|
|
input: AnalyzeJavaScriptApplicationInput,
|
2026-10-11 04:41:49 +08:00
|
|
|
scope: ArtifactResourceScope,
|
2026-07-16 05:29:40 +08:00
|
|
|
signal?: AbortSignal,
|
2026-10-08 03:19:37 +08:00
|
|
|
progress?: ProgressReporter,
|
2026-10-21 21:18:22 -07:00
|
|
|
asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION,
|
2026-10-11 04:41:49 +08:00
|
|
|
): Promise<JavaScriptArtifactReconstructionResult> =>
|
|
|
|
|
scope.run(async () => {
|
|
|
|
|
const reportPhase = async (
|
|
|
|
|
phase: string,
|
|
|
|
|
message: string,
|
|
|
|
|
): Promise<void> => {
|
2026-10-12 02:39:52 +08:00
|
|
|
await progress?.report({
|
2026-10-11 07:22:50 +08:00
|
|
|
phase,
|
|
|
|
|
completed: 0,
|
|
|
|
|
total: 1,
|
|
|
|
|
message,
|
|
|
|
|
});
|
2026-10-12 02:39:52 +08:00
|
|
|
await checkpointJavaScriptAnalysis(signal);
|
2026-10-11 04:41:49 +08:00
|
|
|
};
|
2026-07-16 05:29:40 +08:00
|
|
|
abortIfNeeded(signal);
|
2026-10-24 03:18:32 +08:00
|
|
|
const { path, metadata } = await resolveSelectedInput(input.input_path);
|
|
|
|
|
const format = resolveFormat(path, metadata, input);
|
2026-10-11 04:41:49 +08:00
|
|
|
const snapshot = await scanCanonicalArtifactInventoryInScope(path, {
|
|
|
|
|
resourceScope: scope,
|
2026-10-09 02:40:21 +08:00
|
|
|
signal,
|
2026-10-11 04:41:49 +08:00
|
|
|
integrity: { mode: input.integrity_policy },
|
2026-10-21 21:18:22 -07:00
|
|
|
asarHeaderAdmission,
|
2026-10-11 04:41:49 +08:00
|
|
|
});
|
|
|
|
|
if (snapshot.manifest.root_format !== format)
|
|
|
|
|
throw new ArtifactReaderFailure(
|
|
|
|
|
"format",
|
|
|
|
|
`Artifact inventory classified ${path} as ${snapshot.manifest.root_format}, not ${format}`,
|
|
|
|
|
);
|
2026-10-21 21:18:22 -07:00
|
|
|
const reader = createReader(path, format, asarHeaderAdmission);
|
2026-10-11 07:22:50 +08:00
|
|
|
const readerOwner = {
|
|
|
|
|
kind: "reader" as const,
|
|
|
|
|
reader,
|
|
|
|
|
resource: `JavaScript artifact reader for ${path}`,
|
|
|
|
|
};
|
|
|
|
|
let readerOpen = true;
|
|
|
|
|
let reconstructionOutcome:
|
2026-10-11 04:41:49 +08:00
|
|
|
| {
|
|
|
|
|
readonly kind: "completed";
|
|
|
|
|
readonly result: JavaScriptArtifactReconstructionResult;
|
|
|
|
|
}
|
|
|
|
|
| { readonly kind: "failed"; readonly cause: unknown };
|
|
|
|
|
try {
|
|
|
|
|
await reportPhase(
|
|
|
|
|
"read_javascript_artifacts",
|
|
|
|
|
"Reading inventoried JavaScript application sources",
|
|
|
|
|
);
|
2026-10-11 05:33:15 +08:00
|
|
|
const files = await readJavaScriptArtifactFiles(
|
|
|
|
|
reader,
|
|
|
|
|
snapshot,
|
|
|
|
|
scope,
|
|
|
|
|
signal,
|
2026-10-12 04:07:14 +08:00
|
|
|
input.source_paths,
|
2026-10-21 21:18:22 -07:00
|
|
|
asarHeaderAdmission,
|
2026-10-11 05:33:15 +08:00
|
|
|
);
|
2026-10-11 07:22:50 +08:00
|
|
|
// Release acquisition before analysis consumes these owned file facts.
|
|
|
|
|
const earlyCleanup = await scope.release(readerOwner);
|
|
|
|
|
readerOpen = false;
|
|
|
|
|
if (earlyCleanup.kind === "failed")
|
|
|
|
|
throw ArtifactReaderFailure.withCleanup(
|
|
|
|
|
earlyCleanup.cause,
|
|
|
|
|
ArtifactReaderFailure.cleanupObservation(
|
|
|
|
|
earlyCleanup.cause,
|
|
|
|
|
readerOwner.resource,
|
|
|
|
|
),
|
|
|
|
|
{ kind: "artifact-inventory", inventory: snapshot },
|
|
|
|
|
);
|
2026-10-11 04:41:49 +08:00
|
|
|
await reportPhase(
|
|
|
|
|
"parse_javascript_sources",
|
2026-10-16 03:46:00 +08:00
|
|
|
`Processing ${String(files.files.length - (files.source_selection?.unselected_files ?? 0))} selected application files`,
|
2026-10-11 04:41:49 +08:00
|
|
|
);
|
2026-10-12 09:28:59 +08:00
|
|
|
const semanticProjection = createJavaScriptSemanticGraphProjection({
|
2026-10-17 21:57:31 +08:00
|
|
|
...(input.max_semantic_unknowns === undefined
|
|
|
|
|
? {}
|
|
|
|
|
: { totalUnknowns: input.max_semantic_unknowns }),
|
|
|
|
|
...(input.max_semantic_unknowns_per_file === undefined
|
|
|
|
|
? {}
|
|
|
|
|
: { fileUnknowns: input.max_semantic_unknowns_per_file }),
|
2026-10-12 09:28:59 +08:00
|
|
|
...(input.max_semantic_nodes === undefined
|
|
|
|
|
? {}
|
|
|
|
|
: { totalNodes: input.max_semantic_nodes }),
|
|
|
|
|
...(input.max_semantic_nodes_per_file === undefined
|
|
|
|
|
? {}
|
|
|
|
|
: { fileNodes: input.max_semantic_nodes_per_file }),
|
|
|
|
|
});
|
2026-10-21 19:01:20 +08:00
|
|
|
const worker = await createJavaScriptSourceAnalysis(
|
2026-10-11 07:22:50 +08:00
|
|
|
{
|
|
|
|
|
heapMb:
|
|
|
|
|
input.max_heap_mb ?? JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.heapMb,
|
|
|
|
|
timeoutMs:
|
|
|
|
|
input.analysis_timeout_ms ??
|
|
|
|
|
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.timeoutMs,
|
2026-10-11 04:41:49 +08:00
|
|
|
},
|
2026-10-11 07:22:50 +08:00
|
|
|
files.files.filter(
|
|
|
|
|
(file) => file.kind === "javascript" && file.text.included,
|
|
|
|
|
).length,
|
2026-10-21 19:01:20 +08:00
|
|
|
{ scope },
|
2026-10-11 04:41:49 +08:00
|
|
|
);
|
2026-10-11 07:22:50 +08:00
|
|
|
let outcome;
|
|
|
|
|
let cleanupFailure: AnalysisError | undefined;
|
|
|
|
|
try {
|
|
|
|
|
outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
|
|
|
|
|
files,
|
|
|
|
|
worker,
|
|
|
|
|
semanticProjection,
|
|
|
|
|
async (file, completed, total) => {
|
|
|
|
|
abortIfNeeded(signal);
|
2026-10-16 03:46:00 +08:00
|
|
|
const ast =
|
|
|
|
|
javascriptArtifactAnalysisScopes[file.kind]?.authority ===
|
|
|
|
|
"ast-static-analysis";
|
2026-10-11 07:22:50 +08:00
|
|
|
await progress?.report({
|
2026-10-16 03:46:00 +08:00
|
|
|
phase: ast
|
|
|
|
|
? "parse_javascript_source"
|
|
|
|
|
: "process_javascript_artifact_metadata",
|
2026-10-11 07:22:50 +08:00
|
|
|
completed: 0,
|
|
|
|
|
total: 1,
|
2026-10-16 03:46:00 +08:00
|
|
|
message: `${ast ? "Parsing and projecting" : "Processing metadata for"} ${file.path} (${String(completed + 1)}/${String(total)})`,
|
2026-10-11 07:22:50 +08:00
|
|
|
});
|
|
|
|
|
abortIfNeeded(signal);
|
|
|
|
|
},
|
|
|
|
|
signal,
|
2026-10-24 02:34:27 +08:00
|
|
|
async (file, phase) => {
|
|
|
|
|
abortIfNeeded(signal);
|
|
|
|
|
await progress?.report({
|
|
|
|
|
phase: `javascript_worker_${phase}`,
|
|
|
|
|
completed: 0,
|
|
|
|
|
total: 1,
|
|
|
|
|
message: `JavaScript worker ${phase}: ${file.path}`,
|
|
|
|
|
});
|
|
|
|
|
abortIfNeeded(signal);
|
|
|
|
|
},
|
2026-10-11 07:22:50 +08:00
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
try {
|
|
|
|
|
const cleanup = await scope.release({
|
|
|
|
|
kind: "javascript-source-analysis",
|
|
|
|
|
analysis: worker,
|
|
|
|
|
resource: `JavaScript source analyzer for ${path}`,
|
|
|
|
|
});
|
|
|
|
|
if (cleanup.kind === "failed") throw cleanup.cause;
|
|
|
|
|
} catch (cause: unknown) {
|
|
|
|
|
if (!(cause instanceof AnalysisError)) throw cause;
|
|
|
|
|
cleanupFailure = cause;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const analysis = outcome.analysis;
|
2026-10-24 04:00:10 +08:00
|
|
|
let interruption = outcome.interruption;
|
2026-10-12 02:39:52 +08:00
|
|
|
// Source facts are owned now. Finish their projection cooperatively so a
|
|
|
|
|
// later cancellation can retain Evidence instead of discarding those facts.
|
|
|
|
|
const checkpointCompletedAnalysis = async (): Promise<void> => {
|
|
|
|
|
await setImmediate();
|
|
|
|
|
if (interruption == null && signal?.aborted === true)
|
2026-10-23 00:25:26 +08:00
|
|
|
interruption = new ArtifactOperationError(
|
|
|
|
|
OPERATION,
|
|
|
|
|
"cancelled",
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
{ cause: signal.reason },
|
|
|
|
|
);
|
2026-10-12 02:39:52 +08:00
|
|
|
};
|
|
|
|
|
const reportCompletedPhase = async (
|
|
|
|
|
phase: string,
|
|
|
|
|
message: string,
|
|
|
|
|
): Promise<void> => {
|
|
|
|
|
await checkpointCompletedAnalysis();
|
2026-10-24 04:00:10 +08:00
|
|
|
if (interruption == null && cleanupFailure === undefined)
|
2026-10-12 02:39:52 +08:00
|
|
|
await progress?.report({ phase, completed: 0, total: 1, message });
|
|
|
|
|
await checkpointCompletedAnalysis();
|
|
|
|
|
};
|
|
|
|
|
await reportCompletedPhase(
|
2026-10-11 04:41:49 +08:00
|
|
|
"build_javascript_application_graph",
|
|
|
|
|
"Constructing application and Electron boundary relationships",
|
|
|
|
|
);
|
|
|
|
|
const applicationGraphSteps = buildImmutableJavaScriptArtifactGraphSteps(
|
|
|
|
|
snapshot,
|
|
|
|
|
files,
|
|
|
|
|
analysis,
|
|
|
|
|
);
|
2026-10-12 02:39:52 +08:00
|
|
|
await reportCompletedPhase(
|
2026-10-11 04:41:49 +08:00
|
|
|
"seal_javascript_application_graph",
|
|
|
|
|
"Sealing the validated application graph",
|
|
|
|
|
);
|
|
|
|
|
const graph = await completeJavaScriptAnalysisSteps(
|
|
|
|
|
applicationGraphSteps,
|
|
|
|
|
);
|
2026-10-12 02:39:52 +08:00
|
|
|
await reportCompletedPhase(
|
2026-10-11 04:41:49 +08:00
|
|
|
"build_javascript_semantic_graph",
|
|
|
|
|
"Binding and validating static semantic relationships",
|
|
|
|
|
);
|
|
|
|
|
const semanticGraphSteps = semanticProjection.finishImmutableSteps(
|
|
|
|
|
snapshot.manifest.root_sha256,
|
|
|
|
|
graph,
|
|
|
|
|
);
|
2026-10-12 02:39:52 +08:00
|
|
|
await reportCompletedPhase(
|
2026-10-11 04:41:49 +08:00
|
|
|
"seal_javascript_semantic_graph",
|
|
|
|
|
"Validating and sealing the semantic graph",
|
|
|
|
|
);
|
2026-10-12 02:39:52 +08:00
|
|
|
const semanticGraph =
|
|
|
|
|
await completeJavaScriptAnalysisSteps(semanticGraphSteps);
|
|
|
|
|
await checkpointCompletedAnalysis();
|
2026-10-24 02:48:30 +08:00
|
|
|
const directoryPaths = new Set(
|
|
|
|
|
snapshot.occurrences
|
|
|
|
|
.filter(({ entry_kind }) => entry_kind === "directory")
|
|
|
|
|
.map(({ logical_path }) => logical_path),
|
|
|
|
|
);
|
|
|
|
|
const nextAppContext = createJavaScriptNextAppContext(
|
|
|
|
|
files.files,
|
|
|
|
|
analysis.jsonDocuments,
|
|
|
|
|
directoryPaths,
|
|
|
|
|
);
|
2026-10-11 07:22:50 +08:00
|
|
|
const completed = {
|
2026-10-22 16:54:14 +08:00
|
|
|
route_inventory: await completeJavaScriptAnalysisSteps(
|
|
|
|
|
buildJavaScriptRouteInventorySteps([
|
|
|
|
|
...analysis.files
|
|
|
|
|
.filter(({ file }) => file.kind === "javascript")
|
|
|
|
|
.map(({ file, javascript, analysis_failure }) => ({
|
|
|
|
|
key: file.path,
|
2026-10-24 02:48:30 +08:00
|
|
|
next_app_context: nextAppContext(file),
|
2026-10-22 16:54:14 +08:00
|
|
|
path: file.path,
|
|
|
|
|
url: null,
|
|
|
|
|
sha256: file.sha256,
|
|
|
|
|
container_sha256: file.container_sha256,
|
|
|
|
|
status:
|
|
|
|
|
javascript === null || javascript.parse_status === "failed"
|
|
|
|
|
? ("unavailable" as const)
|
|
|
|
|
: javascript.parse_error_count > 0
|
|
|
|
|
? ("partial" as const)
|
|
|
|
|
: ("complete" as const),
|
|
|
|
|
reason: !file.text.included
|
|
|
|
|
? file.text.reason
|
|
|
|
|
: (analysis_failure?.reason ??
|
|
|
|
|
(javascript === null
|
|
|
|
|
? "Source analysis is unavailable"
|
|
|
|
|
: javascript.parse_status === "failed"
|
|
|
|
|
? "Script parsing failed"
|
|
|
|
|
: javascript.parse_error_count > 0
|
|
|
|
|
? "Script parsing recovered from errors"
|
|
|
|
|
: null)),
|
|
|
|
|
facts: javascript?.routes ?? null,
|
|
|
|
|
})),
|
|
|
|
|
...files.unavailable_relevant_files
|
|
|
|
|
.filter((file) => file.kind === "javascript")
|
|
|
|
|
.map((file) => ({
|
|
|
|
|
key: file.path,
|
2026-10-24 02:48:30 +08:00
|
|
|
next_app_context: nextAppContext(file),
|
2026-10-22 16:54:14 +08:00
|
|
|
path: file.path,
|
|
|
|
|
url: null,
|
|
|
|
|
sha256: file.sha256,
|
|
|
|
|
container_sha256: file.container_sha256,
|
|
|
|
|
status: "unavailable" as const,
|
|
|
|
|
reason: "Inventoried JavaScript source bytes are unavailable",
|
|
|
|
|
facts: null,
|
|
|
|
|
})),
|
|
|
|
|
]),
|
|
|
|
|
),
|
2026-10-11 07:22:50 +08:00
|
|
|
input_path: path,
|
|
|
|
|
format,
|
|
|
|
|
root_artifact_sha256: snapshot.manifest.root_sha256,
|
|
|
|
|
inventory_manifest_id: snapshot.manifest.manifest_id,
|
|
|
|
|
inventory_graph_sha256: snapshot.manifest.graph_sha256,
|
|
|
|
|
integrity_contradictions: snapshot.integrity_contradictions,
|
2026-10-12 04:07:14 +08:00
|
|
|
...(files.source_selection === undefined
|
|
|
|
|
? {}
|
|
|
|
|
: { source_selection: files.source_selection }),
|
2026-10-11 07:22:50 +08:00
|
|
|
graph,
|
|
|
|
|
semantic_graph: semanticGraph,
|
2026-10-24 02:48:30 +08:00
|
|
|
electron_summary: summarizeElectronBoundaries(analysis, directoryPaths),
|
2026-10-11 07:22:50 +08:00
|
|
|
statistics: {
|
2026-10-14 16:53:16 +08:00
|
|
|
relevant_files:
|
|
|
|
|
files.files.length + files.unavailable_relevant_files.length,
|
2026-10-11 07:22:50 +08:00
|
|
|
nested_asar_containers: files.containers.length,
|
|
|
|
|
text_bytes_read: files.text_bytes_read,
|
|
|
|
|
invalid_utf8_files: files.invalid_utf8_files,
|
|
|
|
|
parsed_javascript_files: analysis.files.filter(
|
|
|
|
|
({ javascript }) => javascript !== null,
|
|
|
|
|
).length,
|
2026-10-17 12:33:41 +05:00
|
|
|
skipped_javascript_files: analysis.files.filter(
|
|
|
|
|
isSkippedJavaScriptAnalysis,
|
|
|
|
|
).length,
|
2026-10-11 07:22:50 +08:00
|
|
|
visited_ast_nodes: analysis.visited_ast_nodes,
|
|
|
|
|
findings: analysis.findings,
|
|
|
|
|
modules: analysis.modules,
|
|
|
|
|
parse_failures: analysis.parse_failures,
|
2026-10-11 04:41:49 +08:00
|
|
|
},
|
2026-10-11 07:22:50 +08:00
|
|
|
limitations: analysis.limitations,
|
2026-10-11 04:41:49 +08:00
|
|
|
};
|
2026-10-22 16:54:14 +08:00
|
|
|
await checkpointCompletedAnalysis();
|
2026-10-24 04:00:10 +08:00
|
|
|
const failure =
|
|
|
|
|
cleanupFailure === undefined
|
|
|
|
|
? interruption
|
|
|
|
|
: interruption === null
|
|
|
|
|
? cleanupFailure
|
|
|
|
|
: analysisErrorWithCleanupFailure(
|
|
|
|
|
interruption,
|
|
|
|
|
cleanupFailure,
|
|
|
|
|
OPERATION,
|
|
|
|
|
).bindCleanupDependencies([interruption, cleanupFailure]);
|
|
|
|
|
if (failure !== null) {
|
2026-10-11 07:22:50 +08:00
|
|
|
const { electron_summary: summary, ...application } = completed;
|
|
|
|
|
const partial = await completeJavaScriptAnalysisSteps(
|
|
|
|
|
parseOwnedJavaScriptApplicationAnalysisSteps({
|
|
|
|
|
...application,
|
|
|
|
|
summary,
|
|
|
|
|
limitations: completed.graph.limitations,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
const evidence =
|
|
|
|
|
await createOwnedJavaScriptApplicationEvidenceCooperatively(
|
2026-10-13 21:01:30 +05:30
|
|
|
input,
|
2026-10-11 07:22:50 +08:00
|
|
|
partial,
|
|
|
|
|
);
|
2026-10-24 04:00:10 +08:00
|
|
|
throw failure.retainPartialObservation(evidence);
|
2026-10-11 07:22:50 +08:00
|
|
|
}
|
|
|
|
|
reconstructionOutcome = { kind: "completed", result: completed };
|
2026-10-11 04:41:49 +08:00
|
|
|
} catch (cause: unknown) {
|
2026-10-11 07:22:50 +08:00
|
|
|
reconstructionOutcome = { kind: "failed", cause };
|
2026-10-11 04:41:49 +08:00
|
|
|
}
|
2026-10-11 07:22:50 +08:00
|
|
|
if (readerOpen) {
|
|
|
|
|
const cleanupAttempt = await scope.release(readerOwner);
|
|
|
|
|
if (cleanupAttempt.kind === "failed") {
|
|
|
|
|
const cleanup = ArtifactReaderFailure.cleanupObservation(
|
|
|
|
|
cleanupAttempt.cause,
|
|
|
|
|
readerOwner.resource,
|
|
|
|
|
);
|
|
|
|
|
const partialObservation: ArtifactInventoryPartialObservation = {
|
|
|
|
|
kind: "artifact-inventory",
|
|
|
|
|
inventory: snapshot,
|
|
|
|
|
};
|
|
|
|
|
throw ArtifactReaderFailure.withCleanup(
|
|
|
|
|
reconstructionOutcome.kind === "failed"
|
|
|
|
|
? reconstructionOutcome.cause
|
|
|
|
|
: cleanupAttempt.cause,
|
|
|
|
|
cleanup,
|
|
|
|
|
partialObservation,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-11 04:41:49 +08:00
|
|
|
}
|
2026-10-11 07:22:50 +08:00
|
|
|
if (reconstructionOutcome.kind === "failed")
|
|
|
|
|
throw reconstructionOutcome.cause;
|
|
|
|
|
return reconstructionOutcome.result;
|
2026-10-11 04:41:49 +08:00
|
|
|
});
|
2026-07-16 05:29:40 +08:00
|
|
|
|
2026-10-09 09:13:17 +00:00
|
|
|
const OPERATION = "analyze_javascript_application";
|
|
|
|
|
|
|
|
|
|
/**
|
2026-10-21 03:45:10 +08:00
|
|
|
* Resolve the caller-selected input. A missing, looping, overlong, or
|
|
|
|
|
* unreadable selection is a caller or host-resolution failure, not a damaged
|
|
|
|
|
* artifact.
|
2026-10-09 09:13:17 +00:00
|
|
|
*/
|
2026-10-24 03:18:32 +08:00
|
|
|
const resolveSelectedInput = async (
|
|
|
|
|
inputPath: string,
|
|
|
|
|
): Promise<{ readonly path: string; readonly metadata: Stats }> => {
|
2026-10-09 09:13:17 +00:00
|
|
|
try {
|
|
|
|
|
const path = await realpath(inputPath);
|
2026-10-24 03:18:32 +08:00
|
|
|
const metadata = await lstat(path);
|
|
|
|
|
// Directory enumeration needs read access; resolving its entries also needs search access.
|
|
|
|
|
await access(
|
|
|
|
|
path,
|
|
|
|
|
metadata.isDirectory() ? constants.R_OK | constants.X_OK : constants.R_OK,
|
|
|
|
|
);
|
|
|
|
|
return { path, metadata };
|
2026-10-09 09:13:17 +00:00
|
|
|
} catch (cause: unknown) {
|
|
|
|
|
const code =
|
|
|
|
|
cause instanceof Error && "code" in cause ? String(cause.code) : "";
|
|
|
|
|
if (code === "EACCES" || code === "EPERM")
|
|
|
|
|
throw new AnalysisAccessDeniedError(OPERATION, inputPath, code, {
|
|
|
|
|
cause,
|
|
|
|
|
});
|
|
|
|
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
|
|
|
throw new AnalysisInputError(OPERATION, { cause }, [
|
|
|
|
|
{
|
|
|
|
|
path: ["input_path"],
|
|
|
|
|
reason: "invalid_value",
|
|
|
|
|
message: `No file or directory exists at the selected input path (${code}): ${inputPath}`,
|
|
|
|
|
},
|
|
|
|
|
]);
|
2026-10-21 03:45:10 +08:00
|
|
|
if (code === "ELOOP" || code === "ENAMETOOLONG")
|
|
|
|
|
throw new AnalysisInputError(OPERATION, { cause }, [
|
|
|
|
|
{
|
|
|
|
|
path: ["input_path"],
|
|
|
|
|
reason: "invalid_value",
|
|
|
|
|
message: selectedInputResolutionMessage(code, inputPath),
|
|
|
|
|
},
|
|
|
|
|
]);
|
2026-10-09 09:13:17 +00:00
|
|
|
throw cause;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-21 03:45:10 +08:00
|
|
|
const selectedInputResolutionMessage = (
|
|
|
|
|
code: "ELOOP" | "ENAMETOOLONG",
|
|
|
|
|
inputPath: string,
|
|
|
|
|
): string =>
|
|
|
|
|
code === "ENAMETOOLONG"
|
|
|
|
|
? `Selected input path exceeds the host path or component length limit (ENAMETOOLONG): ${inputPath}`
|
|
|
|
|
: `Selected input path could not be resolved because symbolic-link traversal looped or exceeded the host limit (ELOOP): ${inputPath}`;
|
|
|
|
|
|
2026-10-09 09:13:17 +00:00
|
|
|
/** The selected input's kind, checked against the caller's requested format. */
|
2026-10-24 03:18:32 +08:00
|
|
|
const resolveFormat = (
|
2026-07-16 05:29:40 +08:00
|
|
|
path: string,
|
2026-10-24 03:18:32 +08:00
|
|
|
metadata: Stats,
|
2026-10-24 15:02:26 +08:00
|
|
|
input: AnalyzeJavaScriptApplicationInput,
|
2026-10-24 03:18:32 +08:00
|
|
|
): "asar" | "directory" => {
|
2026-07-16 05:29:40 +08:00
|
|
|
const observed = metadata.isDirectory()
|
|
|
|
|
? "directory"
|
|
|
|
|
: metadata.isFile() && path.toLowerCase().endsWith(".asar")
|
|
|
|
|
? "asar"
|
|
|
|
|
: undefined;
|
|
|
|
|
if (observed === undefined)
|
2026-10-09 09:13:17 +00:00
|
|
|
throw new AnalysisUnsupportedTargetError(
|
|
|
|
|
OPERATION,
|
|
|
|
|
input.input_path,
|
|
|
|
|
"JavaScript application analysis accepts only a directory or an .asar file",
|
|
|
|
|
{
|
|
|
|
|
remediationAction:
|
|
|
|
|
"Select an extracted application directory, an app bundle, or an .asar file such as Contents/Resources/app.asar.",
|
|
|
|
|
},
|
2026-07-16 05:29:40 +08:00
|
|
|
);
|
|
|
|
|
if (input.format !== "auto" && input.format !== observed)
|
2026-10-09 09:13:17 +00:00
|
|
|
throw new AnalysisInputError(OPERATION, undefined, [
|
|
|
|
|
{
|
|
|
|
|
path: ["format"],
|
|
|
|
|
reason: "invalid_value",
|
|
|
|
|
message: `Requested ${input.format} input, but the selected input is ${observed === "asar" ? "an .asar file" : "a directory"}: ${input.input_path}`,
|
|
|
|
|
expected: ["auto", observed],
|
|
|
|
|
},
|
|
|
|
|
]);
|
2026-07-16 05:29:40 +08:00
|
|
|
return observed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const abortIfNeeded = (signal?: AbortSignal): void => {
|
|
|
|
|
if (signal?.aborted === true)
|
|
|
|
|
throw new ArtifactReaderFailure(
|
|
|
|
|
"cancelled",
|
|
|
|
|
"JavaScript artifact reconstruction cancelled",
|
2026-10-21 13:11:15 -07:00
|
|
|
{ cause: signal.reason },
|
2026-07-16 05:29:40 +08:00
|
|
|
);
|
|
|
|
|
};
|