mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
206 lines
7.1 KiB
TypeScript
206 lines
7.1 KiB
TypeScript
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
|
|
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
|
import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
|
|
import {
|
|
collectElectronIpcRecords,
|
|
unambiguousElectronIpcPairings,
|
|
type ElectronIpcRecord,
|
|
} from "./ElectronBoundaryAnalysis.js";
|
|
import {
|
|
addElectronInferenceEdge,
|
|
electronFindingSourceNode,
|
|
} from "./ElectronBoundaryGraphContext.js";
|
|
import {
|
|
artifactLocalIdentity,
|
|
javascriptAnalysisCoverage,
|
|
type JavaScriptArtifactGraphContext,
|
|
type JavaScriptArtifactGraphCoverage,
|
|
} from "./JavaScriptArtifactGraphContext.js";
|
|
import { staticInferenceEvidence } from "./JavaScriptArtifactGraphEvidence.js";
|
|
|
|
interface ProjectedPath {
|
|
readonly file: JavaScriptArtifactFile;
|
|
readonly path: ElectronCallPath;
|
|
readonly coverage: JavaScriptArtifactGraphCoverage;
|
|
readonly nodes: readonly ApplicationNode[];
|
|
}
|
|
|
|
/** Retain context-specific wrapper paths, then join only unique IPC records. */
|
|
export const addElectronCallPaths = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
): void => {
|
|
const projected: ProjectedPath[] = [];
|
|
for (const { file, javascript } of context.analysis.files) {
|
|
if (javascript === null) continue;
|
|
const coverage = javascriptAnalysisCoverage(javascript);
|
|
for (const path of javascript.electron.call_paths ?? []) {
|
|
const pathId = sha256Text(JSON.stringify(path));
|
|
const nodes = path.steps.map((step, index) =>
|
|
context.accumulator.addNode({
|
|
kind: "javascript-expression",
|
|
identity: artifactLocalIdentity(
|
|
file.sha256,
|
|
"electron-call-path-step",
|
|
`${pathId}:${String(index)}`,
|
|
),
|
|
observations: [
|
|
{
|
|
label: step.label,
|
|
properties: {
|
|
path_id: pathId,
|
|
entry: path.entry,
|
|
...(path.entry_path === undefined
|
|
? {}
|
|
: { entry_path: [...path.entry_path] }),
|
|
entry_kind: path.entry_kind,
|
|
channel: path.channel,
|
|
terminal: path.terminal,
|
|
step_kind: step.kind,
|
|
step_index: index,
|
|
module_key: step.module_key,
|
|
module_runtime: step.module_runtime,
|
|
conditions: path.conditions,
|
|
resolution: "candidate",
|
|
},
|
|
evidence: staticInferenceEvidence({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: step.location,
|
|
operation: "substitute-electron-wrapper",
|
|
coverage,
|
|
limitations: [
|
|
"Lexical wrapper substitution is a static candidate; guards, runtime overrides and execution remain unverified.",
|
|
],
|
|
}),
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const first = nodes[0],
|
|
entry = path.steps[0];
|
|
const owner = entry
|
|
? electronFindingSourceNode(
|
|
context,
|
|
file,
|
|
entry.module_key,
|
|
entry.module_runtime ?? undefined,
|
|
)
|
|
: undefined;
|
|
if (first && entry && owner)
|
|
addElectronInferenceEdge(context, {
|
|
source: owner,
|
|
target: first,
|
|
file,
|
|
range: entry.location,
|
|
coverage,
|
|
relation: "contains",
|
|
operation: "locate-electron-wrapper-path",
|
|
properties: {
|
|
path_id: pathId,
|
|
entry: path.entry,
|
|
...(path.entry_path === undefined
|
|
? {}
|
|
: { entry_path: [...path.entry_path] }),
|
|
resolution: "candidate",
|
|
},
|
|
});
|
|
for (let i = 1; i < nodes.length; i += 1) {
|
|
const source = nodes[i - 1],
|
|
target = nodes[i],
|
|
previous = path.steps[i - 1],
|
|
step = path.steps[i];
|
|
if (!source || !target || !previous || !step) continue;
|
|
addElectronInferenceEdge(context, {
|
|
source,
|
|
target,
|
|
file,
|
|
range: step.location,
|
|
coverage,
|
|
relation: ["exposure", "factory", "ipc"].includes(previous.kind)
|
|
? "maps_to"
|
|
: "calls",
|
|
operation: "trace-electron-wrapper-path",
|
|
properties: {
|
|
path_id: pathId,
|
|
entry: path.entry,
|
|
...(path.entry_path === undefined
|
|
? {}
|
|
: { entry_path: [...path.entry_path] }),
|
|
channel: path.channel,
|
|
resolution: "candidate",
|
|
conditions: path.conditions,
|
|
},
|
|
});
|
|
}
|
|
projected.push({ file, path, coverage, nodes });
|
|
}
|
|
}
|
|
for (const { transmission, handler } of unambiguousElectronIpcPairings(
|
|
collectElectronIpcRecords(context.analysis),
|
|
)) {
|
|
const sources = projected.filter(
|
|
(p) => p.path.entry_kind === "bridge-member" && matches(p, transmission),
|
|
);
|
|
const targets = projected.filter(
|
|
(p) => p.path.entry_kind === "ipc-handler" && matches(p, handler),
|
|
);
|
|
for (const from of sources)
|
|
for (const to of targets) {
|
|
const source = from.nodes.at(-1),
|
|
target = to.nodes[0];
|
|
if (!source || !target) continue;
|
|
addElectronInferenceEdge(context, {
|
|
source,
|
|
target,
|
|
file: from.file,
|
|
range: transmission.finding.location,
|
|
coverage: from.coverage,
|
|
relation:
|
|
transmission.finding.mode === "invoke" ? "invokes" : "sends",
|
|
operation: "join-electron-wrapper-paths",
|
|
properties: {
|
|
channel: transmission.finding.channel,
|
|
pairing_basis: "unique-exact-literal-channel",
|
|
resolution: "candidate",
|
|
handler_path: to.file.path,
|
|
bridge_conditions: from.path.conditions,
|
|
handler_conditions: to.path.conditions,
|
|
},
|
|
limitations: [
|
|
"The unique static channel match does not establish runtime registration, sender acceptance, or dispatch.",
|
|
],
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const matches = (
|
|
projected: ProjectedPath,
|
|
record: ElectronIpcRecord,
|
|
): boolean => {
|
|
const { path } = projected;
|
|
const { finding } = record;
|
|
const invocation = finding.invocation_location;
|
|
const sourceStepKind =
|
|
finding.mode === "send" || finding.mode === "invoke" ? "terminal" : "ipc";
|
|
return (
|
|
projected.file.path === record.file.path &&
|
|
path.channel === finding.channel &&
|
|
path.steps.some(
|
|
(step) =>
|
|
step.kind === sourceStepKind &&
|
|
javascriptSourceRangeKey(step.location) ===
|
|
javascriptSourceRangeKey(finding.location),
|
|
) &&
|
|
(invocation === undefined ||
|
|
path.steps.some(
|
|
(step) =>
|
|
(step.kind === "call" || step.kind === "exposure") &&
|
|
javascriptSourceRangeKey(step.location) ===
|
|
javascriptSourceRangeKey(invocation),
|
|
))
|
|
);
|
|
};
|