import * as t from "@babel/types"; import { addBoundedFinding, addLocatedFinding, moduleAtOffset, } from "./javascriptStaticAnalysisFindings.js"; import { STATIC_ROUTE_CALL_NAMES, argumentValue, calleeName, endpointArgument as endpointArgumentHelper, prefixedArgument, propertyName, range, rangeForOffsets, sanitizeCandidate, sourceSlice, staticPath, staticPathResolutionContext, storageKind, stringValue, } from "./javascriptStaticAnalysisHelpers.js"; import type { JavaScriptAnalysisAccumulator as AnalysisAccumulator, JavaScriptEndpointInput as EndpointInput, JavaScriptFindingContext as FindingContext, JavaScriptReferenceInput as ReferenceInput, } from "./javascriptStaticAnalysisState.js"; /** Inspect one call or new expression for references, endpoints, storage, and roles. */ export const inspectCall = ( node: t.CallExpression | t.NewExpression, context: FindingContext, ): void => { const { accumulator } = context; const name = calleeName(node.callee); const first = stringValue(node.arguments[0]); const module = moduleAtOffset(node.start, context.accumulator.modules); const moduleRequireName = module?.requireName ?? null; const requireCall = requireCallSpecifier(node, name, moduleRequireName); if (requireCall !== null) addReference(context, { node, kind: "require", specifier: requireCall.specifier, }); const chunkLoad = chunkLoadSpecifier(node, name, moduleRequireName); if (chunkLoad !== null) addReference(context, { node, kind: "dynamic-import", specifier: chunkLoad.specifier, }); if (name === "Worker" || name.endsWith(".Worker")) addReference(context, { node, kind: "worker", specifier: first }); if (name.endsWith("serviceWorker.register")) addReference(context, { node, kind: "service-worker", specifier: first }); inspectEndpointCall(node, name, first, context); inspectStorageCall(node, name, first, context); if (name.endsWith("loadFile") && first !== undefined) addLocatedFinding(context, { collection: accumulator.roles, key: `role\0renderer\0${first}`, node, value: { role: "renderer", path: first, resolution_context: "filesystem-expression", mechanism: `call:${name}`, module_key: null, location: range(node), }, }); if (name.endsWith("loadURL") && first !== undefined) addLocatedFinding(context, { collection: accumulator.roles, key: `role\0renderer\0${first}`, node, value: { role: "renderer", path: first, resolution_context: "module-specifier", mechanism: `call:${name}`, module_key: null, location: range(node), }, }); }; const requireCallSpecifier = ( node: t.CallExpression | t.NewExpression, name: string, moduleRequireName: string | null, ): { readonly specifier: string | undefined } | null => { if (name === "require" || name.endsWith(".require")) return { specifier: argumentValue(node.arguments[0]) }; if (name === "__webpack_require__" || name === moduleRequireName) return { specifier: argumentValue(node.arguments[0]) }; if ( name === "__webpack_require__.bind" || (moduleRequireName !== null && name === `${moduleRequireName}.bind`) ) return { specifier: argumentValue(node.arguments[1]) }; return null; }; const chunkLoadSpecifier = ( node: t.CallExpression | t.NewExpression, name: string, moduleRequireName: string | null, ): { readonly specifier: string | undefined } | null => { if ( name !== "__webpack_require__.e" && (moduleRequireName === null || name !== `${moduleRequireName}.e`) ) return null; return { specifier: prefixedArgument("chunk:", node.arguments[0]) }; }; const inspectEndpointCall = ( node: t.CallExpression | t.NewExpression, name: string, first: string | undefined, context: FindingContext, ): void => { const route = STATIC_ROUTE_CALL_NAMES.some( (candidate) => name === candidate || name.endsWith(`.${candidate}`), ); if (route && first !== undefined) addEndpoint(context, { node, kind: "route", value: first, mechanism: `call:${name}`, }); const endpoint = endpointArgumentHelper(name, node.arguments); if (endpoint !== undefined) addEndpoint(context, { node, kind: "network", value: endpoint, mechanism: `call:${name}`, }); }; export const inspectRouteProperty = ( node: t.ObjectProperty, context: FindingContext, ): void => { const key = propertyName(node.key); if ((key === "path" || key === "route") && t.isStringLiteral(node.value)) addEndpoint(context, { node, kind: "route", value: node.value.value, mechanism: `property:${key}`, }); }; export const inspectRoleProperty = ( node: t.ObjectProperty, context: FindingContext, ): void => { if (propertyName(node.key) !== "preload") return; const path = staticPath(node.value); if (path === undefined) return; addLocatedFinding(context, { collection: context.accumulator.roles, key: `role\0preload\0${path}`, node, value: { role: "preload", path, resolution_context: staticPathResolutionContext(node.value), mechanism: "property:preload", module_key: null, location: range(node), }, }); }; const inspectStorageCall = ( node: t.CallExpression | t.NewExpression, name: string, first: string | undefined, context: FindingContext, ): void => { const storage = storageKind(name); if (storage === undefined) return; addLocatedFinding(context, { collection: context.accumulator.storage, key: `storage\0${storage}\0${first ?? ""}`, node, value: { kind: storage, name: first ?? null, mechanism: `call:${name}`, module_key: null, location: range(node), }, }); }; export const addReference = ( context: FindingContext, input: ReferenceInput, ): void => { const boundedSpecifier = input.specifier?.slice(0, 4_096) ?? null; const expression = boundedSpecifier === null ? sourceSlice(context.source, input.node).slice(0, 4_096) : null; addLocatedFinding(context, { collection: context.accumulator.references, key: `reference\0${input.kind}\0${boundedSpecifier ?? expression}`, node: input.node, value: { kind: input.kind, specifier: boundedSpecifier, expression, module_key: null, location: range(input.node), }, }); }; const addEndpoint = (context: FindingContext, input: EndpointInput): void => addLocatedFinding(context, { collection: context.accumulator.endpoints, key: `endpoint\0${input.kind}\0${input.value}`, node: input.node, value: { kind: input.kind, value: sanitizeCandidate(input.value), mechanism: input.mechanism, module_key: null, location: range(input.node), }, }); export const addSourceMapDirectives = ( source: string, accumulator: AnalysisAccumulator, maximum: number, ): void => { for (const match of source.matchAll( /\/\/[#@]\s*sourceMappingURL\s*=\s*([^\s]+)/gu, )) { const declared = match[1]?.slice(0, 4_096); if (declared === undefined) continue; const start = match.index; const location = rangeForOffsets(source, start, start + match[0].length); addBoundedFinding(accumulator, `source-map\0${declared}`, maximum, () => accumulator.sourceMaps.push({ declared_url: declared, location }), ); } };