mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import { collectJavaScriptSemanticAsyncEffects } from "./javascriptSemanticAsyncEffects.js";
|
|
import { collectJavaScriptSemanticCalls } from "./javascriptSemanticCalls.js";
|
|
import { collectJavaScriptSemanticChildProcesses } from "./javascriptSemanticChildProcesses.js";
|
|
import { collectJavaScriptSemanticFingerprintsSteps } from "./javascriptSemanticFingerprints.js";
|
|
import { collectJavaScriptSemanticDataEffects } from "./javascriptSemanticDataEffects.js";
|
|
import type {
|
|
JavaScriptSemanticCallable,
|
|
JavaScriptSemanticIr,
|
|
JavaScriptSemanticReference,
|
|
} from "./javascriptSemanticIr.js";
|
|
import { collectJavaScriptSemanticPromises } from "./javascriptSemanticPromises.js";
|
|
import { collectJavaScriptSemanticObjects } from "./javascriptSemanticObjects.js";
|
|
import { collectJavaScriptSemanticResources } from "./javascriptSemanticResources.js";
|
|
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
|
|
|
|
import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js";
|
|
import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js";
|
|
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
|
import {
|
|
readExactJavaScriptLiteral,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js";
|
|
import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "./javascriptAnalysisResourceControls.js";
|
|
|
|
type DerivedSemanticAnalysis = Pick<
|
|
JavaScriptSemanticIr,
|
|
| "assignmentFlows"
|
|
| "expressionFlows"
|
|
| "initializerFlows"
|
|
| "literalReturns"
|
|
| "expressionFlowsTruncated"
|
|
| "argumentFlows"
|
|
| "callResultFlows"
|
|
| "callReturnFlows"
|
|
| "callSites"
|
|
| "boundaryOperations"
|
|
| "childProcessInteractions"
|
|
| "childProcessSpawns"
|
|
| "closureCaptures"
|
|
| "configurationOperations"
|
|
| "eventOperations"
|
|
| "frontiers"
|
|
| "functionFingerprints"
|
|
| "objectOperations"
|
|
| "promiseOperations"
|
|
| "requestOperations"
|
|
| "resourceOperations"
|
|
| "timerOperations"
|
|
>;
|
|
|
|
/** Compose bounded semantic passes after lexical definitions are available. */
|
|
/**
|
|
* Collect derived semantics with a yield between collectors. Each collector
|
|
* completes before the next starts, so results equal the synchronous form.
|
|
*/
|
|
export function* collectJavaScriptDerivedSemanticsSteps(
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
references: readonly JavaScriptSemanticReference[],
|
|
parserPartial: boolean,
|
|
expressionNodeBudget: number = JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.semanticNodesPerFile,
|
|
): Generator<void, DerivedSemanticAnalysis> {
|
|
const calls = collectJavaScriptSemanticCalls(program, state, callables);
|
|
yield;
|
|
const promiseOperations = collectJavaScriptSemanticPromises(
|
|
program,
|
|
state,
|
|
callables,
|
|
);
|
|
yield;
|
|
const asyncEffects = collectJavaScriptSemanticAsyncEffects(
|
|
program,
|
|
state,
|
|
callables,
|
|
);
|
|
yield;
|
|
const childProcesses = collectJavaScriptSemanticChildProcesses(
|
|
program,
|
|
state,
|
|
callables,
|
|
);
|
|
yield;
|
|
const dataEffects = collectJavaScriptSemanticDataEffects(
|
|
program,
|
|
state,
|
|
callables,
|
|
);
|
|
yield;
|
|
const resourceOperations = collectJavaScriptSemanticResources(
|
|
program,
|
|
state,
|
|
callables,
|
|
);
|
|
yield;
|
|
const objectOperations = collectJavaScriptSemanticObjects(
|
|
program,
|
|
state,
|
|
callables,
|
|
);
|
|
yield;
|
|
const assignmentFlows = yield* collectAssignmentFlowsSteps(
|
|
program,
|
|
state,
|
|
callables,
|
|
);
|
|
const expressionFlows = yield* collectExpressionFlowsSteps(
|
|
program,
|
|
state,
|
|
callables,
|
|
expressionNodeBudget,
|
|
);
|
|
const initializerFlows = expressionFlows.initializers;
|
|
const functionFingerprints =
|
|
yield* collectJavaScriptSemanticFingerprintsSteps({
|
|
state,
|
|
callables,
|
|
calls,
|
|
promises: promiseOperations,
|
|
events: asyncEffects.eventOperations,
|
|
timers: asyncEffects.timerOperations,
|
|
childProcessSpawns: childProcesses.childProcessSpawns,
|
|
childProcessInteractions: childProcesses.childProcessInteractions,
|
|
requestOperations: dataEffects.requestOperations,
|
|
resourceOperations,
|
|
parserPartial,
|
|
});
|
|
return {
|
|
assignmentFlows,
|
|
expressionFlows: expressionFlows.expressions,
|
|
initializerFlows,
|
|
literalReturns: expressionFlows.literalReturns,
|
|
expressionFlowsTruncated: expressionFlows.truncated,
|
|
...calls,
|
|
promiseOperations,
|
|
...asyncEffects,
|
|
...childProcesses,
|
|
...dataEffects,
|
|
resourceOperations,
|
|
objectOperations,
|
|
functionFingerprints,
|
|
};
|
|
}
|
|
|
|
const isInfluenceExpression = (node: t.Node): boolean =>
|
|
t.isBinaryExpression(node) ||
|
|
t.isLogicalExpression(node) ||
|
|
t.isUnaryExpression(node) ||
|
|
t.isTemplateLiteral(node) ||
|
|
t.isConditionalExpression(node) ||
|
|
t.isSequenceExpression(node);
|
|
|
|
/** One cooperative pass retains only immediate value operands, not subtree ranges. */
|
|
function* collectExpressionFlowsSteps(
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
budget: number,
|
|
): Generator<
|
|
void,
|
|
{
|
|
expressions: JavaScriptSemanticIr["expressionFlows"];
|
|
initializers: JavaScriptSemanticIr["initializerFlows"];
|
|
literalReturns: JavaScriptSemanticIr["literalReturns"];
|
|
truncated: boolean;
|
|
}
|
|
> {
|
|
const expressions: JavaScriptSemanticIr["expressionFlows"][number][] = [];
|
|
const initializers: JavaScriptSemanticIr["initializerFlows"][number][] = [];
|
|
const literalReturns: JavaScriptSemanticIr["literalReturns"][number][] = [];
|
|
const candidates: { node: t.Node; ownerCallableId: string | null }[] = [];
|
|
let truncated = false;
|
|
const stack = createSemanticCallableIdStack(
|
|
new Set(callables.map((item) => item.callableId)),
|
|
);
|
|
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
|
|
enter: (node, parent) => {
|
|
stack.enter(node, parent);
|
|
const returnValue = t.isReturnStatement(node)
|
|
? node.argument
|
|
: t.isArrowFunctionExpression(node) && !t.isBlockStatement(node.body)
|
|
? node.body
|
|
: null;
|
|
if (returnValue != null) {
|
|
const expression = unwrapJavaScriptExpression(returnValue).node;
|
|
const exact = readExactJavaScriptLiteral(expression);
|
|
if (exact.found) {
|
|
const value = semanticPrimitiveSet([exact.value], state.precision);
|
|
if (value.status === "literal" && literalReturns.length < budget)
|
|
literalReturns.push({
|
|
location: range(expression),
|
|
value: value.value,
|
|
});
|
|
else if (value.status === "literal") truncated = true;
|
|
else if (
|
|
value.status === "unknown" &&
|
|
value.resourceLimit === "primitive-bytes"
|
|
)
|
|
state.valueResourceLimits.add("primitive-bytes");
|
|
}
|
|
}
|
|
if (
|
|
t.isVariableDeclarator(node) &&
|
|
t.isIdentifier(node.id) &&
|
|
node.init != null
|
|
) {
|
|
const init = unwrapJavaScriptExpression(node.init).node;
|
|
if (isInfluenceExpression(init)) {
|
|
if (initializers.length < budget)
|
|
initializers.push({ target: range(node.id), source: range(init) });
|
|
else truncated = true;
|
|
}
|
|
}
|
|
if (
|
|
isInfluenceExpression(node) &&
|
|
!(t.isTemplateLiteral(node) && t.isTaggedTemplateExpression(parent))
|
|
)
|
|
if (candidates.length < budget)
|
|
candidates.push({
|
|
node,
|
|
ownerCallableId: stack.callables.at(-1) ?? null,
|
|
});
|
|
else truncated = true;
|
|
},
|
|
exit: (node) => stack.exit(node),
|
|
});
|
|
let processedOperands = 0;
|
|
for (const [
|
|
candidateIndex,
|
|
{ node, ownerCallableId },
|
|
] of candidates.entries()) {
|
|
if (candidateIndex % 64 === 0) yield;
|
|
let operator: string;
|
|
let values: readonly t.Node[];
|
|
if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
|
|
operator = node.operator;
|
|
values = [node.left, node.right];
|
|
} else if (t.isUnaryExpression(node)) {
|
|
operator = node.operator;
|
|
values = [node.argument];
|
|
} else if (t.isTemplateLiteral(node)) {
|
|
operator = "template";
|
|
values = node.expressions;
|
|
} else if (t.isConditionalExpression(node)) {
|
|
operator = "?:";
|
|
values = [node.test, node.consequent, node.alternate];
|
|
} else if (t.isSequenceExpression(node)) {
|
|
operator = ",";
|
|
values = node.expressions;
|
|
} else continue;
|
|
const operands: JavaScriptSemanticIr["expressionFlows"][number]["operands"][number][] =
|
|
[];
|
|
for (const [index, value] of values.entries()) {
|
|
if (processedOperands >= budget) {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
if (processedOperands % 64 === 0) yield;
|
|
processedOperands += 1;
|
|
const conditional =
|
|
(t.isLogicalExpression(node) && index === 1) ||
|
|
(t.isConditionalExpression(node) && index > 0);
|
|
// Earlier sequence expressions may change the final result via side effects.
|
|
const unsupported =
|
|
(t.isUnaryExpression(node) &&
|
|
(node.operator === "void" || node.operator === "delete")) ||
|
|
(t.isSequenceExpression(node) && index !== values.length - 1);
|
|
const operand = unwrapJavaScriptExpression(value).node;
|
|
const exact = readExactJavaScriptLiteral(operand);
|
|
const literal = exact.found
|
|
? semanticPrimitiveSet([exact.value], state.precision)
|
|
: null;
|
|
if (
|
|
literal?.status === "unknown" &&
|
|
literal.resourceLimit === "primitive-bytes"
|
|
)
|
|
state.valueResourceLimits.add("primitive-bytes");
|
|
operands.push({
|
|
location: range(operand),
|
|
conditional,
|
|
kind: unsupported
|
|
? "unsupported"
|
|
: t.isIdentifier(operand)
|
|
? "reference"
|
|
: literal?.status === "literal"
|
|
? "literal"
|
|
: isInfluenceExpression(operand)
|
|
? "expression"
|
|
: "unsupported",
|
|
...(!unsupported && literal?.status === "literal"
|
|
? { literal: literal.value }
|
|
: {}),
|
|
});
|
|
}
|
|
expressions.push({
|
|
location: range(node),
|
|
operator,
|
|
ownerCallableId,
|
|
operands,
|
|
});
|
|
}
|
|
return { expressions, initializers, literalReturns, truncated };
|
|
}
|
|
|
|
/** Keep assignment syntax in the IR so graph consumers never reparse source. */
|
|
function* collectAssignmentFlowsSteps(
|
|
program: t.Program,
|
|
state: JavaScriptSemanticAnalysisState,
|
|
callables: readonly JavaScriptSemanticCallable[],
|
|
): Generator<void, JavaScriptSemanticIr["assignmentFlows"]> {
|
|
const flows: JavaScriptSemanticIr["assignmentFlows"][number][] = [];
|
|
const stack = createSemanticCallableIdStack(
|
|
new Set(callables.map((item) => item.callableId)),
|
|
);
|
|
yield* traverseJavaScriptValueAstSteps(program, state.erasedSyntaxNodes, {
|
|
enter: (node, parent) => {
|
|
stack.enter(node, parent);
|
|
if (!t.isAssignmentExpression(node)) return;
|
|
const target = unwrapJavaScriptExpression(node.left).node;
|
|
// Destructuring has its own property-selection owner.
|
|
if (!t.isIdentifier(target) && !t.isMemberExpression(target)) return;
|
|
const source = unwrapJavaScriptExpression(node.right).node;
|
|
flows.push({
|
|
target: range(target),
|
|
source: range(source),
|
|
sourceIsReference:
|
|
t.isIdentifier(source) || t.isMemberExpression(source),
|
|
operator: node.operator,
|
|
ownerCallableId: stack.callables.at(-1) ?? null,
|
|
});
|
|
},
|
|
exit: (node) => stack.exit(node),
|
|
});
|
|
return flows;
|
|
}
|