mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
* fix(javascript): preserve literal percent runtime paths * fix(javascript): ignore resolve prefixes before the final anchor * fix(electron): retain application roots for loadFile declarations * fix(electron): resolve loadFile from enclosing application package * fix(electron): retain outer application root across package scopes * fix(electron): derive application ownership from inventoried packages * fix(electron): retain resolved package entrypoint ownership * fix(electron): preserve competing application ownership * fix(electron): retain invalid and omitted runtime ownership * fix(electron): preserve omitted module ownership and runtime paths * fix(electron): preserve proven roots before confining page paths * fix(electron): retain possible owners behind missing imports * fix(electron): distinguish absent imports and owned page candidates * fix(electron): keep unavailable JSON out of owner gaps * fix(electron): retain exact loader formats and default entry status * fix(electron): retain unavailable entrypoint ownership candidates * test(ghidra): preserve Windows Node probe prerequisites * test(ghidra): assert cancellation during profile shutdown * fix(electron): separate examined imports from frontend ownership * fix(electron): retain unavailable directory entrypoint owners * test(jeb): retain Linux ownership prerequisite in isolation * fix(javascript): retain unchecked loader ownership and literal main paths * fix(javascript): preserve runtime paths and call-site module aliases * fix(javascript): bound module alias traversal and preserve projections * fix(javascript): retain host module and nested artifact path boundaries * fix(javascript): preserve ESM and archive module loader provenance * test(javascript): distinguish unmapped containers from missing exports * fix: retain default module aliases and Electron builtin imports * fix: preserve host file URL and inward ASAR ownership * fix: preserve CommonJS file URL classification in CI fixtures * fix: retain bound module loaders and Windows entrypoint uncertainty * fix: retain ASAR uncertainty and invalidate changed Electron app roots * fix(javascript): retain ownership uncertainty across loader boundaries * fix(javascript): preserve loader sources and bound alias queries * fix(javascript): retain destructured loader sources * fix(javascript): preserve loader uncertainty at resolution boundaries * fix(javascript): preserve callable and package launch ownership * fix(javascript): preserve captured values across member reads * test(process): assert tagged filesystem cancellation failures * fix(javascript): preserve inventory bases and invocation read timing * fix(javascript): preserve ownership across indirect and unread loads * test(javascript): order fixture directory creation on Windows * test(javascript): share ordered filesystem fixture writer * fix(javascript): unify loader provenance and package validation * fix(javascript): preserve captured Node require loader values * fix(javascript): preserve loader invocation authority * fix(javascript): verify generic loader invocation authority * fix(javascript): preserve conditional loader method alternatives * fix(javascript): require proven factories for source association * fix(javascript): honor replaced Electron app path setters * fix(javascript): prove path and assignment source ownership * fix(javascript): retain native delegate eligibility through shared writes * fix(process): settle stale baseline errors after confirmed absence * fix(javascript): preserve assignment and path boundary evidence * fix(javascript): prove source-directory resolve anchors * fix: preserve application path provenance and portable schemas * fix(javascript): retain CommonJS exported setter effects * fix: isolate Linux identity observations and retain native module exports * fix(javascript): preserve published app and helper provenance * fix(javascript): normalize typed stores and native path fixtures
396 lines
15 KiB
TypeScript
396 lines
15 KiB
TypeScript
import { ELECTRON_IDENTITY_LIMITATION } from "../../domain/javascript/javascriptElectronMemberWrites.js";
|
|
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
|
import {
|
|
createJavaScriptApplicationGraph,
|
|
sealTransferredJavaScriptApplicationGraphSteps,
|
|
type JavaScriptApplicationGraph,
|
|
} from "../../domain/javascript/javascriptApplicationGraph.js";
|
|
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFileSet } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
|
|
import {
|
|
HTML_INLINE_SCRIPT_LOCATION_COVERAGE_LIMIT,
|
|
omittedHtmlInlineScriptLocations,
|
|
selfReferenceOmissions,
|
|
dependencyManifest,
|
|
applicationContainerIdentity,
|
|
type JavaScriptArtifactGraphContext,
|
|
} from "./JavaScriptArtifactGraphContext.js";
|
|
import {
|
|
addJavaScriptHtmlRoles,
|
|
addJavaScriptSourceMapOriginals,
|
|
} from "./JavaScriptArtifactGraphDocuments.js";
|
|
import { addJavaScriptStaticFindingsSteps } from "./JavaScriptArtifactGraphFindings.js";
|
|
import {
|
|
addJavaScriptModuleRelationshipsSteps,
|
|
type JavaScriptModuleRelationshipOmissions,
|
|
addJavaScriptSourceModules,
|
|
} from "./JavaScriptModuleRelationships.js";
|
|
import {
|
|
completeApplicationCoverage,
|
|
partialApplicationCoverage,
|
|
truncatedApplicationCoverage,
|
|
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
|
import type { JavaScriptSemanticResourceLimit } from "../../domain/javascript/javascriptSemanticValueTypes.js";
|
|
import {
|
|
semanticCoverageResourceLimits,
|
|
semanticResourceLimitCoverage,
|
|
} from "../../domain/javascript/javascriptSemanticCoverage.js";
|
|
import { semanticResourceLimitReason } from "../../domain/javascript/javascriptSemanticResourceLimits.js";
|
|
import {
|
|
addJavaScriptArtifactContainers,
|
|
addJavaScriptArtifactFiles,
|
|
addJavaScriptPackageNodes,
|
|
createJavaScriptArtifactRootNode,
|
|
} from "./JavaScriptArtifactGraphStructure.js";
|
|
import { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
|
|
import { addElectronBoundaries } from "./ElectronBoundaryGraph.js";
|
|
import {
|
|
classifyElectronIpcPairings,
|
|
collectElectronIpcRecords,
|
|
} from "./ElectronBoundaryAnalysis.js";
|
|
|
|
/** Project artifact and AST facts into JavaScript Application Graph. */
|
|
export const buildJavaScriptArtifactGraph = (
|
|
snapshot: ArtifactInventorySnapshot,
|
|
fileSet: JavaScriptArtifactFileSet,
|
|
analysis: JavaScriptModuleArtifactAnalysis,
|
|
): JavaScriptApplicationGraph =>
|
|
createJavaScriptApplicationGraph(
|
|
completeSynchronously(
|
|
buildJavaScriptArtifactGraphInputSteps(snapshot, fileSet, analysis),
|
|
),
|
|
);
|
|
|
|
/**
|
|
* Build a validated application graph with cooperative immutable ownership.
|
|
* Constructing the graph input also runs in steps: projecting every module
|
|
* relationship and finding at once held the event loop for seconds.
|
|
*/
|
|
export function* buildImmutableJavaScriptArtifactGraphSteps(
|
|
snapshot: ArtifactInventorySnapshot,
|
|
fileSet: JavaScriptArtifactFileSet,
|
|
analysis: JavaScriptModuleArtifactAnalysis,
|
|
): Generator<void, JavaScriptApplicationGraph> {
|
|
const input = yield* buildJavaScriptArtifactGraphInputSteps(
|
|
snapshot,
|
|
fileSet,
|
|
analysis,
|
|
);
|
|
// The input was built here and is not retained, so it can be transferred.
|
|
return yield* sealTransferredJavaScriptApplicationGraphSteps(input);
|
|
}
|
|
|
|
const completeSynchronously = <Value>(steps: Iterator<void, Value>): Value => {
|
|
for (;;) {
|
|
const step = steps.next();
|
|
if (step.done === true) return step.value;
|
|
}
|
|
};
|
|
|
|
function* buildJavaScriptArtifactGraphInputSteps(
|
|
snapshot: ArtifactInventorySnapshot,
|
|
fileSet: JavaScriptArtifactFileSet,
|
|
analysis: JavaScriptModuleArtifactAnalysis,
|
|
): Generator<void, unknown> {
|
|
const accumulator = new JavaScriptArtifactGraphAccumulator();
|
|
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
|
|
const parsedPackages = new Set(
|
|
analysis.packages
|
|
.filter((metadata) => metadata.status === "included")
|
|
.map(({ path }) => path),
|
|
);
|
|
const applicationOwnershipUnknownContainers = new Set<string>();
|
|
const applicationModuleGapSources = new Map<string, Set<string>>();
|
|
const applicationModuleReferencesRetained = new Set<string>();
|
|
const recordModuleGap = (file: {
|
|
path: string;
|
|
container_sha256: string;
|
|
}) => {
|
|
const container = applicationContainerIdentity(fileSet, file);
|
|
const sources =
|
|
applicationModuleGapSources.get(container) ?? new Set<string>();
|
|
sources.add(file.path);
|
|
applicationModuleGapSources.set(container, sources);
|
|
};
|
|
for (const files of [fileSet.files, fileSet.unavailable_relevant_files]) {
|
|
for (const file of files) {
|
|
if (
|
|
file.kind === "package-json" &&
|
|
!dependencyManifest(file.path) &&
|
|
!parsedPackages.has(file.path)
|
|
)
|
|
applicationOwnershipUnknownContainers.add(
|
|
applicationContainerIdentity(fileSet, file),
|
|
);
|
|
if (
|
|
file.kind === "javascript" &&
|
|
(!("text" in file) || !file.text.included)
|
|
)
|
|
recordModuleGap(file);
|
|
}
|
|
}
|
|
// Omitted module relationships can hide another launch owner. Do not expand
|
|
// retained facts beyond the projection guard merely to recover ownership.
|
|
for (const analyzed of analysis.files) {
|
|
if (
|
|
analyzed.application_projection_failure !== undefined ||
|
|
analyzed.analysis_failure !== undefined ||
|
|
analyzed.javascript?.parse_status === "failed" ||
|
|
(analyzed.javascript?.parse_error_count ?? 0) > 0
|
|
)
|
|
recordModuleGap(analyzed.file);
|
|
else if (analyzed.javascript !== null)
|
|
applicationModuleReferencesRetained.add(analyzed.file.path);
|
|
}
|
|
const unavailableFilesByPath = new Map(
|
|
fileSet.unavailable_relevant_files.map((file) => [file.path, file]),
|
|
);
|
|
const context: JavaScriptArtifactGraphContext = {
|
|
accumulator,
|
|
snapshot,
|
|
fileSet,
|
|
analysis,
|
|
directoryPaths: new Set(
|
|
snapshot.occurrences
|
|
.filter(({ entry_kind }) => entry_kind === "directory")
|
|
.map(({ logical_path }) => logical_path),
|
|
),
|
|
root,
|
|
filesByPath: new Map(fileSet.files.map((file) => [file.path, file])),
|
|
unanalyzedFiles: {
|
|
get: (path) =>
|
|
unavailableFilesByPath.get(path) ?? fileSet.unanalyzed_files?.get(path),
|
|
hasDirectory: (path, container) =>
|
|
fileSet.unanalyzed_files?.hasDirectory(path, container) ?? false,
|
|
},
|
|
fileNodes: new Map(),
|
|
assetNodes: new Map(),
|
|
sourceModuleNodes: new Map(),
|
|
chunkNodes: new Map(),
|
|
moduleNodes: new Map(),
|
|
containerNodes: new Map([[snapshot.manifest.root_sha256, root]]),
|
|
applicationRootsBySource: new Map(),
|
|
applicationOwnershipUnknownContainers,
|
|
applicationModuleGapSources,
|
|
applicationModuleReferencesRetained,
|
|
applicationPossibleRootsByContainer: new Map(),
|
|
};
|
|
addJavaScriptArtifactContainers(context);
|
|
yield;
|
|
addJavaScriptArtifactFiles(context);
|
|
yield;
|
|
// Raw observations remain on the file nodes. Only admitted expansions feed
|
|
// the relationship builders, including their Electron and bundler callers.
|
|
const projectionContext = {
|
|
...context,
|
|
analysis: {
|
|
...analysis,
|
|
files: analysis.files.map((file) =>
|
|
file.application_projection_failure === undefined
|
|
? file
|
|
: { ...file, javascript: null, semantic: null },
|
|
),
|
|
},
|
|
};
|
|
const packageRoots = addJavaScriptPackageNodes(projectionContext);
|
|
yield;
|
|
addJavaScriptSourceModules(projectionContext);
|
|
yield;
|
|
const bundlerLimitations = addJavaScriptBundlerNodes(projectionContext);
|
|
const relationshipOmissions =
|
|
yield* addJavaScriptModuleRelationshipsSteps(projectionContext);
|
|
const findingLimitations =
|
|
yield* addJavaScriptStaticFindingsSteps(projectionContext);
|
|
addElectronBoundaries(projectionContext);
|
|
yield;
|
|
addJavaScriptHtmlRoles(projectionContext);
|
|
yield;
|
|
addJavaScriptSourceMapOriginals(projectionContext);
|
|
yield;
|
|
const coverage = graphCoverage(context);
|
|
return {
|
|
schema: "JavaScriptApplicationGraph",
|
|
root_node_ids:
|
|
packageRoots.length === 0
|
|
? [root.node_id]
|
|
: packageRoots.map(({ node_id: id }) => id),
|
|
nodes: accumulator.nodes(),
|
|
edges: accumulator.edges(),
|
|
coverage,
|
|
limitations: [
|
|
...bundlerLimitations,
|
|
...findingLimitations,
|
|
...graphLimitations(context, coverage.status, relationshipOmissions),
|
|
],
|
|
};
|
|
}
|
|
|
|
const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
|
|
const resourceLimits = semanticResourceLimits(context);
|
|
const sourceMapPolicyGap = context.analysis.source_maps.some(
|
|
({ status }) => status === "invalid",
|
|
);
|
|
const malformedStructuredData =
|
|
context.analysis.packages.some(({ status }) => status !== "included") ||
|
|
context.analysis.json_modules.some(({ status }) => status !== "included") ||
|
|
context.analysis.source_maps.some(({ status }) => status === "invalid");
|
|
const partialJavaScript = context.analysis.files.some(
|
|
({ javascript }) =>
|
|
javascript !== null && javascript.parse_status === "partial",
|
|
);
|
|
const unknownGap =
|
|
(context.fileSet.source_selection?.unselected_files ?? 0) > 0 ||
|
|
context.fileSet.unavailable_relevant_files.length > 0 ||
|
|
context.analysis.files.some(
|
|
({ analysis_failure, application_projection_failure }) =>
|
|
analysis_failure !== undefined ||
|
|
application_projection_failure !== undefined,
|
|
) ||
|
|
context.analysis.parse_failures > 0 ||
|
|
context.fileSet.invalid_utf8_files > 0 ||
|
|
context.analysis.html_inline_scripts.length > 0 ||
|
|
sourceMapPolicyGap ||
|
|
malformedStructuredData ||
|
|
partialJavaScript;
|
|
const executionLimits = context.analysis.files.flatMap(
|
|
({ analysis_failure, application_projection_failure }) =>
|
|
[analysis_failure, application_projection_failure].flatMap((failure) =>
|
|
failure === undefined
|
|
? []
|
|
: javaScriptAnalysisCoverageLimits(failure.limits),
|
|
),
|
|
);
|
|
// Capped inline script locations truncate the result. The total omission
|
|
// stays unknown because the unanalyzed bodies hide an unknown fact count.
|
|
const inlineScriptsTruncated =
|
|
omittedHtmlInlineScriptLocations(context.analysis.html_inline_scripts) > 0;
|
|
if (
|
|
resourceLimits.length > 0 ||
|
|
executionLimits.length > 0 ||
|
|
inlineScriptsTruncated
|
|
) {
|
|
const limits = [
|
|
...new Map(
|
|
[
|
|
...semanticResourceLimitCoverage(resourceLimits),
|
|
...executionLimits,
|
|
...(inlineScriptsTruncated
|
|
? [HTML_INLINE_SCRIPT_LOCATION_COVERAGE_LIMIT]
|
|
: []),
|
|
].map((limit) => [JSON.stringify(limit), limit]),
|
|
).values(),
|
|
];
|
|
return inlineScriptsTruncated
|
|
? truncatedApplicationCoverage(limits, null)
|
|
: partialApplicationCoverage(limits, null);
|
|
}
|
|
if (unknownGap) return partialApplicationCoverage([], null);
|
|
if (context.snapshot.integrity_contradictions.length > 0) {
|
|
const nestedArchiveWasOpaque =
|
|
context.snapshot.integrity_contradictions.some(({ logical_path }) =>
|
|
logical_path.toLowerCase().endsWith(".asar"),
|
|
);
|
|
return partialApplicationCoverage([], nestedArchiveWasOpaque ? null : 0);
|
|
}
|
|
return completeApplicationCoverage();
|
|
};
|
|
|
|
const semanticResourceLimits = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
): JavaScriptSemanticResourceLimit[] =>
|
|
[
|
|
...new Set(
|
|
context.analysis.files.flatMap(({ semantic }) =>
|
|
semantic === null
|
|
? []
|
|
: semanticCoverageResourceLimits(semantic.ir.coverage),
|
|
),
|
|
),
|
|
].sort();
|
|
|
|
const graphLimitations = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
coverage: "complete" | "partial" | "unknown" | "unavailable",
|
|
relationshipOmissions: JavaScriptModuleRelationshipOmissions,
|
|
): string[] => {
|
|
const ipc = collectElectronIpcRecords(context.analysis);
|
|
const pairings = classifyElectronIpcPairings(ipc);
|
|
const electronFindings = context.analysis.files.reduce(
|
|
(count, { javascript }) =>
|
|
count +
|
|
(javascript === null
|
|
? 0
|
|
: javascript.electron.browser_windows.length +
|
|
javascript.electron.context_bridge_apis.length +
|
|
javascript.electron.ipc.length +
|
|
javascript.electron.sender_validations.length +
|
|
javascript.electron.utility_processes.length +
|
|
javascript.electron.native_addon_bindings.length),
|
|
0,
|
|
);
|
|
return [
|
|
...context.analysis.limitations,
|
|
...(context.fileSet.source_selection === undefined
|
|
? []
|
|
: [
|
|
`Caller-selected source analysis leaves ${String(context.fileSet.source_selection.unselected_files)} inventoried files outside analysis scope. Inventory identity covers the complete artifact; relationships through unselected sources remain unknown.`,
|
|
]),
|
|
...context.snapshot.integrity_contradictions.map(
|
|
({ logical_path: path }) =>
|
|
`Artifact integrity metadata contradicts observed bytes at ${path}; the observed bytes are untrusted.`,
|
|
),
|
|
...selfReferenceOmissions(
|
|
relationshipOmissions.selfImports,
|
|
["import specifier", "import specifiers"],
|
|
"the importing module",
|
|
),
|
|
"CommonJS and ESM binding relationships were recovered from inert syntax and resolved only within the inventoried artifact container.",
|
|
"Webpack/Rspack factories were recovered from AST literals; REA did not invoke push handlers or bundle bootstrap code.",
|
|
"Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.",
|
|
...(context.analysis.files.some(({ javascript }) =>
|
|
javascript?.limitations.includes(ELECTRON_IDENTITY_LIMITATION),
|
|
)
|
|
? [ELECTRON_IDENTITY_LIMITATION]
|
|
: []),
|
|
...(electronFindings === 0
|
|
? []
|
|
: [
|
|
"Electron relationships are derived from inert syntax; runtime registration, reachability, defaults, and enforcement remain unproven.",
|
|
]),
|
|
...(ipc.some(({ finding }) => finding.channel === null)
|
|
? [
|
|
"Dynamic IPC channel expressions remain unresolved and are never paired by textual similarity.",
|
|
]
|
|
: []),
|
|
...(pairings.some(({ status }) => status === "ambiguous")
|
|
? [
|
|
"Some literal IPC channels have multiple compatible main handlers; ambiguous calls are not paired to any handler.",
|
|
]
|
|
: []),
|
|
...(context.analysis.files.some(
|
|
({ javascript }) =>
|
|
(javascript?.electron.sender_validations.length ?? 0) > 0,
|
|
)
|
|
? [
|
|
"Sender, frame, URL, and origin checks are observations only; REA does not claim that they enforce a complete authorization policy.",
|
|
]
|
|
: []),
|
|
...semanticResourceLimits(context).map(semanticResourceLimitReason),
|
|
...(context.analysis.files.some(
|
|
({ javascript }) =>
|
|
(javascript?.electron.native_addon_bindings.length ?? 0) > 0,
|
|
)
|
|
? [
|
|
"Native member names are requested by JavaScript syntax and are not verified binary exports in this workflow.",
|
|
]
|
|
: []),
|
|
...(coverage === "complete"
|
|
? []
|
|
: [
|
|
"Graph coverage is incomplete; unavailable facts remain explicit and must not be read as absence.",
|
|
]),
|
|
];
|
|
};
|
|
import { javaScriptAnalysisCoverageLimits } from "../../domain/javascript/javascriptAnalysisResourceControls.js";
|