Files
reverse-engineering-anything/src/application/javascript/ElectronBoundaryGraphCallPaths.ts
T
1201c25542 fix(ci): align static checks and regression fixtures with current contracts (#7349)
* fix(ci): retire unused internal analysis declarations

* test: use an admitted app bundle for provider routing

(cherry picked from commit 00aa17f12a)

* test: keep bundle routing coverage outside oversized group

(cherry picked from commit 8658353b0d)

* fix(ci): align regression fixtures with current admission contracts

* test: bind extraction close fixtures to their ZIP digest

(cherry picked from commit 626d0250db)

* test(cli): match typed analysis outcomes in function detail fixture

* test(ghidra): preserve empty Java property separator framing

* test(electron): retain unknown IPC ownership for native module gaps

* fix(electron): share source-scoped call path identity

* test(captures): assert safe source identity on SAZ refusal

* test(apple): match root occurrence kind to inventory manifest

* fix(examples): align directory root occurrence identity

* test(electron): align equivalent fixture assertions with upstream

* test(process): assert retained capture root identity

* test(windows): declare the PE fixture optional header

* test(ci): refresh owned registration and verifier fixtures

* test(setup): use owned registrations for refresh fixtures

* test(process): control unavailable ownership in CLI cleanup fixture

* test(process): guard controlled compiler output arguments

* test(setup): align equivalent entrypoint guards with upstream

* test(process): release the cleanup target through an owned gate

* test(ci): align semantic identities and owned fixture facts

* test(pcap): retain the upstream DNS wire fixture branch

* test(pcap): use the canonical wire ownership fixture

* test(pcap): assert exact DNS wire query labels

* test(ci): preserve admission and ownership fixture prerequisites

* test(ci): align authenticated fixture and cleanup boundaries

* test(windows): align native artifact fixtures with upstream

* test(android): request complete evidence for inline MCP contracts

* test: avoid pinning diagnostic wording

* test: verify cleanup diagnostics at their source

* test: strengthen evidence and cleanup checks

* test: avoid pinning registration guidance

---------

Co-authored-by: Plastic-time <Plastic-time@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-27 21:24:30 +08:00

204 lines
7.0 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 { electronCallPathId } from "./ElectronCallPathIdentity.js";
import type {
ElectronIpcContext,
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,
ipc: ElectronIpcContext,
): 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 = electronCallPathId(file.path, 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 ipc.pairings) {
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),
))
);
};