mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
b8fab74297
Keep direct empty bridge objects observed, reject unexecuted initializers, invalidate modified bootstrap tables and Electron identities, and retain selection predicates through symbolic values and wrapper paths. Track lexical binding identity and completed activation boundaries without leaking analysis order into earlier reads. Separate wrapper invocation provenance from the underlying IPC source range so repeated registrations remain ambiguous through transfer and graph joins. Use the same offered transfer budget for module preflight and final writing. Index static literal properties once instead of rescanning every member. Keep the original 10,001-member capacity fixture; Flameox confirmed the repeated lookup cost and the optimized fixture completes in under one second locally. Prune the stale compact-schema tool inventory assertion and update the public call-graph registration expectation to its declared result envelope. Validation: build, check, docs:check, 83 focused tests, 10 Electron identity acceptance cases, 2 full-scale finding-capacity cases, and 7 compact-schema transport tests. No real Electron runtime execution is claimed.
372 lines
12 KiB
TypeScript
372 lines
12 KiB
TypeScript
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
|
import type { ElectronSenderValidationFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
import {
|
|
artifactLocalIdentity,
|
|
javascriptAnalysisCoverage,
|
|
type JavaScriptArtifactGraphContext,
|
|
type JavaScriptArtifactGraphCoverage,
|
|
} from "./JavaScriptArtifactGraphContext.js";
|
|
import {
|
|
astObservationEvidence,
|
|
staticInferenceEvidence,
|
|
} from "./JavaScriptArtifactGraphEvidence.js";
|
|
import {
|
|
collectElectronIpcRecords,
|
|
unambiguousElectronIpcPairings,
|
|
type ElectronIpcRecord,
|
|
} from "./ElectronBoundaryAnalysis.js";
|
|
import {
|
|
addElectronAstEdge,
|
|
addElectronInferenceEdge,
|
|
electronFindingSourceNode,
|
|
electronObservationIdentity,
|
|
electronRangeContains,
|
|
electronRangeKey,
|
|
} from "./ElectronBoundaryGraphContext.js";
|
|
|
|
interface IpcGraphState {
|
|
readonly context: JavaScriptArtifactGraphContext;
|
|
readonly records: readonly ElectronIpcRecord[];
|
|
readonly channels: Map<string, ApplicationNode>;
|
|
readonly handlers: Map<string, ApplicationNode>;
|
|
readonly sources: Map<string, ApplicationNode>;
|
|
}
|
|
|
|
interface IpcHandlerInput {
|
|
readonly context: JavaScriptArtifactGraphContext;
|
|
readonly record: ElectronIpcRecord;
|
|
readonly source: ApplicationNode;
|
|
readonly channel: ApplicationNode;
|
|
readonly coverage: JavaScriptArtifactGraphCoverage;
|
|
}
|
|
|
|
interface ValidationObservationInput {
|
|
readonly context: JavaScriptArtifactGraphContext;
|
|
readonly file: JavaScriptArtifactFile;
|
|
readonly validation: ElectronSenderValidationFinding;
|
|
readonly target: ApplicationNode;
|
|
readonly coverage: JavaScriptArtifactGraphCoverage;
|
|
}
|
|
|
|
/** Project IPC syntax, handler locations, validations, and exact pairings. */
|
|
export const addElectronIpcBoundaries = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
): void => {
|
|
const state: IpcGraphState = {
|
|
context,
|
|
records: collectElectronIpcRecords(context.analysis),
|
|
channels: new Map(),
|
|
handlers: new Map(),
|
|
sources: new Map(),
|
|
};
|
|
for (const record of state.records) addIpcRecord(state, record);
|
|
addValidationObservations(state);
|
|
addLiteralPairings(state);
|
|
};
|
|
|
|
const addIpcRecord = (
|
|
state: IpcGraphState,
|
|
record: ElectronIpcRecord,
|
|
): void => {
|
|
const { context } = state;
|
|
const coverage = coverageFor(context, record.file);
|
|
if (coverage === undefined) return;
|
|
const source = electronFindingSourceNode(
|
|
context,
|
|
record.file,
|
|
record.finding.module_key,
|
|
record.finding.module_runtime,
|
|
);
|
|
if (source === undefined) return;
|
|
state.sources.set(record.key, source);
|
|
const channel = addChannelNode(context, record, coverage);
|
|
state.channels.set(record.key, channel);
|
|
if (record.finding.mode === "listen" || record.finding.mode === "handle") {
|
|
const handler = addHandlerNode({
|
|
context,
|
|
record,
|
|
source,
|
|
channel,
|
|
coverage,
|
|
});
|
|
state.handlers.set(record.key, handler);
|
|
return;
|
|
}
|
|
addElectronInferenceEdge(context, {
|
|
source,
|
|
target: channel,
|
|
file: record.file,
|
|
range: record.finding.location,
|
|
coverage,
|
|
relation: record.finding.mode === "invoke" ? "invokes" : "sends",
|
|
operation: "map-ipc-transmission",
|
|
properties: {
|
|
side: record.finding.side,
|
|
operation: record.finding.operation,
|
|
channel: record.finding.channel,
|
|
channel_expression: record.finding.channel_expression,
|
|
resolution: record.finding.channel === null ? "dynamic" : "literal",
|
|
},
|
|
});
|
|
};
|
|
|
|
const addChannelNode = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
record: ElectronIpcRecord,
|
|
coverage: JavaScriptArtifactGraphCoverage,
|
|
): ApplicationNode => {
|
|
const { finding, file } = record;
|
|
const identityKey =
|
|
finding.channel === null
|
|
? `dynamic:${record.key}`
|
|
: `literal:${finding.channel}`;
|
|
return context.accumulator.addNode({
|
|
kind: "ipc-channel",
|
|
identity:
|
|
finding.resolution_basis === "wrapper-substitution"
|
|
? artifactLocalIdentity(
|
|
file.sha256,
|
|
"electron-inferred-ipc-channel",
|
|
identityKey,
|
|
)
|
|
: electronObservationIdentity(
|
|
context,
|
|
"electron-ipc-channel",
|
|
identityKey,
|
|
),
|
|
observations: [
|
|
{
|
|
label: finding.channel ?? finding.channel_expression,
|
|
properties: {
|
|
side: finding.side,
|
|
operation: finding.operation,
|
|
mode: finding.mode,
|
|
channel: finding.channel,
|
|
channel_expression: finding.channel_expression,
|
|
resolution: finding.channel === null ? "dynamic" : "literal",
|
|
...(finding.invocation_location === undefined
|
|
? {}
|
|
: { invocation_location: finding.invocation_location }),
|
|
},
|
|
evidence: (finding.resolution_basis === "wrapper-substitution"
|
|
? staticInferenceEvidence
|
|
: astObservationEvidence)({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: finding.location,
|
|
operation: "observe-ipc-channel-expression",
|
|
coverage,
|
|
limitations:
|
|
finding.resolution_basis === "wrapper-substitution"
|
|
? [
|
|
"The channel is inferred through lexical wrapper substitution; runtime registration and dispatch remain unverified.",
|
|
]
|
|
: finding.channel === null
|
|
? [
|
|
"The channel expression is dynamic; its runtime value and pairings remain unknown.",
|
|
]
|
|
: [],
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const addHandlerNode = (input: IpcHandlerInput): ApplicationNode => {
|
|
const { context, record, source, channel, coverage } = input;
|
|
const { finding, file } = record;
|
|
const handlerRange = finding.handler_location ?? finding.location;
|
|
const handler = context.accumulator.addNode({
|
|
kind: "ipc-handler",
|
|
identity: artifactLocalIdentity(
|
|
file.sha256,
|
|
"electron-ipc-handler",
|
|
`${finding.side}:${finding.operation}:${finding.channel ?? finding.channel_expression}:${electronRangeKey(handlerRange)}`,
|
|
),
|
|
observations: [
|
|
{
|
|
label: finding.channel,
|
|
properties: {
|
|
side: finding.side,
|
|
operation: finding.operation,
|
|
mode: finding.mode,
|
|
channel: finding.channel,
|
|
channel_expression: finding.channel_expression,
|
|
handler_kind: finding.handler_kind,
|
|
handler_location: finding.handler_location,
|
|
...(finding.invocation_location === undefined
|
|
? {}
|
|
: { invocation_location: finding.invocation_location }),
|
|
},
|
|
evidence: (finding.resolution_basis === "wrapper-substitution"
|
|
? staticInferenceEvidence
|
|
: astObservationEvidence)({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: handlerRange,
|
|
operation: "observe-ipc-handler",
|
|
coverage,
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
addElectronAstEdge(context, {
|
|
source,
|
|
target: handler,
|
|
file,
|
|
range: handlerRange,
|
|
coverage,
|
|
relation: "contains",
|
|
operation: "locate-ipc-handler",
|
|
properties: { side: finding.side, operation: finding.operation },
|
|
});
|
|
addElectronInferenceEdge(context, {
|
|
source: handler,
|
|
target: channel,
|
|
file,
|
|
range: finding.location,
|
|
coverage,
|
|
relation: "handles",
|
|
operation: "map-ipc-handler-registration",
|
|
properties: {
|
|
channel: finding.channel,
|
|
channel_expression: finding.channel_expression,
|
|
resolution: finding.channel === null ? "dynamic" : "literal",
|
|
...(finding.invocation_location === undefined
|
|
? {}
|
|
: { invocation_location: finding.invocation_location }),
|
|
},
|
|
});
|
|
return handler;
|
|
};
|
|
|
|
const addLiteralPairings = (state: IpcGraphState): void => {
|
|
for (const pairing of unambiguousElectronIpcPairings(state.records)) {
|
|
const source = state.sources.get(pairing.transmission.key);
|
|
const handler = state.handlers.get(pairing.handler.key);
|
|
const coverage = coverageFor(state.context, pairing.transmission.file);
|
|
if (source === undefined || handler === undefined || coverage === undefined)
|
|
continue;
|
|
addElectronInferenceEdge(state.context, {
|
|
source,
|
|
target: handler,
|
|
file: pairing.transmission.file,
|
|
range: pairing.transmission.finding.location,
|
|
coverage,
|
|
relation:
|
|
pairing.transmission.finding.mode === "invoke" ? "invokes" : "sends",
|
|
operation: "pair-ipc-literal-channel",
|
|
properties: {
|
|
channel: pairing.transmission.finding.channel,
|
|
pairing_basis: "unique-exact-literal-channel",
|
|
handler_path: pairing.handler.file.path,
|
|
handler_operation: pairing.handler.finding.operation,
|
|
},
|
|
limitations: [
|
|
"Literal channel equality does not prove that the handler is registered or reachable at runtime.",
|
|
],
|
|
});
|
|
}
|
|
};
|
|
|
|
const addValidationObservations = (state: IpcGraphState): void => {
|
|
for (const analyzed of state.context.analysis.files) {
|
|
const { file, javascript } = analyzed;
|
|
if (javascript === null) continue;
|
|
const coverage = javascriptAnalysisCoverage(javascript);
|
|
for (const validation of javascript.electron.sender_validations) {
|
|
const handlerRecord = containingHandler(state.records, file, validation);
|
|
const target =
|
|
handlerRecord === undefined
|
|
? electronFindingSourceNode(
|
|
state.context,
|
|
file,
|
|
validation.module_key,
|
|
validation.module_runtime,
|
|
)
|
|
: state.handlers.get(handlerRecord.key);
|
|
if (target !== undefined)
|
|
addValidationObservation({
|
|
context: state.context,
|
|
file,
|
|
validation,
|
|
target,
|
|
coverage,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const addValidationObservation = (input: ValidationObservationInput): void => {
|
|
const { context, file, validation, target, coverage } = input;
|
|
context.accumulator.addNode({
|
|
kind: target.kind,
|
|
identity: target.identity,
|
|
observations: [
|
|
{
|
|
label: `${validation.subject} validation candidate`,
|
|
properties: {
|
|
fact_kind: "sender-validation-candidate",
|
|
subject: validation.subject,
|
|
mechanism: validation.mechanism,
|
|
expected: validation.expected,
|
|
enforcement: validation.enforcement,
|
|
},
|
|
evidence: astObservationEvidence({
|
|
sha256: file.sha256,
|
|
path: file.path,
|
|
range: validation.location,
|
|
operation: "observe-ipc-sender-validation-candidate",
|
|
coverage,
|
|
limitations: [
|
|
"A comparison or string check is visible, but static syntax alone does not prove rejection, ordering, or complete sender validation.",
|
|
],
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const containingHandler = (
|
|
records: readonly ElectronIpcRecord[],
|
|
file: JavaScriptArtifactFile,
|
|
validation: ElectronSenderValidationFinding,
|
|
): ElectronIpcRecord | undefined =>
|
|
records
|
|
.filter(
|
|
(record) =>
|
|
record.file.path === file.path &&
|
|
record.finding.handler_location !== null &&
|
|
electronRangeContains(
|
|
record.finding.handler_location,
|
|
validation.location,
|
|
),
|
|
)
|
|
.sort(
|
|
(left, right) =>
|
|
rangeMagnitude(left.finding.handler_location) -
|
|
rangeMagnitude(right.finding.handler_location),
|
|
)[0];
|
|
|
|
const rangeMagnitude = (
|
|
range: ElectronIpcRecord["finding"]["handler_location"],
|
|
): number =>
|
|
range === null
|
|
? Number.MAX_SAFE_INTEGER
|
|
: (range.end.line - range.start.line) * 1_000_000 +
|
|
range.end.column -
|
|
range.start.column;
|
|
|
|
const coverageFor = (
|
|
context: JavaScriptArtifactGraphContext,
|
|
file: JavaScriptArtifactFile,
|
|
): JavaScriptArtifactGraphCoverage | undefined => {
|
|
const javascript = context.analysis.files.find(
|
|
({ file: candidate }) => candidate.path === file.path,
|
|
)?.javascript;
|
|
return javascript === null || javascript === undefined
|
|
? undefined
|
|
: javascriptAnalysisCoverage(javascript);
|
|
};
|