Files
reverse-engineering-anything/src/domain/javascriptStaticAnalysis.ts
T

202 lines
6.2 KiB
TypeScript

import { parse } from "@babel/parser";
import * as t from "@babel/types";
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
import {
detectVendors,
failedJavaScriptStaticAnalysis,
registrationKey,
sortedUnique,
stringValue,
} from "./javascriptStaticAnalysisHelpers.js";
import {
addReference,
addSourceMapDirectives,
inspectCall,
inspectRouteProperty,
inspectRoleProperty,
} from "./javascriptStaticAnalysisCalls.js";
import {
inspectBundlerRegistration,
inspectEsbuildWrapper,
} from "./javascriptStaticAnalysisBundler.js";
import { finalizeLocatedFindings } from "./javascriptStaticAnalysisFindings.js";
import {
createJavaScriptAnalysisAccumulator,
type JavaScriptAnalysisAccumulator as AnalysisAccumulator,
} from "./javascriptStaticAnalysisState.js";
import type {
JavaScriptStaticAnalysis,
JavaScriptStaticAnalysisLimits,
} from "./javascriptStaticAnalysisTypes.js";
/** Parse one bounded JavaScript artifact and recover static structure only. */
export const analyzeJavaScriptStaticSource = (
source: string,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
let file: ReturnType<typeof parse>;
try {
file = parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript"],
});
} catch {
return failedJavaScriptStaticAnalysis();
}
const accumulator = createJavaScriptAnalysisAccumulator();
traverseStaticSource(source, file, accumulator, limits);
addSourceMapDirectives(source, accumulator, limits.maxFindings);
return finalizeStaticAnalysis(source, file, accumulator, limits);
};
const traverseStaticSource = (
source: string,
file: ReturnType<typeof parse>,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): void => {
if (limits.now() > limits.deadline) accumulator.truncated = true;
if (!accumulator.truncated)
t.traverseFast(file, (node) => {
accumulator.visitedNodes += 1;
if (
accumulator.visitedNodes > limits.maxAstNodes ||
(accumulator.visitedNodes % 1_024 === 0 &&
limits.now() > limits.deadline)
) {
accumulator.truncated = true;
return t.traverseFast.stop;
}
inspectNode(source, node, accumulator, limits);
return undefined;
});
};
const finalizeStaticAnalysis = (
source: string,
file: ReturnType<typeof parse>,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
const parserErrors = file.errors.length;
const limitations = [
...(parserErrors === 0
? []
: [
"The parser recovered from syntax errors; affected facts are partial.",
]),
...(accumulator.truncated
? ["AST analysis stopped at the configured node or time bound."]
: []),
...(accumulator.droppedFindings === 0
? []
: ["Static findings were truncated at the configured finding bound."]),
...(accumulator.structuralTruncation
? ["Bundler metadata reached a per-record structural bound."]
: []),
...(accumulator.unknownFindings === 0
? []
: [
"One or more static keys, expressions, or Electron boundary values were dynamic and remain unknown.",
]),
"JavaScript syntax was parsed as data and was never evaluated.",
];
return {
parse_status:
accumulator.truncated || accumulator.droppedFindings > 0
? "truncated"
: parserErrors > 0 || accumulator.unknownFindings > 0
? "partial"
: "complete",
parse_error_count: parserErrors,
visited_ast_nodes: Math.min(accumulator.visitedNodes, limits.maxAstNodes),
dropped_findings: accumulator.droppedFindings,
references: finalizeLocatedFindings(
accumulator.references,
accumulator.modules,
),
endpoints: finalizeLocatedFindings(
accumulator.endpoints,
accumulator.modules,
),
storage: finalizeLocatedFindings(accumulator.storage, accumulator.modules),
bundler_registrations: sortedUnique(
accumulator.registrations,
registrationKey,
),
role_paths: finalizeLocatedFindings(accumulator.roles, accumulator.modules),
source_map_urls: accumulator.sourceMaps,
vendors: detectVendors(source),
electron: {
browser_windows: finalizeLocatedFindings(
accumulator.browserWindows,
accumulator.modules,
),
context_bridge_apis: finalizeLocatedFindings(
accumulator.contextBridgeApis,
accumulator.modules,
),
ipc: finalizeLocatedFindings(accumulator.ipc, accumulator.modules),
sender_validations: finalizeLocatedFindings(
accumulator.senderValidations,
accumulator.modules,
),
utility_processes: finalizeLocatedFindings(
accumulator.utilityProcesses,
accumulator.modules,
),
native_addon_bindings: finalizeLocatedFindings(
accumulator.nativeAddonBindings,
accumulator.modules,
),
},
limitations,
};
};
const inspectNode = (
source: string,
node: t.Node,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): void => {
const findings = {
source,
accumulator,
maximum: limits.maxFindings,
};
inspectElectronStaticNode(node, findings);
if (t.isCallExpression(node)) {
inspectBundlerRegistration(source, node, accumulator, limits);
inspectEsbuildWrapper(source, node, accumulator, limits);
inspectCall(source, node, findings);
} else if (t.isNewExpression(node)) inspectCall(source, node, findings);
if (
(t.isImportDeclaration(node) || t.isExportAllDeclaration(node)) &&
node.source !== undefined
)
addReference(findings, {
node,
kind: "static-import",
specifier: node.source.value,
});
else if (t.isExportNamedDeclaration(node) && node.source != null)
addReference(findings, {
node,
kind: "static-import",
specifier: node.source.value,
});
else if (t.isImportExpression(node))
addReference(findings, {
node,
kind: "dynamic-import",
specifier: stringValue(node.source),
});
if (t.isObjectProperty(node)) {
inspectRouteProperty(node, findings);
inspectRoleProperty(node, findings);
}
};