fix(javascript): qualify renderer endpoint observations

This commit is contained in:
morluto committed 2026-10-27 09:15:24 +08:00
1 parent 9cad06db10
commit 0e1500dc8d
4 files changed
+99 -8

No files matched your search

@@ -518,7 +518,7 @@ const inspectEndpointCall = (
context: FindingContext,
bindings: JavaScriptSemanticAnalysisState,
): void => {
if (isKnownNetworkRequest(node, bindings)) {
if (isKnownNetworkRequest(node, bindings, context)) {
const target = argumentNode(node.arguments[0]);
if (target === undefined) return;
const literal =
@@ -609,8 +609,17 @@ const isNetworkRequestSpelling = (name: string): boolean =>
const isKnownNetworkRequest = (
node: JavaScriptCallLike,
bindings: JavaScriptSemanticAnalysisState,
context: FindingContext,
): boolean => {
if (isFetchCall(node, bindings)) return true;
const callee = unwrapJavaScriptExpression(node.callee).node;
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
semanticStaticPropertyKey(callee.property, callee.computed) === "loadURL" &&
context.rendererLoad?.(node.callee, node) === true
)
return true;
if (
t.isNewExpression(node) &&
(isUnmodifiedGlobalReference(node.callee, bindings, "WebSocket") ||
@@ -633,7 +642,7 @@ export const finishPartialNetworkEndpoints = (
index: JavaScriptBindingIndex,
): void => {
for (const { node, name } of context.accumulator.pendingNetworkCalls) {
if (!isKnownNetworkRequest(node, bindings)) {
if (!isKnownNetworkRequest(node, bindings, context)) {
if (!isIntrinsicMapLookup(node, bindings, index))
inspectLiteralEndpointCall(node, name, context);
continue;
@@ -657,7 +657,7 @@ const isKeyedCollectionReceiver = (callee: t.Node): boolean => {
);
};
/** A literal endpoint shape or an ambiguous argument of an HTTP-method-named call. */
/** A literal endpoint shape or an argument whose callee spelling is unproven. */
export interface JavaScriptEndpointArgument {
readonly kind: "endpoint" | "candidate";
readonly value: string;
@@ -728,8 +728,10 @@ export const endpointArgument = (
// Beacon URLs are endpoint observations, retaining the upstream API support.
if (name === "sendBeacon" || name.endsWith(".sendBeacon"))
return literal(stringValue(args[0]));
if (name === "loadURL" || name.endsWith(".loadURL"))
return literal(stringValue(args[0]));
if (name === "loadURL" || name.endsWith(".loadURL")) {
const value = stringValue(args[0]);
return value === undefined ? undefined : { kind: "candidate", value };
}
return undefined;
};
+8 -3
View File
@@ -1087,12 +1087,17 @@ describe("web bundle configuration and navigation parity", () => {
});
});
it("recognizes loadURL endpoints like static analysis", () => {
it("keeps loadURL spelling without Electron receiver proof as a candidate", () => {
const result = analyzeWebBundleFixture(
inspection('window.win.loadURL("https://embedded.test/app");'),
);
expect(result.observations.endpoints).toContainEqual(
expect.objectContaining({ value: "https://embedded.test/app" }),
expect(result.observations.endpoints).toEqual([]);
expect(result.inferences).toContainEqual(
expect.objectContaining({
kind: "endpoint_candidate",
value: "https://embedded.test/app",
confidence: "low",
}),
);
});
});
@@ -0,0 +1,75 @@
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
cliTest(
"preserves renderer endpoint certainty through CLI graph delivery",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-renderer-endpoints-");
await writeFixtureFiles(root, {
"main.cjs": `
const { BrowserWindow } = require("electron");
const replaced = new BrowserWindow();
replaced.loadURL = () => {};
replaced.loadURL("https://replaced.test");
const local = { loadURL(value) { return value; } };
local.loadURL("https://local.test");
const intact = new BrowserWindow();
intact.loadURL("https://intact.test");
new BrowserWindow().loadURL(runtimeUrl);
new BrowserWindow().loadFile("page.html");
`,
"page.html": "<p>Local renderer</p>",
});
const result = await analyzeJavaScriptDirectory("CLI", cli, root);
const endpoints = result.graph.nodes.filter(
({ kind }) => kind === "endpoint",
);
expect(endpoints).toHaveLength(4);
for (const value of ["https://replaced.test", "https://local.test"]) {
const endpoint = endpoints.find(({ observations }) =>
observations.some(({ properties }) => properties.value === value),
);
expect(endpoint?.observations[0]).toMatchObject({
properties: { value, interpretation: "candidate" },
evidence: { state: "inferred", confidence: "low" },
});
expect(endpoint?.observations[0]?.evidence.limitations).toEqual(
expect.arrayContaining([
expect.stringContaining("receiver does not establish a network call"),
]),
);
expect(
result.graph.edges.find(
({ target_node_id }) => target_node_id === endpoint?.node_id,
),
).toMatchObject({
relation: "calls",
properties: { interpretation: "candidate" },
evidence: { state: "inferred", confidence: "low" },
});
}
const intact = endpoints.find(({ observations }) =>
observations.some(
({ properties }) => properties.value === "https://intact.test",
),
);
expect(intact?.observations[0]).toMatchObject({
properties: { value: "https://intact.test" },
evidence: { state: "observed", confidence: "exact" },
});
expect(intact?.observations[0]?.properties).not.toHaveProperty(
"interpretation",
);
const dynamic = endpoints.find(({ observations }) =>
observations.some(({ properties }) => properties.value === "runtimeUrl"),
);
expect(dynamic?.observations[0]).toMatchObject({
properties: { value: "runtimeUrl", resolution: "partial" },
evidence: { state: "observed" },
});
},
);