rea-agents 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +129 -45
  2. package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
  3. package/bridge/hopper_bridge.py +13 -3
  4. package/dist/application/AbortablePromise.js +25 -0
  5. package/dist/application/AnalysisProvider.js +3 -0
  6. package/dist/application/AnalysisProviderEvaluation.js +60 -0
  7. package/dist/application/AnalysisProviderRegistry.js +228 -0
  8. package/dist/application/AnalysisSnapshotCache.js +41 -21
  9. package/dist/application/AnalysisSnapshotFiles.js +5 -6
  10. package/dist/application/BinarySession.js +218 -101
  11. package/dist/application/CompositeProvider.js +20 -8
  12. package/dist/application/DirectAnalysis.js +49 -35
  13. package/dist/application/Doctor.js +54 -2
  14. package/dist/application/EnhancedTools.js +1 -1
  15. package/dist/application/InvestigationProviders.js +11 -0
  16. package/dist/application/JavaScriptArtifactAnalysis.js +299 -0
  17. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  18. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  19. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  20. package/dist/application/JavaScriptArtifactGraphBuilder.js +101 -0
  21. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  22. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  23. package/dist/application/JavaScriptArtifactGraphEvidence.js +89 -0
  24. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  25. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  26. package/dist/application/JavaScriptArtifactReconstruction.js +74 -0
  27. package/dist/application/JavaScriptArtifactReconstructionInput.js +99 -0
  28. package/dist/application/MacHopper.js +20 -4
  29. package/dist/application/ProcessCaptureLifecycle.js +1 -1
  30. package/dist/application/ProcessHarness.js +1 -1
  31. package/dist/application/ProcessSampling.js +1 -1
  32. package/dist/application/SessionProviderRouter.js +219 -0
  33. package/dist/application/Setup.js +84 -60
  34. package/dist/application/SetupClients.js +1 -1
  35. package/dist/application/SetupPlan.js +9 -2
  36. package/dist/application/SupportedClients.js +40 -16
  37. package/dist/application/Upgrade.js +1 -1
  38. package/dist/application/runtime.js +9 -6
  39. package/dist/artifacts/AsarArtifactReader.js +23 -3
  40. package/dist/browser/CdpBrowserProvider.js +4 -2
  41. package/dist/browser/CdpElectronProvider.js +4 -2
  42. package/dist/browser/CdpEndpoint.js +14 -1
  43. package/dist/catalogIdentity.js +2 -41
  44. package/dist/cli.js +58 -34
  45. package/dist/cliBrowserAdvancedCommands.js +5 -4
  46. package/dist/cliBrowserCommands.js +5 -4
  47. package/dist/cliCommandNames.js +43 -0
  48. package/dist/cliElectronCommands.js +3 -2
  49. package/dist/cliEvidenceCommands.js +4 -3
  50. package/dist/cliInvestigationCommands.js +2 -1
  51. package/dist/cliPolicyCommands.js +2 -1
  52. package/dist/cliProcessCommands.js +3 -2
  53. package/dist/config.js +18 -2
  54. package/dist/contracts/browserToolContracts.js +5 -5
  55. package/dist/contracts/electronToolContracts.js +3 -3
  56. package/dist/contracts/enhancedInputs.js +2 -2
  57. package/dist/contracts/providerSelection.js +28 -0
  58. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  59. package/dist/contracts/toolContracts.js +22 -20
  60. package/dist/contracts/toolOutputSchemas.js +117 -22
  61. package/dist/doctorRuntime.js +6 -0
  62. package/dist/domain/analysisProfile.js +78 -0
  63. package/dist/domain/analysisSnapshot.js +92 -46
  64. package/dist/domain/artifactGraph.js +1 -0
  65. package/dist/domain/binaryTarget.js +4 -29
  66. package/dist/domain/errors.js +81 -6
  67. package/dist/domain/evidence.js +44 -5
  68. package/dist/domain/evidenceBundle.js +9 -7
  69. package/dist/domain/functionComparison.js +9 -3
  70. package/dist/domain/functionComparisonNormalization.js +37 -0
  71. package/dist/domain/functionDossierEvidence.js +4 -1
  72. package/dist/domain/hopperValues.js +66 -9
  73. package/dist/domain/investigationWorkspace.js +2 -1
  74. package/dist/domain/javascriptApplicationEvidenceSchemas.js +361 -0
  75. package/dist/domain/javascriptApplicationGraph.js +281 -0
  76. package/dist/domain/javascriptApplicationGraphSchemas.js +132 -0
  77. package/dist/domain/javascriptAstFingerprint.js +143 -0
  78. package/dist/domain/javascriptStaticAnalysis.js +358 -0
  79. package/dist/domain/javascriptStaticAnalysisHelpers.js +264 -0
  80. package/dist/domain/javascriptStaticAnalysisState.js +17 -0
  81. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  82. package/dist/generatedPackageMetadata.js +1 -1
  83. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  84. package/dist/ghidra/GhidraClient.js +400 -0
  85. package/dist/ghidra/GhidraClientTypes.js +1 -0
  86. package/dist/ghidra/GhidraDefaults.js +20 -0
  87. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  88. package/dist/ghidra/GhidraDoctor.js +59 -0
  89. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  90. package/dist/ghidra/GhidraInstallation.js +239 -0
  91. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  92. package/dist/ghidra/GhidraLauncher.js +139 -0
  93. package/dist/ghidra/GhidraProvider.js +360 -0
  94. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  95. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  96. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  97. package/dist/ghidra/GhidraSessionError.js +39 -0
  98. package/dist/ghidra/GhidraSessionValues.js +62 -0
  99. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  100. package/dist/ghidra/protocol.js +69 -0
  101. package/dist/hopper/BridgeLauncher.js +19 -54
  102. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  103. package/dist/hopper/HopperClient.js +154 -162
  104. package/dist/hopper/HopperProvider.js +99 -7
  105. package/dist/hopper/HopperSessionValues.js +27 -0
  106. package/dist/hopper/HopperSocketConnection.js +31 -0
  107. package/dist/native/NativeMacOSProvider.js +3 -1
  108. package/dist/process/PendingOperations.js +59 -0
  109. package/dist/process/PrivateRuntimeRoot.js +33 -0
  110. package/dist/process/ProviderDeadline.js +95 -0
  111. package/dist/process/ProviderProcess.js +275 -0
  112. package/dist/server/createServer.js +1 -0
  113. package/dist/server/promptCompletion.js +9 -3
  114. package/dist/server/registerEnhancedTools.js +9 -6
  115. package/dist/server/registerOfficialTools.js +3 -0
  116. package/dist/server/registerSessionTools.js +6 -1
  117. package/package.json +8 -6
  118. package/scripts/rea.mjs +35 -1
  119. /package/dist/{application → process}/ProcessOwnership.js +0 -0
@@ -13,20 +13,19 @@ import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/anal
13
13
  import { err, ok } from "../domain/result.js";
14
14
  import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
15
15
  import { authorizeFileReadWithDeferredWrite, } from "./DeferredFileAuthorization.js";
16
- const WORKFLOW_PROVIDER = {
17
- id: "rea-workflow",
18
- name: "REA composed investigation workflow",
19
- version: "1",
20
- };
16
+ import { REA_WORKFLOW_PROVIDER, workflowAnalysisProfile, } from "./InvestigationProviders.js";
21
17
  /**
22
- * Open one binary, execute one tool, and always release the bridge session.
18
+ * Open one binary, execute one tool, and always release provider resources.
23
19
  * Unlike MCP mode, every CLI invocation is intentionally isolated and does not
24
- * retain a target or bridge resources for a subsequent command.
20
+ * retain a target or provider client for a subsequent command.
25
21
  */
26
22
  export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => withProcessCancellation(options.signal, (signal) => runAnalysis(path, tool, arguments_, {
27
23
  logger: options.logger ?? silentLogger,
28
24
  snapshotPath: options.snapshotPath,
29
25
  signal,
26
+ ...(options.providerId === undefined
27
+ ? {}
28
+ : { providerId: options.providerId }),
30
29
  ...(options.permissionAuthority === undefined
31
30
  ? {}
32
31
  : { permissionAuthority: options.permissionAuthority }),
@@ -37,7 +36,7 @@ export const runProviderAnalysis = async (path, tool, arguments_, logger = silen
37
36
  snapshotPath: undefined,
38
37
  signal: operationSignal,
39
38
  }));
40
- /** Describe configured providers without opening a target or launching Hopper. */
39
+ /** Describe configured providers without opening a target or starting one. */
41
40
  export const runSessionStatus = async (logger = silentLogger) => {
42
41
  const config = parseConfig(process.env);
43
42
  if (!config.ok)
@@ -135,30 +134,44 @@ const runAnalysis = async (path, tool, arguments_, options) => {
135
134
  });
136
135
  if (!authorization.ok)
137
136
  return cliError(authorization.error);
138
- const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
139
- if (unavailable !== undefined)
140
- return cliError(unavailable);
141
137
  const session = createBinarySession(config.value, logger);
142
138
  try {
143
139
  const prepared = await prepareSnapshot({
144
140
  path,
145
- tool,
146
- arguments: arguments_,
147
141
  snapshotPath,
148
142
  policy: config.value.analysisSnapshotFilePolicy,
149
- provider: replayProviderFor(session, tool),
150
143
  });
151
144
  if (!prepared.ok)
152
145
  return cliError(prepared.error);
153
- if (prepared.value.evidence !== undefined)
154
- return prepared.value.evidence;
155
- const writable = await authorizeDeferredWrite(authorization.value);
156
- if (!writable.ok)
157
- return cliError(writable.error);
158
146
  const { snapshot } = prepared.value;
159
- const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
147
+ const opened = await session.open(path, {
148
+ signal,
149
+ ...(snapshot === undefined ? {} : { snapshot }),
150
+ ...(options.providerId === undefined
151
+ ? {}
152
+ : { providerId: options.providerId }),
153
+ });
160
154
  if (!opened.ok)
161
155
  return cliError(opened.error);
156
+ const evidenceProfile = analysisProfileForEvidence(session, tool);
157
+ const bindingProfile = session.analysisProfile();
158
+ if (snapshot !== undefined &&
159
+ evidenceProfile !== undefined &&
160
+ bindingProfile !== undefined) {
161
+ const cached = snapshotEvidenceForQuery(snapshot, {
162
+ target: opened.value,
163
+ bindingProfile,
164
+ operation: tool,
165
+ parameters: arguments_,
166
+ provider: replayProviderFor(session, tool),
167
+ evidenceProfile,
168
+ });
169
+ if (cached !== undefined)
170
+ return cached;
171
+ }
172
+ const writable = await authorizeDeferredWrite(authorization.value);
173
+ if (!writable.ok)
174
+ return cliError(writable.error);
162
175
  let output;
163
176
  let evidence;
164
177
  if (tool === "binary_overview" ||
@@ -170,10 +183,13 @@ const runAnalysis = async (path, tool, arguments_, options) => {
170
183
  else {
171
184
  evidence = createEvidence(opened.value, tool === "analyze_function"
172
185
  ? session.providerIdentity(tool)
173
- : WORKFLOW_PROVIDER, {
186
+ : REA_WORKFLOW_PROVIDER, {
174
187
  operation: tool,
175
188
  parameters: arguments_,
176
189
  result: result.value,
190
+ ...(evidenceProfile === undefined
191
+ ? {}
192
+ : { analysisProfile: evidenceProfile }),
177
193
  confidence: "derived",
178
194
  limitations: ["Derived by an REA composed workflow."],
179
195
  });
@@ -189,6 +205,9 @@ const runAnalysis = async (path, tool, arguments_, options) => {
189
205
  operation: tool,
190
206
  parameters: arguments_,
191
207
  result: result.value.result,
208
+ ...(result.value.analysisProfile === undefined
209
+ ? {}
210
+ : { analysisProfile: result.value.analysisProfile }),
192
211
  rawResult: result.value.rawResult,
193
212
  limitations: result.value.limitations,
194
213
  locations: result.value.locations,
@@ -226,7 +245,7 @@ const withProcessCancellation = async (suppliedSignal, operation) => {
226
245
  }
227
246
  };
228
247
  const prepareSnapshot = async (options) => {
229
- const { path, tool, arguments: arguments_, snapshotPath, policy, provider, } = options;
248
+ const { path, snapshotPath, policy } = options;
230
249
  if (snapshotPath === undefined || !(await fileExists(snapshotPath)))
231
250
  return ok({});
232
251
  const loaded = await readAnalysisSnapshot(snapshotPath, policy);
@@ -237,22 +256,17 @@ const prepareSnapshot = async (options) => {
237
256
  return target;
238
257
  if (!snapshotMatchesTarget(loaded.value.target, target.value))
239
258
  return err(new EvidenceIntegrityError("Analysis snapshot target does not match the requested binary"));
240
- const evidence = snapshotEvidenceForQuery(loaded.value, {
241
- target: target.value,
242
- operation: tool,
243
- parameters: arguments_,
244
- provider,
245
- });
246
- return ok(evidence === undefined
247
- ? { snapshot: loaded.value }
248
- : { snapshot: loaded.value, evidence });
259
+ return ok({ snapshot: loaded.value });
249
260
  };
250
- const snapshotUnavailable = (snapshotPath, loaderArgs) => snapshotPath !== undefined && loaderArgs.length > 0
251
- ? new EvidenceIntegrityError("Analysis snapshots are unavailable with custom Hopper loader arguments")
252
- : undefined;
253
261
  const replayProviderFor = (session, tool) => tool === "binary_overview" || tool === "trace_feature"
254
- ? WORKFLOW_PROVIDER
262
+ ? REA_WORKFLOW_PROVIDER
255
263
  : session.providerIdentity(tool);
264
+ const analysisProfileForEvidence = (session, tool) => {
265
+ if (tool !== "binary_overview" && tool !== "trace_feature")
266
+ return session.analysisProfile(tool);
267
+ const upstream = session.analysisProfile();
268
+ return upstream === undefined ? undefined : workflowAnalysisProfile(upstream);
269
+ };
256
270
  const fileExists = async (path) => {
257
271
  try {
258
272
  await access(path);
@@ -65,6 +65,7 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
65
65
  remediation: unsupportedHopperRemediation(host.configuredHopperPath === hopperPath),
66
66
  classification: "config_drift",
67
67
  }));
68
+ const providerInspections = await inspectDoctorProviders(host, checks);
68
69
  if (host.platform === "linux" && hopperPath !== undefined)
69
70
  checks.push(check("hopper-demo-runtime", await host.linuxDemoRuntimeReady(), undefined, {
70
71
  remediation: "Rerun rea setup to install the Xvfb, xauth, Python, X11, and XTEST packages required for Linux demo sessions.",
@@ -89,8 +90,16 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
89
90
  "Run rea setup, then restart the affected client.",
90
91
  });
91
92
  return {
92
- healthy: checks.every(({ ok }) => ok),
93
+ healthy: doctorHealthy(checks, {
94
+ hopperAvailable: hopperPath !== undefined &&
95
+ checks
96
+ .filter(({ name }) => name.startsWith("hopper"))
97
+ .every(({ ok }) => ok),
98
+ hopperConfigured: host.configuredHopperPath !== undefined,
99
+ providerInspections,
100
+ }),
93
101
  ...(hopperPath === undefined ? {} : { hopperPath }),
102
+ ...(providerInspections === undefined ? {} : { providerInspections }),
94
103
  checks,
95
104
  identity: {
96
105
  cli_package_version: PRODUCT_IDENTITY.packageVersion,
@@ -129,7 +138,7 @@ const unsupportedHopperRemediation = (configured) => configured
129
138
  ? "Unset or update HOPPER_LAUNCHER_PATH, install the Hopper build supported by this REA release, or update REA."
130
139
  : "Install the Hopper build supported by this REA release, or update REA for a newer Hopper build.";
131
140
  /** Create diagnostics backed by the current process and host commands. */
132
- export const systemDoctorHost = () => ({
141
+ export const systemDoctorHost = (options = {}) => ({
133
142
  platform: process.platform,
134
143
  nodeVersion: process.versions.node,
135
144
  ...(process.env.HOPPER_LAUNCHER_PATH === undefined
@@ -208,6 +217,9 @@ export const systemDoctorHost = () => ({
208
217
  }
209
218
  return paths;
210
219
  },
220
+ ...(options.providerInspections === undefined
221
+ ? {}
222
+ : { providerInspections: options.providerInspections }),
211
223
  async installationPaths() {
212
224
  try {
213
225
  const command = process.platform === "win32" ? "where" : "which";
@@ -243,3 +255,43 @@ const check = (name, ok, detail, failure) => ({
243
255
  ...(detail === undefined ? {} : { detail }),
244
256
  ...(ok ? {} : { remediation: failure.remediation }),
245
257
  });
258
+ const providerDoctorChecks = (inspection) => inspection.checks.map((candidate) => ({
259
+ name: candidate.name === "configuration"
260
+ ? inspection.id
261
+ : `${inspection.id}-${candidate.name}`,
262
+ ok: candidate.ok,
263
+ classification: candidate.ok ? "healthy" : candidate.classification,
264
+ detail: candidate.detail,
265
+ ...(candidate.remediation === null
266
+ ? {}
267
+ : { remediation: candidate.remediation }),
268
+ }));
269
+ const inspectDoctorProviders = async (host, checks) => {
270
+ const inspections = await host.providerInspections?.();
271
+ if (inspections === undefined)
272
+ return undefined;
273
+ for (const inspection of inspections)
274
+ checks.push(...providerDoctorChecks(inspection));
275
+ return inspections;
276
+ };
277
+ const doctorHealthy = (checks, providers) => {
278
+ if (providers.providerInspections === undefined)
279
+ return checks.every(({ ok }) => ok);
280
+ const providerChecks = checks.filter(({ name }) => name.startsWith("hopper") ||
281
+ providers.providerInspections?.some((inspection) => providerCheckName(inspection.id, name)) === true);
282
+ const coreHealthy = checks
283
+ .filter((candidate) => !providerChecks.includes(candidate))
284
+ .every(({ ok }) => ok);
285
+ const configuredProvidersHealthy = providerChecks
286
+ .filter(({ name }) => {
287
+ if (name.startsWith("hopper"))
288
+ return providers.hopperConfigured;
289
+ return providers.providerInspections?.some((inspection) => inspection.configured && providerCheckName(inspection.id, name));
290
+ })
291
+ .every(({ ok }) => ok);
292
+ return (coreHealthy &&
293
+ configuredProvidersHealthy &&
294
+ (providers.hopperAvailable ||
295
+ providers.providerInspections.some(({ available }) => available)));
296
+ };
297
+ const providerCheckName = (providerId, name) => name === providerId || name.startsWith(`${providerId}-`);
@@ -4,7 +4,7 @@ import { addressDistance, parseDocuments, parseFunctionDossier, parseAddressedPa
4
4
  import { err, ok } from "../domain/result.js";
5
5
  import { categorizeSwiftTypes, discoverObjcClasses, discoverObjcProtocols, discoverSwiftClasses, } from "../domain/symbolAnalysis.js";
6
6
  /**
7
- * Composes official Hopper operations into bounded reverse-engineering tools.
7
+ * Composes direct provider operations into bounded reverse-engineering tools.
8
8
  * Inputs are parsed again at this application boundary even though MCP normally
9
9
  * validates them, allowing direct callers to use the same service safely.
10
10
  */
@@ -1,3 +1,14 @@
1
+ import { createAnalysisProfile, } from "../domain/analysisProfile.js";
2
+ /** Provider identity for deterministic composed analysis workflows. */
3
+ export const REA_WORKFLOW_PROVIDER = {
4
+ id: "rea-workflow",
5
+ name: "REA composed investigation workflow",
6
+ version: "1",
7
+ };
8
+ /** Commit a workflow result to the exact upstream deep-analysis profile. */
9
+ export const workflowAnalysisProfile = (upstream) => createAnalysisProfile(REA_WORKFLOW_PROVIDER, 1, {
10
+ upstream_analysis_profile: upstream,
11
+ });
1
12
  /** Provider identity for deterministic artifact inventories. */
2
13
  export const ARTIFACT_GRAPH_PROVIDER = {
3
14
  id: "rea-artifact-graph",
@@ -0,0 +1,299 @@
1
+ import { createHash } from "node:crypto";
2
+ import { analyzeJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
3
+ /** Analyze selected text files under one shared AST, finding, module, and time budget. */
4
+ export const analyzeJavaScriptArtifactFiles = (fileSet, input, now) => {
5
+ const state = emptyArtifactAnalysis();
6
+ const context = {
7
+ state,
8
+ input,
9
+ deadline: now() + input.limits.max_parse_milliseconds,
10
+ now,
11
+ };
12
+ for (const file of fileSet.files)
13
+ analyzeArtifactFile(file, context);
14
+ return finalizeArtifactAnalysis(state, input);
15
+ };
16
+ const finalizeArtifactAnalysis = (state, input) => {
17
+ const omittedHtmlFindings = Math.max(0, state.htmlScripts.length + state.findings - input.limits.max_findings);
18
+ const findings = Math.min(input.limits.max_findings, state.findings + state.htmlScripts.length);
19
+ const sourceMapTruncations = state.sourceMaps.filter(({ status }) => status === "truncated").length;
20
+ const truncatedScopes = state.truncatedScopes +
21
+ sourceMapTruncations +
22
+ (omittedHtmlFindings > 0 ? 1 : 0);
23
+ return {
24
+ files: state.files,
25
+ packages: state.packages,
26
+ html_scripts: state.htmlScripts.slice(0, input.limits.max_findings),
27
+ source_maps: state.sourceMaps,
28
+ visited_ast_nodes: state.visitedNodes,
29
+ findings,
30
+ modules: state.modules,
31
+ parse_failures: state.parseFailures,
32
+ truncated_scopes: truncatedScopes,
33
+ limitations: [
34
+ "JavaScript and HTML were parsed as inert text; bundle bootstrap code was never executed.",
35
+ "Static paths and relationships may remain unresolved when expressions are dynamic or obfuscated.",
36
+ ...(input.source_map_read_approved
37
+ ? []
38
+ : [
39
+ "Source maps were inventoried but their contents were not read without separate approval.",
40
+ ]),
41
+ ...(truncatedScopes === 0
42
+ ? []
43
+ : ["One or more static-analysis scopes reached an approved bound."]),
44
+ ],
45
+ };
46
+ };
47
+ const analyzeArtifactFile = (file, context) => {
48
+ const { state, input } = context;
49
+ if (file.kind === "package-json")
50
+ state.packages.push(parsePackage(file));
51
+ if (file.kind === "html" && file.text.included)
52
+ state.htmlScripts.push(...parseHtmlScripts(file.path, file.text.value, Math.max(0, input.limits.max_findings - state.findings)));
53
+ if (file.kind === "source-map")
54
+ addSourceMap(file, context);
55
+ if (file.kind !== "javascript" || !file.text.included) {
56
+ state.files.push({ file, javascript: null });
57
+ return;
58
+ }
59
+ const remainingNodes = input.limits.max_ast_nodes - state.visitedNodes;
60
+ const remainingFindings = input.limits.max_findings - state.findings;
61
+ const remainingModules = input.limits.max_modules - state.modules;
62
+ if (remainingNodes <= 0 ||
63
+ remainingFindings <= 0 ||
64
+ remainingModules <= 0 ||
65
+ context.now() > context.deadline) {
66
+ state.files.push({ file, javascript: null });
67
+ state.truncatedScopes += 1;
68
+ return;
69
+ }
70
+ const analysis = analyzeJavaScriptStaticSource(file.text.value, {
71
+ maxAstNodes: remainingNodes,
72
+ maxFindings: remainingFindings,
73
+ maxModules: remainingModules,
74
+ deadline: context.deadline,
75
+ now: context.now,
76
+ });
77
+ state.files.push({ file, javascript: analysis });
78
+ state.visitedNodes += analysis.visited_ast_nodes;
79
+ state.findings += findingCount(analysis);
80
+ state.modules += analysis.bundler_registrations.reduce((count, registration) => count + registration.modules.length, 0);
81
+ if (analysis.parse_status === "failed")
82
+ state.parseFailures += 1;
83
+ if (analysis.parse_status === "truncated")
84
+ state.truncatedScopes += 1;
85
+ };
86
+ const addSourceMap = (file, context) => {
87
+ const sourceMap = parseSourceMap(file, {
88
+ deadline: context.deadline,
89
+ now: context.now,
90
+ maximumSources: Math.max(0, context.input.limits.max_source_map_sources -
91
+ context.state.sourceMapSources),
92
+ });
93
+ context.state.sourceMaps.push(sourceMap);
94
+ context.state.sourceMapSources += sourceMap.sources.length;
95
+ };
96
+ const emptyArtifactAnalysis = () => ({
97
+ files: [],
98
+ packages: [],
99
+ htmlScripts: [],
100
+ sourceMaps: [],
101
+ visitedNodes: 0,
102
+ findings: 0,
103
+ modules: 0,
104
+ parseFailures: 0,
105
+ truncatedScopes: 0,
106
+ sourceMapSources: 0,
107
+ });
108
+ const parsePackage = (file) => {
109
+ if (!file.text.included)
110
+ return unavailablePackage(file, "Package metadata text was unavailable.");
111
+ let value;
112
+ try {
113
+ value = JSON.parse(file.text.value);
114
+ }
115
+ catch {
116
+ return invalidPackage(file, "package.json is not valid JSON.");
117
+ }
118
+ if (!isRecord(value))
119
+ return invalidPackage(file, "package.json root is not an object.");
120
+ return {
121
+ path: file.path,
122
+ sha256: file.sha256,
123
+ status: "included",
124
+ name: boundedString(value.name),
125
+ version: boundedString(value.version),
126
+ main: boundedString(value.main),
127
+ renderer: boundedString(value.renderer) ??
128
+ boundedString(value.browser) ??
129
+ boundedString(value.module),
130
+ limitation: null,
131
+ };
132
+ };
133
+ const invalidPackage = (file, limitation) => ({
134
+ ...unavailablePackage(file, limitation),
135
+ status: "invalid",
136
+ });
137
+ const unavailablePackage = (file, limitation) => ({
138
+ path: file.path,
139
+ sha256: file.sha256,
140
+ status: "unavailable",
141
+ name: null,
142
+ version: null,
143
+ main: null,
144
+ renderer: null,
145
+ limitation,
146
+ });
147
+ const parseHtmlScripts = (path, text, maximum) => {
148
+ const scripts = [];
149
+ const pattern = /<script\b[^>]*\bsrc\s*=\s*(["'])([^"']+)\1[^>]*>/giu;
150
+ for (const match of text.matchAll(pattern)) {
151
+ const script = match[2]?.slice(0, 4_096);
152
+ if (script === undefined || scripts.length >= maximum)
153
+ break;
154
+ const start = match.index;
155
+ scripts.push({
156
+ html_path: path,
157
+ script_path: script,
158
+ location: rangeForOffsets(text, start, start + match[0].length),
159
+ });
160
+ }
161
+ return scripts;
162
+ };
163
+ const parseSourceMap = (file, context) => {
164
+ if (!file.text.included)
165
+ return unavailableSourceMap(file);
166
+ if (context.now() > context.deadline)
167
+ return truncatedSourceMap(file, null, "Parse deadline reached before source-map decoding.");
168
+ let value;
169
+ try {
170
+ value = JSON.parse(file.text.value);
171
+ }
172
+ catch {
173
+ return invalidSourceMap(file, "Source map is not valid JSON.");
174
+ }
175
+ if (context.now() > context.deadline)
176
+ return truncatedSourceMap(file, null, "Parse deadline elapsed during source-map decoding.");
177
+ const maps = flattenSourceMaps(value, 10_000);
178
+ if (maps === undefined)
179
+ return invalidSourceMap(file, "Source map is not a bounded version 3 map.");
180
+ return collectSourceMapOriginals(file, maps, context);
181
+ };
182
+ const collectSourceMapOriginals = (file, maps, context) => {
183
+ const sources = [];
184
+ let total = 0;
185
+ for (const map of maps) {
186
+ const names = map.sources;
187
+ if (!Array.isArray(names))
188
+ return invalidSourceMap(file, "Source map has no sources array.");
189
+ const contents = Array.isArray(map.sourcesContent)
190
+ ? map.sourcesContent
191
+ : [];
192
+ const root = typeof map.sourceRoot === "string" ? map.sourceRoot : "";
193
+ for (const [index, raw] of names.entries()) {
194
+ if (total % 1_024 === 0 && context.now() > context.deadline)
195
+ return {
196
+ path: file.path,
197
+ sha256: file.sha256,
198
+ status: "truncated",
199
+ sources,
200
+ omitted_sources: null,
201
+ limitation: "Parse deadline elapsed during source-map traversal.",
202
+ };
203
+ if (typeof raw !== "string")
204
+ return invalidSourceMap(file, "Source map contains a non-string source name.");
205
+ total += 1;
206
+ if (sources.length >= context.maximumSources)
207
+ continue;
208
+ const content = typeof contents[index] === "string" ? contents[index] : null;
209
+ sources.push({
210
+ source: resolveSourceName(root, raw),
211
+ content,
212
+ content_sha256: content === null ? null : sha256(content),
213
+ });
214
+ }
215
+ }
216
+ const omitted = total - sources.length;
217
+ return {
218
+ path: file.path,
219
+ sha256: file.sha256,
220
+ status: omitted === 0 ? "included" : "truncated",
221
+ sources,
222
+ omitted_sources: omitted,
223
+ limitation: omitted === 0
224
+ ? null
225
+ : "Original-source inventory reached the approved source-map limit.",
226
+ };
227
+ };
228
+ const unavailableSourceMap = (file) => {
229
+ if (file.text.included)
230
+ throw new TypeError("Expected unavailable source-map text");
231
+ const notApproved = file.text.reason === "not-approved";
232
+ return {
233
+ path: file.path,
234
+ sha256: file.sha256,
235
+ status: notApproved ? "not-approved" : "invalid",
236
+ sources: [],
237
+ omitted_sources: 0,
238
+ limitation: notApproved
239
+ ? "Source-map content requires separate approval."
240
+ : "Source-map text was unavailable within approved limits.",
241
+ };
242
+ };
243
+ const flattenSourceMaps = (root, maximum) => {
244
+ if (!isRecord(root) || root.version !== 3)
245
+ return undefined;
246
+ const maps = [];
247
+ const pending = [root];
248
+ while (pending.length > 0) {
249
+ const map = pending.pop();
250
+ if (map === undefined || map.version !== 3)
251
+ return undefined;
252
+ if (Array.isArray(map.sections)) {
253
+ if (pending.length + maps.length + map.sections.length > maximum)
254
+ return undefined;
255
+ for (const section of map.sections) {
256
+ if (!isRecord(section) || !isRecord(section.map))
257
+ return undefined;
258
+ pending.push(section.map);
259
+ }
260
+ }
261
+ else
262
+ maps.push(map);
263
+ }
264
+ return maps;
265
+ };
266
+ const truncatedSourceMap = (file, omitted, limitation) => ({
267
+ path: file.path,
268
+ sha256: file.sha256,
269
+ status: "truncated",
270
+ sources: [],
271
+ omitted_sources: omitted,
272
+ limitation,
273
+ });
274
+ const invalidSourceMap = (file, limitation) => ({
275
+ path: file.path,
276
+ sha256: file.sha256,
277
+ status: "invalid",
278
+ sources: [],
279
+ omitted_sources: 0,
280
+ limitation,
281
+ });
282
+ const findingCount = (analysis) => analysis.references.length +
283
+ analysis.endpoints.length +
284
+ analysis.storage.length +
285
+ analysis.role_paths.length +
286
+ analysis.source_map_urls.length +
287
+ analysis.bundler_registrations.length;
288
+ const rangeForOffsets = (text, start, end) => ({
289
+ start: pointForOffset(text, start),
290
+ end: pointForOffset(text, end),
291
+ });
292
+ const pointForOffset = (text, offset) => {
293
+ const lines = text.slice(0, offset).split("\n");
294
+ return { line: lines.length, column: lines.at(-1)?.length ?? 0 };
295
+ };
296
+ const resolveSourceName = (root, source) => `${root}${root !== "" && !root.endsWith("/") ? "/" : ""}${source}`.slice(0, 4_096);
297
+ const boundedString = (value) => typeof value === "string" && value.length > 0 ? value.slice(0, 4_096) : null;
298
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
299
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);