mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
fix(javascript): qualify network receiver evidence
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
semanticStaticPropertyName,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import { semanticOpaqueGlobalMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticStableInitializer,
|
||||
type JavaScriptBindingIndex,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import * as t from "@babel/types";
|
||||
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
|
||||
|
||||
export const isFetchCall = (
|
||||
node: t.Node,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
): boolean =>
|
||||
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
|
||||
isUnmodifiedGlobalReference(node.callee, bindings, "fetch");
|
||||
|
||||
/** Browser request identity requires an intact intrinsic, not a method spelling. */
|
||||
export const isBrowserNetworkRequest = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
): boolean =>
|
||||
isFetchCall(node, bindings) ||
|
||||
(t.isNewExpression(node) &&
|
||||
(isUnmodifiedGlobalReference(node.callee, bindings, "WebSocket") ||
|
||||
isUnmodifiedGlobalReference(node.callee, bindings, "EventSource"))) ||
|
||||
((t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
|
||||
isUnmodifiedGlobalReference(node.callee, bindings, [
|
||||
"navigator",
|
||||
"sendBeacon",
|
||||
]));
|
||||
|
||||
const isXhrOpenRequest = (
|
||||
node: t.Node,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
index?: Pick<JavaScriptBindingIndex, "referenceMayHaveChangedAt">,
|
||||
): boolean | undefined => {
|
||||
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
|
||||
return false;
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
if (
|
||||
(!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) ||
|
||||
semanticStaticPropertyName(callee.property, callee.computed) !== "open"
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
["XMLHttpRequest", "Object"].some((owner) =>
|
||||
["open", "__proto__"].some(
|
||||
(member) =>
|
||||
bindings.ambientWrites.covers([
|
||||
"global",
|
||||
owner,
|
||||
"prototype",
|
||||
member,
|
||||
]) ||
|
||||
bindings.ambientEscapes.covers([
|
||||
"global",
|
||||
owner,
|
||||
"prototype",
|
||||
member,
|
||||
]),
|
||||
),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
let receiver = unwrapJavaScriptExpression(callee.object).node;
|
||||
const seen = new Set<string>();
|
||||
while (t.isIdentifier(receiver)) {
|
||||
if (
|
||||
index !== undefined &&
|
||||
(index.referenceMayHaveChangedAt(node, receiver, ["open"]) ||
|
||||
index.referenceMayHaveChangedAt(node, receiver, ["__proto__"]))
|
||||
)
|
||||
return false;
|
||||
const binding = resolveSemanticBindingState(
|
||||
bindings,
|
||||
receiver,
|
||||
receiver.name,
|
||||
);
|
||||
if (binding === undefined || seen.has(binding.bindingId)) return false;
|
||||
seen.add(binding.bindingId);
|
||||
const initializer = semanticStableInitializer(binding, bindings);
|
||||
if (
|
||||
initializer === undefined ||
|
||||
initializer.projection.length !== 0 ||
|
||||
!semanticBindingAtRead(binding, receiver, bindings.parentsByNode)
|
||||
.hasDefiniteInitializer
|
||||
)
|
||||
return false;
|
||||
receiver = unwrapJavaScriptExpression(initializer.node).node;
|
||||
}
|
||||
if (
|
||||
!t.isNewExpression(receiver) ||
|
||||
!isUnmodifiedGlobalReference(receiver.callee, bindings, "XMLHttpRequest") ||
|
||||
semanticOpaqueGlobalMayHaveChanged(callee, bindings)
|
||||
)
|
||||
return false;
|
||||
return index === undefined && seen.size > 0 ? undefined : true;
|
||||
};
|
||||
|
||||
/** Resolve once through the shared mutation index when lexical proof needs it. */
|
||||
export const createXhrOpenRequestQuery =
|
||||
(index: JavaScriptBindingIndex): ((node: t.Node) => boolean) =>
|
||||
(node) => {
|
||||
const proof = isXhrOpenRequest(node, index.bindings);
|
||||
if (proof !== undefined) return proof;
|
||||
completeSemanticSteps(index.indexMemberMutationFactsSteps());
|
||||
return isXhrOpenRequest(node, index.bindings, index) === true;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createXhrOpenRequestQuery } from "./javascriptNetworkRequests.js";
|
||||
import { uniqueSortedBy } from "../canonicalOrdering.js";
|
||||
import * as t from "@babel/types";
|
||||
import { JavaScriptRouteScan } from "./javascriptRouteScan.js";
|
||||
@@ -114,6 +115,7 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
|
||||
bindings,
|
||||
);
|
||||
const findings: JavaScriptFindingContext = {
|
||||
xhrOpenRequest: createXhrOpenRequestQuery(index),
|
||||
pathIntrinsics: createStaticPathIntrinsicQuery(index),
|
||||
browserWindowReceiver: (node, site, suffix) =>
|
||||
resolveElectronBrowserWindowReceiver(node, site, suffix, findings, index),
|
||||
|
||||
@@ -357,8 +357,8 @@ it.each(expressionWrappers)(
|
||||
)
|
||||
.toEqual([
|
||||
["local-window", "candidate"],
|
||||
["/local-db", null],
|
||||
["/local-cache", null],
|
||||
["/local-db", "candidate"],
|
||||
["/local-cache", "candidate"],
|
||||
["changed-window", "candidate"],
|
||||
["uncertain-window", "candidate"],
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
isBrowserNetworkRequest,
|
||||
isFetchCall,
|
||||
} from "./javascriptNetworkRequests.js";
|
||||
import * as t from "@babel/types";
|
||||
import { isIntrinsicMapLookup } from "./javascriptSemanticMapOperations.js";
|
||||
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
|
||||
@@ -491,13 +495,6 @@ export const indexVitePreloadDependencyTable = (
|
||||
return escapedOrMutated ? dependencies.map(() => undefined) : dependencies;
|
||||
};
|
||||
|
||||
const isFetchCall = (
|
||||
node: t.Node,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
): boolean =>
|
||||
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
|
||||
isUnmodifiedGlobalReference(node.callee, bindings, "fetch");
|
||||
|
||||
const requestConstructorUrl = (
|
||||
node: t.Node,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
@@ -591,7 +588,10 @@ const inspectLiteralEndpointCall = (
|
||||
kind: "network",
|
||||
value: endpoint.value,
|
||||
mechanism: `call:${name}`,
|
||||
...(endpoint.kind === "candidate" ? { interpretation: "candidate" } : {}),
|
||||
...(endpoint.kind === "candidate" &&
|
||||
context.xhrOpenRequest?.(node) !== true
|
||||
? { interpretation: "candidate" }
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -611,7 +611,7 @@ const isKnownNetworkRequest = (
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
context: FindingContext,
|
||||
): boolean => {
|
||||
if (isFetchCall(node, bindings)) return true;
|
||||
if (isBrowserNetworkRequest(node, bindings)) return true;
|
||||
const callee = unwrapJavaScriptExpression(node.callee).node;
|
||||
if (
|
||||
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
|
||||
@@ -620,19 +620,7 @@ const isKnownNetworkRequest = (
|
||||
context.rendererLoad?.(node.callee, node) === true
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
t.isNewExpression(node) &&
|
||||
(isUnmodifiedGlobalReference(node.callee, bindings, "WebSocket") ||
|
||||
isUnmodifiedGlobalReference(node.callee, bindings, "EventSource"))
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
|
||||
isUnmodifiedGlobalReference(node.callee, bindings, [
|
||||
"navigator",
|
||||
"sendBeacon",
|
||||
])
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Finish queued targets after shared member-mutation facts are indexed. */
|
||||
|
||||
@@ -675,10 +675,10 @@ export const endpointArgument = (
|
||||
callee: t.Node,
|
||||
receiverFact?: JavaScriptOpenReceiverFact,
|
||||
): JavaScriptEndpointArgument | undefined => {
|
||||
const literal = (
|
||||
const candidate = (
|
||||
value: string | undefined,
|
||||
): JavaScriptEndpointArgument | undefined =>
|
||||
value === undefined ? undefined : { kind: "endpoint", value };
|
||||
value === undefined ? undefined : { kind: "candidate", value };
|
||||
if (receiverFact === "window") return undefined;
|
||||
if (
|
||||
name === "fetch" ||
|
||||
@@ -687,17 +687,11 @@ export const endpointArgument = (
|
||||
name === "EventSource" ||
|
||||
name.endsWith(".EventSource")
|
||||
)
|
||||
return literal(stringValue(args[0]));
|
||||
return candidate(stringValue(args[0]));
|
||||
if (name.endsWith(".open")) {
|
||||
const value = xhrOpenUrl(name, args[0], args[1], receiverFact);
|
||||
if (value === undefined) return undefined;
|
||||
return {
|
||||
kind:
|
||||
stringValue(args[0]) !== undefined || isRequestUrlReference(value)
|
||||
? "endpoint"
|
||||
: "candidate",
|
||||
value,
|
||||
};
|
||||
return { kind: "candidate", value };
|
||||
}
|
||||
const method = ["get", "post", "put", "patch", "delete", "request"].find(
|
||||
(candidate) => name === candidate || name.endsWith(`.${candidate}`),
|
||||
@@ -717,17 +711,10 @@ export const endpointArgument = (
|
||||
receiverFact === "extension-state")))
|
||||
)
|
||||
return undefined;
|
||||
return {
|
||||
kind:
|
||||
method !== "get" && method !== "delete" && isRequestUrlReference(value)
|
||||
? "endpoint"
|
||||
: "candidate",
|
||||
value,
|
||||
};
|
||||
return { kind: "candidate", value };
|
||||
}
|
||||
// Beacon URLs are endpoint observations, retaining the upstream API support.
|
||||
if (name === "sendBeacon" || name.endsWith(".sendBeacon"))
|
||||
return literal(stringValue(args[0]));
|
||||
return candidate(stringValue(args[0]));
|
||||
if (name === "loadURL" || name.endsWith(".loadURL")) {
|
||||
const value = stringValue(args[0]);
|
||||
return value === undefined ? undefined : { kind: "candidate", value };
|
||||
|
||||
@@ -110,6 +110,7 @@ export interface JavaScriptFindingContext {
|
||||
) =>
|
||||
| { readonly owner: t.NewExpression; readonly path: readonly string[] }
|
||||
| undefined;
|
||||
readonly xhrOpenRequest?: (node: t.Node) => boolean;
|
||||
readonly rendererLoad?: (callee: t.Node, site: t.Node) => boolean;
|
||||
readonly rpcScan: JavaScriptRpcScan;
|
||||
/** Shared bounded argv/nullish argument shapes for optional-slot signatures. */
|
||||
|
||||
@@ -585,12 +585,17 @@ describe("web bundle source-map coverage", () => {
|
||||
});
|
||||
|
||||
describe("web bundle static-analysis parity", () => {
|
||||
it("recognizes endpoints through nested dynamic receivers like static analysis", () => {
|
||||
it("qualifies network spellings through nested dynamic receivers", () => {
|
||||
const result = analyzeWebBundleFixture(
|
||||
inspection('window[recv].fetch("/nested-fetch");'),
|
||||
);
|
||||
expect(result.observations.endpoints).toContainEqual(
|
||||
expect.objectContaining({ value: "/nested-fetch" }),
|
||||
expect(result.observations.endpoints).toEqual([]);
|
||||
expect(result.inferences).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "endpoint_candidate",
|
||||
value: "/nested-fetch",
|
||||
confidence: "low",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -628,11 +633,17 @@ describe("web bundle static-analysis parity", () => {
|
||||
);
|
||||
expect(
|
||||
result.observations.endpoints.map(({ value }) => value).sort(),
|
||||
).toEqual(["/shadowed-parent-xhr"]);
|
||||
expect(
|
||||
result.inferences
|
||||
.filter(({ kind }) => kind === "endpoint_candidate")
|
||||
.map(({ value }) => value)
|
||||
.sort(),
|
||||
).toEqual([
|
||||
"/dav/",
|
||||
"/dynamic-method",
|
||||
"/shadowed-parent-xhr",
|
||||
"/xhr-get",
|
||||
"api/relative",
|
||||
"https://xhr.example.test/submit",
|
||||
]);
|
||||
expect(result.inferences).toContainEqual(
|
||||
@@ -654,9 +665,9 @@ describe("web bundle API classification parity", () => {
|
||||
indexedDB.open("GET", "/storage-named-xhr");
|
||||
`;
|
||||
expect(
|
||||
analyzeWebBundleFixture(inspection(source)).observations.endpoints.map(
|
||||
({ value }) => value,
|
||||
),
|
||||
analyzeWebBundleFixture(inspection(source))
|
||||
.inferences.filter(({ kind }) => kind === "endpoint_candidate")
|
||||
.map(({ value }) => value),
|
||||
).toEqual(["/storage-named-xhr"]);
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(analysis.endpoints.map(({ value }) => value)).toEqual([
|
||||
@@ -686,7 +697,7 @@ describe("web bundle API classification parity", () => {
|
||||
);
|
||||
expect(
|
||||
result.observations.endpoints.map(({ value }) => value).sort(),
|
||||
).toEqual(["/api", "/dav"]);
|
||||
).toEqual(["/api"]);
|
||||
});
|
||||
|
||||
it("classifies optional and one-hop aliased open calls", () => {
|
||||
@@ -733,7 +744,7 @@ describe("web bundle API classification parity", () => {
|
||||
);
|
||||
expect(
|
||||
result.observations.endpoints.map(({ value }) => value).sort(),
|
||||
).toEqual(["/search", "/v1/headers"]);
|
||||
).toEqual([]);
|
||||
expect(
|
||||
result.inferences
|
||||
.filter(({ kind }) => kind === "endpoint_candidate")
|
||||
@@ -741,7 +752,9 @@ describe("web bundle API classification parity", () => {
|
||||
.sort(),
|
||||
).toEqual([
|
||||
"../search",
|
||||
"/search",
|
||||
"/users",
|
||||
"/v1/headers",
|
||||
"cache-key",
|
||||
"https://api.example.test/headers",
|
||||
"https://api.example.test/users",
|
||||
@@ -839,13 +852,12 @@ describe("web bundle explicit request reference forms", () => {
|
||||
caches.open(name, "#cache-name");
|
||||
fs.open(path, "r");
|
||||
`;
|
||||
const expected = [
|
||||
const expected: string[] = [];
|
||||
const expectedCandidates = [
|
||||
"?view=1",
|
||||
"#view=1",
|
||||
"#literal=1",
|
||||
"https:api.example.test/xhr",
|
||||
].sort();
|
||||
const expectedCandidates = [
|
||||
...references,
|
||||
...ambiguous,
|
||||
"POST",
|
||||
@@ -894,7 +906,7 @@ describe("web bundle endpoint candidate classification", () => {
|
||||
const result = analyzeWebBundleFixture(inspection(source));
|
||||
expect(
|
||||
result.observations.endpoints.map(({ value }) => value).sort(),
|
||||
).toEqual(["login", "ping"]);
|
||||
).toEqual(["login"]);
|
||||
const candidates = result.inferences.filter(
|
||||
({ kind }) => kind === "endpoint_candidate",
|
||||
);
|
||||
@@ -905,6 +917,7 @@ describe("web bundle endpoint candidate classification", () => {
|
||||
"X",
|
||||
"login",
|
||||
"passwordControl",
|
||||
"ping",
|
||||
"token",
|
||||
"users",
|
||||
]);
|
||||
@@ -1857,7 +1870,6 @@ it("retains direct and literal computed call names", () => {
|
||||
expect(result.observations.endpoints.map(({ value }) => value)).toEqual([
|
||||
"/direct",
|
||||
"/literal",
|
||||
"/nested-receiver",
|
||||
]);
|
||||
expect(
|
||||
result.observations.chunks.edges.map(({ specifier }) => specifier),
|
||||
@@ -2113,3 +2125,61 @@ it("retains captured Next script identity while deriving decoded route patterns"
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("qualifies local browser API spellings while preserving intrinsic requests", () => {
|
||||
const result = analyzeWebBundleFixture(
|
||||
inspection(`
|
||||
const local = { fetch(x) { return x; }, sendBeacon(x) { return x; }, EventSource(x) { return x; } };
|
||||
local.fetch('/local-fetch'); local.sendBeacon('/local-beacon'); local.EventSource('/local-events');
|
||||
const other = { open(a,b) { return b; }, post(x) { return x; }, put(x) { return x; }, patch(x) { return x; }, request(x) { return x; } };
|
||||
other.open('GET', '/local-open'); other.post('/local-post'); other.put('/local-put'); other.patch('/local-patch'); other.request('/local-request');
|
||||
fetch('/intrinsic-fetch'); navigator.sendBeacon('/intrinsic-beacon'); new EventSource('/intrinsic-events');
|
||||
`),
|
||||
);
|
||||
expect(
|
||||
result.observations.endpoints.map(({ value }) => value).sort(),
|
||||
).toEqual(["/intrinsic-beacon", "/intrinsic-events", "/intrinsic-fetch"]);
|
||||
expect(
|
||||
result.inferences
|
||||
.filter(({ kind }) => kind === "endpoint_candidate")
|
||||
.map(({ value }) => value)
|
||||
.sort(),
|
||||
).toEqual([
|
||||
"/local-beacon",
|
||||
"/local-events",
|
||||
"/local-fetch",
|
||||
"/local-open",
|
||||
"/local-patch",
|
||||
"/local-post",
|
||||
"/local-put",
|
||||
"/local-request",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["", true],
|
||||
["xhr.open = replacement;", false],
|
||||
["XMLHttpRequest.prototype.open = replacement;", false],
|
||||
["const alias = xhr; alias.open = replacement;", false],
|
||||
["mutate(xhr);", false],
|
||||
])(
|
||||
"preserves XHR receiver proof across visible mutation: %s",
|
||||
(mutation, observed) => {
|
||||
const source = `const xhr = new XMLHttpRequest(); ${mutation} xhr.open('GET', '/xhr');`;
|
||||
const result = analyzeWebBundleFixture(inspection(source));
|
||||
expect(result.observations.endpoints.map(({ value }) => value)).toEqual(
|
||||
observed ? ["/xhr"] : [],
|
||||
);
|
||||
expect(
|
||||
result.inferences
|
||||
.filter(({ kind }) => kind === "endpoint_candidate")
|
||||
.map(({ value }) => value),
|
||||
).toEqual(observed ? [] : ["/xhr"]);
|
||||
expect(analyzeJavaScriptStaticSource(source).endpoints).toContainEqual(
|
||||
expect.objectContaining({
|
||||
value: "/xhr",
|
||||
...(observed ? {} : { interpretation: "candidate" }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
isBrowserNetworkRequest,
|
||||
createXhrOpenRequestQuery,
|
||||
} from "./javascript/javascriptNetworkRequests.js";
|
||||
import type { JavaScriptSemanticAnalysisState } from "./javascript/javascriptSemanticState.js";
|
||||
import type {
|
||||
AnalysisAccumulator,
|
||||
IncludedScript,
|
||||
@@ -89,6 +94,7 @@ export const analyzeScript = (
|
||||
}
|
||||
return isIntrinsicMapLookup(node, index.bindings, index) === true;
|
||||
};
|
||||
const isXhrRequest = createXhrOpenRequestQuery(index);
|
||||
const rpc = createJavaScriptRpcScan(
|
||||
script.source.artifact.text,
|
||||
receivers.rpcClientRoots,
|
||||
@@ -98,7 +104,15 @@ export const analyzeScript = (
|
||||
enter: (node, parent) => {
|
||||
accumulator.visitedNodes += 1;
|
||||
routeScan.inspect(node, parent);
|
||||
inspectNode(script, node, accumulator, openReceiverFacts, isMapLookup);
|
||||
inspectNode(
|
||||
script,
|
||||
node,
|
||||
accumulator,
|
||||
openReceiverFacts,
|
||||
isMapLookup,
|
||||
index.bindings,
|
||||
isXhrRequest,
|
||||
);
|
||||
const endpoint = rpc.inspect(node);
|
||||
if (endpoint !== undefined)
|
||||
addFinding({
|
||||
@@ -134,6 +148,8 @@ const inspectNode = (
|
||||
isMapLookup: (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
) => boolean,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
isXhrRequest: (node: t.Node) => boolean,
|
||||
): void => {
|
||||
const moduleSource = semanticStaticModuleSource(node);
|
||||
if (moduleSource !== null)
|
||||
@@ -160,7 +176,15 @@ const inspectNode = (
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
)
|
||||
inspectCall(script, node, accumulator, openReceiverFacts, isMapLookup);
|
||||
inspectCall(
|
||||
script,
|
||||
node,
|
||||
accumulator,
|
||||
openReceiverFacts,
|
||||
isMapLookup,
|
||||
bindings,
|
||||
isXhrRequest,
|
||||
);
|
||||
};
|
||||
|
||||
const inspectCall = (
|
||||
@@ -171,6 +195,8 @@ const inspectCall = (
|
||||
isMapLookup: (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
) => boolean,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
isXhrRequest: (node: t.Node) => boolean,
|
||||
): void => {
|
||||
const name = calleeName(node.callee);
|
||||
const first = stringValue(node.arguments[0]);
|
||||
@@ -206,8 +232,13 @@ const inspectCall = (
|
||||
node.callee,
|
||||
openReceiverFacts.get(node.start ?? -1),
|
||||
);
|
||||
const mapLookup = isMapLookup(node);
|
||||
const knownRequest =
|
||||
isBrowserNetworkRequest(node, bindings) || isXhrRequest(node);
|
||||
const networkEndpoint =
|
||||
endpoint !== undefined && !isMapLookup(node) ? endpoint : undefined;
|
||||
endpoint !== undefined && !mapLookup
|
||||
? { ...endpoint, kind: knownRequest ? "endpoint" : endpoint.kind }
|
||||
: undefined;
|
||||
if (networkEndpoint?.kind === "candidate")
|
||||
addEndpointCandidate(
|
||||
script,
|
||||
|
||||
@@ -73,3 +73,72 @@ cliTest(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"qualifies local browser request spellings through CLI Evidence",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-network-spellings-");
|
||||
await writeFixtureFiles(root, {
|
||||
"index.js": `
|
||||
const local = { fetch(x) { return x; }, sendBeacon(x) { return x; }, EventSource(x) { return x; } };
|
||||
local.fetch('/local-fetch'); local.sendBeacon('/local-beacon'); local.EventSource('/local-events');
|
||||
const other = { open(a,b) { return b; }, post(x) { return x; }, put(x) { return x; }, patch(x) { return x; }, request(x) { return x; } };
|
||||
other.open('GET', '/local-open'); other.post('/local-post'); other.put('/local-put'); other.patch('/local-patch'); other.request('/local-request');
|
||||
fetch('/intrinsic-fetch'); navigator.sendBeacon('/intrinsic-beacon'); new EventSource('/intrinsic-events');
|
||||
`,
|
||||
});
|
||||
const result = await analyzeJavaScriptDirectory("CLI", cli, root);
|
||||
const endpoints = result.graph.nodes.filter(
|
||||
({ kind }) => kind === "endpoint",
|
||||
);
|
||||
expect(endpoints).toHaveLength(11);
|
||||
for (const endpoint of endpoints) {
|
||||
const observation = endpoint.observations[0]!;
|
||||
const local = String(observation.properties.value).startsWith("/local-");
|
||||
expect(observation.evidence).toMatchObject(
|
||||
local
|
||||
? { state: "inferred", confidence: "low" }
|
||||
: { state: "observed", confidence: "exact" },
|
||||
);
|
||||
expect(observation.properties.interpretation).toBe(
|
||||
local ? "candidate" : undefined,
|
||||
);
|
||||
if (local)
|
||||
expect(
|
||||
result.graph.edges.find(
|
||||
({ target_node_id }) => target_node_id === endpoint.node_id,
|
||||
)?.evidence,
|
||||
).toMatchObject({ state: "inferred", confidence: "low" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves binding-backed XHR identity and qualifies replaced open methods",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-xhr-identity-");
|
||||
await writeFixtureFiles(root, {
|
||||
"index.js": `
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/intact-xhr');
|
||||
const replaced = new XMLHttpRequest();
|
||||
replaced.open = (method, value) => value;
|
||||
replaced.open('GET', '/replaced-xhr');
|
||||
{ const XMLHttpRequest = class {}; const local = new XMLHttpRequest(); local.open('GET', '/local-xhr'); }
|
||||
`,
|
||||
});
|
||||
const result = await analyzeJavaScriptDirectory("CLI", cli, root);
|
||||
const endpoints = result.graph.nodes.filter(
|
||||
({ kind }) => kind === "endpoint",
|
||||
);
|
||||
expect(endpoints).toHaveLength(3);
|
||||
for (const endpoint of endpoints) {
|
||||
const observation = endpoint.observations[0]!;
|
||||
expect(observation.evidence).toMatchObject(
|
||||
observation.properties.value === "/intact-xhr"
|
||||
? { state: "observed", confidence: "exact" }
|
||||
: { state: "inferred", confidence: "low" },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user