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.
- package/README.md +129 -45
- package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
- package/bridge/hopper_bridge.py +13 -3
- package/dist/application/AbortablePromise.js +25 -0
- package/dist/application/AnalysisProvider.js +3 -0
- package/dist/application/AnalysisProviderEvaluation.js +60 -0
- package/dist/application/AnalysisProviderRegistry.js +228 -0
- package/dist/application/AnalysisSnapshotCache.js +41 -21
- package/dist/application/AnalysisSnapshotFiles.js +5 -6
- package/dist/application/BinarySession.js +218 -101
- package/dist/application/CompositeProvider.js +20 -8
- package/dist/application/DirectAnalysis.js +49 -35
- package/dist/application/Doctor.js +54 -2
- package/dist/application/EnhancedTools.js +1 -1
- package/dist/application/InvestigationProviders.js +11 -0
- package/dist/application/JavaScriptArtifactAnalysis.js +299 -0
- package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
- package/dist/application/JavaScriptArtifactFiles.js +231 -0
- package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
- package/dist/application/JavaScriptArtifactGraphBuilder.js +101 -0
- package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
- package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
- package/dist/application/JavaScriptArtifactGraphEvidence.js +89 -0
- package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
- package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
- package/dist/application/JavaScriptArtifactReconstruction.js +74 -0
- package/dist/application/JavaScriptArtifactReconstructionInput.js +99 -0
- package/dist/application/MacHopper.js +20 -4
- package/dist/application/ProcessCaptureLifecycle.js +1 -1
- package/dist/application/ProcessHarness.js +1 -1
- package/dist/application/ProcessSampling.js +1 -1
- package/dist/application/SessionProviderRouter.js +219 -0
- package/dist/application/Setup.js +84 -60
- package/dist/application/SetupClients.js +1 -1
- package/dist/application/SetupPlan.js +9 -2
- package/dist/application/SupportedClients.js +40 -16
- package/dist/application/Upgrade.js +1 -1
- package/dist/application/runtime.js +9 -6
- package/dist/artifacts/AsarArtifactReader.js +23 -3
- package/dist/browser/CdpBrowserProvider.js +4 -2
- package/dist/browser/CdpElectronProvider.js +4 -2
- package/dist/browser/CdpEndpoint.js +14 -1
- package/dist/catalogIdentity.js +2 -41
- package/dist/cli.js +58 -34
- package/dist/cliBrowserAdvancedCommands.js +5 -4
- package/dist/cliBrowserCommands.js +5 -4
- package/dist/cliCommandNames.js +43 -0
- package/dist/cliElectronCommands.js +3 -2
- package/dist/cliEvidenceCommands.js +4 -3
- package/dist/cliInvestigationCommands.js +2 -1
- package/dist/cliPolicyCommands.js +2 -1
- package/dist/cliProcessCommands.js +3 -2
- package/dist/config.js +18 -2
- package/dist/contracts/browserToolContracts.js +5 -5
- package/dist/contracts/electronToolContracts.js +3 -3
- package/dist/contracts/enhancedInputs.js +2 -2
- package/dist/contracts/providerSelection.js +28 -0
- package/dist/contracts/sessionLifecycleInputs.js +2 -0
- package/dist/contracts/toolContracts.js +22 -20
- package/dist/contracts/toolOutputSchemas.js +117 -22
- package/dist/doctorRuntime.js +6 -0
- package/dist/domain/analysisProfile.js +78 -0
- package/dist/domain/analysisSnapshot.js +92 -46
- package/dist/domain/artifactGraph.js +1 -0
- package/dist/domain/binaryTarget.js +4 -29
- package/dist/domain/errors.js +81 -6
- package/dist/domain/evidence.js +44 -5
- package/dist/domain/evidenceBundle.js +9 -7
- package/dist/domain/functionComparison.js +9 -3
- package/dist/domain/functionComparisonNormalization.js +37 -0
- package/dist/domain/functionDossierEvidence.js +4 -1
- package/dist/domain/hopperValues.js +66 -9
- package/dist/domain/investigationWorkspace.js +2 -1
- package/dist/domain/javascriptApplicationEvidenceSchemas.js +361 -0
- package/dist/domain/javascriptApplicationGraph.js +281 -0
- package/dist/domain/javascriptApplicationGraphSchemas.js +132 -0
- package/dist/domain/javascriptAstFingerprint.js +143 -0
- package/dist/domain/javascriptStaticAnalysis.js +358 -0
- package/dist/domain/javascriptStaticAnalysisHelpers.js +264 -0
- package/dist/domain/javascriptStaticAnalysisState.js +17 -0
- package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
- package/dist/generatedPackageMetadata.js +1 -1
- package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
- package/dist/ghidra/GhidraClient.js +400 -0
- package/dist/ghidra/GhidraClientTypes.js +1 -0
- package/dist/ghidra/GhidraDefaults.js +20 -0
- package/dist/ghidra/GhidraDiagnostics.js +43 -0
- package/dist/ghidra/GhidraDoctor.js +59 -0
- package/dist/ghidra/GhidraFunctionValues.js +264 -0
- package/dist/ghidra/GhidraInstallation.js +239 -0
- package/dist/ghidra/GhidraInventoryValues.js +217 -0
- package/dist/ghidra/GhidraLauncher.js +139 -0
- package/dist/ghidra/GhidraProvider.js +360 -0
- package/dist/ghidra/GhidraRequestQueue.js +98 -0
- package/dist/ghidra/GhidraResponseBuffer.js +31 -0
- package/dist/ghidra/GhidraResponseRouter.js +24 -0
- package/dist/ghidra/GhidraSessionError.js +39 -0
- package/dist/ghidra/GhidraSessionValues.js +62 -0
- package/dist/ghidra/GhidraSocketConnection.js +46 -0
- package/dist/ghidra/protocol.js +69 -0
- package/dist/hopper/BridgeLauncher.js +19 -54
- package/dist/hopper/HopperAnalysisProfile.js +106 -0
- package/dist/hopper/HopperClient.js +154 -162
- package/dist/hopper/HopperProvider.js +99 -7
- package/dist/hopper/HopperSessionValues.js +27 -0
- package/dist/hopper/HopperSocketConnection.js +31 -0
- package/dist/native/NativeMacOSProvider.js +3 -1
- package/dist/process/PendingOperations.js +59 -0
- package/dist/process/PrivateRuntimeRoot.js +33 -0
- package/dist/process/ProviderDeadline.js +95 -0
- package/dist/process/ProviderProcess.js +275 -0
- package/dist/server/createServer.js +1 -0
- package/dist/server/promptCompletion.js +9 -3
- package/dist/server/registerEnhancedTools.js +9 -6
- package/dist/server/registerOfficialTools.js +3 -0
- package/dist/server/registerSessionTools.js +6 -1
- package/package.json +8 -6
- package/scripts/rea.mjs +35 -1
- /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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
:
|
|
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,
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|