mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
Electron recognition matched callee spellings, so a bundler rename such as
esbuild's `import { ipcMain as ipcMain2 } from "electron"`, or a destructured
`const { contextBridge: bridge } = require("electron")`, hid every IPC
handler, BrowserWindow, contextBridge and utility process behind it.
Resolve each call and construction root lexically to its Electron import or
require origin, and classify the callee under that export's name. Shadowed
locals keep their own meaning.
On Hermes Desktop v0.21.6, whose main process is one esbuild ESM bundle,
recognized main IPC handlers rise from 1 to 204, BrowserWindow constructions
from 0 to 11, and paired renderer transmissions from 1 to 201.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPY2GmufMNGQKoEgRUx1pd
256 lines
8.5 KiB
TypeScript
256 lines
8.5 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import type {
|
|
ElectronBrowserWindowFinding,
|
|
ElectronBrowserWindowPreload,
|
|
ElectronContextBridgeApiKey,
|
|
ElectronContextBridgeFinding,
|
|
ElectronStaticValue,
|
|
ElectronUtilityProcessFinding,
|
|
ElectronWebPreference,
|
|
} from "./electronStaticAnalysisTypes.js";
|
|
import {
|
|
boundedExpression,
|
|
collectContextBridgeMembers,
|
|
electronCalleeName,
|
|
electronStaticValue,
|
|
objectProperty,
|
|
} from "./electronStaticAnalysisValues.js";
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js";
|
|
import {
|
|
argumentNode,
|
|
range,
|
|
staticPath,
|
|
staticPathResolutionContext,
|
|
} from "./javascriptStaticAnalysisHelpers.js";
|
|
import { semanticStaticPropertyName } from "./javascriptAstValues.js";
|
|
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
|
|
|
|
/** Inspect BrowserWindow, contextBridge, and utility-process syntax. */
|
|
export const inspectElectronBrowserNode = (
|
|
node: t.Node,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
if (t.isNewExpression(node)) inspectBrowserWindow(node, context);
|
|
if (!t.isCallExpression(node)) return;
|
|
inspectContextBridge(node, context);
|
|
inspectUtilityProcess(node, context);
|
|
};
|
|
|
|
const inspectBrowserWindow = (
|
|
node: t.NewExpression,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
const name = electronCalleeName(node.callee, context);
|
|
if (name !== "BrowserWindow" && !name.endsWith(".BrowserWindow")) return;
|
|
const options = argumentNode(node.arguments[0]);
|
|
const collected = collectWindowOptions(context.source, options);
|
|
context.accumulator.unknownFindings += collected.unknown;
|
|
const finding: ElectronBrowserWindowFinding = {
|
|
options_status:
|
|
options === undefined
|
|
? "missing"
|
|
: t.isObjectExpression(options)
|
|
? "object-literal"
|
|
: "dynamic",
|
|
web_preferences_status: collected.status,
|
|
web_preferences: collected.preferences,
|
|
...collected.preload,
|
|
module_key: null,
|
|
location: range(node),
|
|
};
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.browserWindows,
|
|
key: `electron-window\0${name}`,
|
|
node,
|
|
value: finding,
|
|
});
|
|
};
|
|
|
|
const collectWindowOptions = (
|
|
source: string,
|
|
options: t.Node | undefined,
|
|
): {
|
|
readonly status: ElectronBrowserWindowFinding["web_preferences_status"];
|
|
readonly preferences: readonly ElectronWebPreference[];
|
|
readonly preload: ElectronBrowserWindowPreload;
|
|
readonly unknown: number;
|
|
} => {
|
|
const lookup = objectProperty(options, "webPreferences");
|
|
if (
|
|
lookup.status !== "explicit" ||
|
|
!t.isObjectExpression(lookup.property.value)
|
|
)
|
|
return {
|
|
status: lookup.status === "missing" ? "missing" : "dynamic",
|
|
preferences: [],
|
|
preload: { preload_path: null, preload_resolution_context: null },
|
|
unknown: lookup.status === "missing" ? 0 : 1,
|
|
};
|
|
return collectWebPreferences(source, lookup.property.value);
|
|
};
|
|
|
|
const collectWebPreferences = (
|
|
source: string,
|
|
object: t.ObjectExpression,
|
|
): {
|
|
readonly status: "object-literal";
|
|
readonly preferences: readonly ElectronWebPreference[];
|
|
readonly preload: ElectronBrowserWindowPreload;
|
|
readonly unknown: number;
|
|
} => {
|
|
const preferences: ElectronWebPreference[] = [];
|
|
let unknown = 0;
|
|
const effectivePreload = objectProperty(object, "preload");
|
|
const preloadPath =
|
|
effectivePreload.status === "explicit"
|
|
? staticPath(effectivePreload.property.value)
|
|
: undefined;
|
|
const preload: ElectronBrowserWindowPreload =
|
|
effectivePreload.status === "explicit" && preloadPath !== undefined
|
|
? {
|
|
preload_path: preloadPath,
|
|
preload_resolution_context: staticPathResolutionContext(
|
|
effectivePreload.property.value,
|
|
),
|
|
}
|
|
: { preload_path: null, preload_resolution_context: null };
|
|
for (const property of object.properties) {
|
|
if (t.isSpreadElement(property)) {
|
|
unknown += 1;
|
|
preferences.push({
|
|
name: `[spread@${String(property.start ?? -1)}]`,
|
|
value: electronStaticValue(source, property.argument),
|
|
});
|
|
continue;
|
|
}
|
|
const name = semanticStaticPropertyName(property.key, property.computed);
|
|
if (name === "" || property.computed) {
|
|
unknown += 1;
|
|
preferences.push({
|
|
name: `[dynamic@${String(property.start ?? -1)}]`,
|
|
value: electronStaticValue(source, property),
|
|
});
|
|
continue;
|
|
}
|
|
if (t.isObjectMethod(property)) {
|
|
unknown += 1;
|
|
preferences.push({
|
|
name,
|
|
value: electronStaticValue(source, property),
|
|
});
|
|
continue;
|
|
}
|
|
const path = name === "preload" ? staticPath(property.value) : undefined;
|
|
const value: ElectronStaticValue =
|
|
path === undefined
|
|
? electronStaticValue(source, property.value)
|
|
: { status: "literal", value: path, expression: null };
|
|
if (value.status === "dynamic") unknown += 1;
|
|
preferences.push({ name, value });
|
|
}
|
|
preferences.sort((left, right) =>
|
|
compareUnicodeCodePoints(left.name, right.name),
|
|
);
|
|
return {
|
|
status: "object-literal",
|
|
preferences,
|
|
preload,
|
|
unknown,
|
|
};
|
|
};
|
|
|
|
const inspectContextBridge = (
|
|
node: t.CallExpression,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
const name = electronCalleeName(node.callee, context);
|
|
const main =
|
|
name === "contextBridge.exposeInMainWorld" ||
|
|
name.endsWith(".contextBridge.exposeInMainWorld");
|
|
const isolated =
|
|
name === "contextBridge.exposeInIsolatedWorld" ||
|
|
name.endsWith(".contextBridge.exposeInIsolatedWorld");
|
|
if (!main && !isolated) return;
|
|
const keyNode = argumentNode(node.arguments[isolated ? 1 : 0]);
|
|
const apiNode = argumentNode(node.arguments[isolated ? 2 : 1]);
|
|
const key = electronStaticValue(context.source, keyNode);
|
|
const api = collectContextBridgeMembers(apiNode);
|
|
const worldId = isolated
|
|
? electronStaticValue(context.source, argumentNode(node.arguments[0]))
|
|
: null;
|
|
const unknown =
|
|
(key.status === "dynamic" ? 1 : 0) +
|
|
(worldId?.status === "dynamic" ? 1 : 0) +
|
|
api.unknown;
|
|
context.accumulator.unknownFindings += unknown;
|
|
const apiKey: ElectronContextBridgeApiKey =
|
|
key.status === "dynamic"
|
|
? { api_key: null, api_key_expression: key.expression }
|
|
: typeof key.value === "string"
|
|
? { api_key: key.value, api_key_expression: null }
|
|
: { api_key: null, api_key_expression: null };
|
|
const finding: ElectronContextBridgeFinding = {
|
|
...apiKey,
|
|
world: isolated ? "isolated" : "main",
|
|
world_id: worldId,
|
|
api_status: api.status,
|
|
members: api.members,
|
|
unknown_members: api.unknown,
|
|
module_key: null,
|
|
location: range(node),
|
|
};
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.contextBridgeApis,
|
|
key: `context-bridge\0${isolated ? "isolated" : "main"}\0${finding.api_key ?? finding.api_key_expression ?? "[missing]"}`,
|
|
node,
|
|
value: finding,
|
|
});
|
|
};
|
|
|
|
const inspectUtilityProcess = (
|
|
node: t.CallExpression,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
const name = electronCalleeName(node.callee, context);
|
|
if (name !== "utilityProcess.fork" && !name.endsWith(".utilityProcess.fork"))
|
|
return;
|
|
const moduleNode = argumentNode(node.arguments[0]);
|
|
const modulePath =
|
|
moduleNode === undefined ? undefined : staticPath(moduleNode);
|
|
const options = argumentNode(node.arguments[2]);
|
|
const serviceNameProperty = objectProperty(options, "serviceName");
|
|
const serviceName =
|
|
serviceNameProperty.status === "explicit"
|
|
? staticPath(serviceNameProperty.property.value)
|
|
: undefined;
|
|
if (serviceNameProperty.status !== "missing" && serviceName === undefined)
|
|
context.accumulator.unknownFindings += 1;
|
|
if (modulePath === undefined) context.accumulator.unknownFindings += 1;
|
|
const moduleReference =
|
|
modulePath === undefined || moduleNode === undefined
|
|
? {
|
|
module_path: null,
|
|
module_resolution_context: null,
|
|
module_expression: boundedExpression(context.source, moduleNode),
|
|
}
|
|
: {
|
|
module_path: modulePath,
|
|
module_resolution_context: staticPathResolutionContext(moduleNode),
|
|
module_expression: null,
|
|
};
|
|
const finding: ElectronUtilityProcessFinding = {
|
|
...moduleReference,
|
|
service_name: serviceName ?? null,
|
|
module_key: null,
|
|
location: range(node),
|
|
};
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.utilityProcesses,
|
|
key: `electron-utility\0${finding.module_path ?? finding.module_expression}`,
|
|
node,
|
|
value: finding,
|
|
});
|
|
};
|