rea-agents 2.5.0 → 3.0.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 +14 -4
- package/bridge/ghidra/ReaGhidraBridge.java +477 -4
- package/bridge/hopper_bridge.py +54 -4
- package/dist/application/AnalysisContextQueries.js +130 -0
- package/dist/application/AnalysisSnapshotCache.js +4 -2
- package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
- package/dist/application/BinarySession.js +5 -4
- package/dist/application/BinarySessionComposition.js +21 -0
- package/dist/application/BinarySessionRecords.js +79 -9
- package/dist/application/CapabilityInventory.js +16 -2
- package/dist/application/ClientRegistrationStatus.js +4 -1
- package/dist/application/DirectAnalysis.js +6 -2
- package/dist/application/EnhancedTools.js +37 -7
- package/dist/application/EvidenceLedger.js +45 -16
- package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
- package/dist/application/LazyAnalysisProvider.js +77 -0
- package/dist/application/LoopbackReplay.js +1 -1
- package/dist/application/NativeApiInspection.js +80 -0
- package/dist/application/SessionProviderRouter.js +6 -6
- package/dist/application/SetupClientConfiguration.js +4 -2
- package/dist/application/runtime.js +26 -9
- package/dist/browser/PlaywrightBrowserScenarioProvider.js +8 -2
- package/dist/catalogIdentity.js +3 -3
- package/dist/cli/artifactCommands.js +5 -3
- package/dist/cli/coreAnalysisCommands.js +41 -0
- package/dist/cliCommandNames.js +1 -0
- package/dist/contracts/artifactToolContracts.js +8 -3
- package/dist/contracts/enhancedInputs.js +10 -0
- package/dist/contracts/functionWorkflowToolContracts.js +30 -0
- package/dist/contracts/promptContracts.js +14 -15
- package/dist/contracts/toolContractExamples.js +3 -0
- package/dist/contracts/toolContracts.js +21 -4
- package/dist/contracts/toolEffects.js +4 -0
- package/dist/contracts/toolOutputSchemaGroups.js +50 -10
- package/dist/domain/analysisErrorPresentation.js +8 -2
- package/dist/domain/analysisErrorProjection.js +1 -0
- package/dist/domain/bytecodeProvider.js +179 -0
- package/dist/domain/conformancePackage.js +169 -0
- package/dist/domain/conformanceReplay.js +104 -0
- package/dist/domain/conformanceTrustGate.js +193 -0
- package/dist/domain/customProtocolCapture.js +137 -0
- package/dist/domain/eventProcessTree.js +214 -0
- package/dist/domain/evidenceBundle.js +47 -0
- package/dist/domain/hopperStartupFailure.js +1 -1
- package/dist/domain/hopperValues.js +4 -1
- package/dist/domain/inputIssueProjection.js +7 -1
- package/dist/domain/javascriptSemanticAnalysis.js +8 -12
- package/dist/domain/javascriptSourceParser.js +15 -0
- package/dist/domain/javascriptStaticAnalysis.js +8 -12
- package/dist/domain/mobileApplicationInvestigation.js +159 -0
- package/dist/domain/nativeApiBoundary.js +130 -0
- package/dist/domain/objcSwiftMetadata.js +150 -0
- package/dist/domain/packageAnalysis.js +123 -0
- package/dist/domain/peInspection.js +196 -0
- package/dist/domain/protocolCapture.js +243 -0
- package/dist/dotnet/ManagedStaticProvider.js +34 -5
- package/dist/generatedMcpToolCatalog.js +37 -0
- package/dist/generatedPackageMetadata.js +4 -4
- package/dist/ghidra/GhidraDefaults.js +1 -1
- package/dist/ghidra/GhidraFunctionValues.js +27 -1
- package/dist/ghidra/GhidraProviderCapabilities.js +40 -38
- package/dist/hopper/HopperClient.js +26 -2
- package/dist/hopper/HopperProvider.js +42 -30
- package/dist/hopper/HopperRequestQueue.js +32 -4
- package/dist/hopper/HopperResponseStream.js +2 -2
- package/dist/hopper/protocol.js +63 -21
- package/dist/main/transport.js +2 -5
- package/dist/main.js +14 -12
- package/dist/mcpDoctor.js +2 -2
- package/dist/mcpStartupPolicy.js +14 -0
- package/dist/server/LazyToolCatalog.js +134 -0
- package/dist/server/createServer.js +64 -171
- package/dist/server/hydrateServerTools.js +191 -0
- package/dist/server/registerEnhancedTools.js +13 -3
- package/dist/server/registerEvidenceResources.js +50 -101
- package/dist/server/registerInvestigationTools.js +1 -2
- package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
- package/dist/server/registerReconstructionObligationLedgerResource.js +0 -7
- package/dist/server/registerReconstructionReadinessResource.js +0 -7
- package/dist/server/registerSessionRecordTools.js +82 -12
- package/dist/server/registerSessionStatusTool.js +1 -1
- package/dist/server/registerSessionTools.js +21 -1
- package/dist/server/toolRegistrationOptions.js +21 -6
- package/dist/server/toolResult.js +12 -0
- package/package.json +21 -10
- package/scripts/package-runner-bootstrap.mjs +72 -0
- package/scripts/rea.mjs +11 -1
- package/skills/reverse-engineer-anything/SKILL.md +10 -3
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Event types for event-backed process-tree capture. */
|
|
3
|
+
export const processEventTypeSchema = z.enum([
|
|
4
|
+
"spawn",
|
|
5
|
+
"exit",
|
|
6
|
+
"re_exec",
|
|
7
|
+
"reparent",
|
|
8
|
+
"signal",
|
|
9
|
+
"name_change",
|
|
10
|
+
"thread_create",
|
|
11
|
+
"thread_exit",
|
|
12
|
+
]);
|
|
13
|
+
/** A single event-backed process-tree event. */
|
|
14
|
+
export const processTreeEventSchema = z.strictObject({
|
|
15
|
+
/** Monotonic event sequence number. */
|
|
16
|
+
sequence: z.number().int().nonnegative(),
|
|
17
|
+
/** Event timestamp in milliseconds. */
|
|
18
|
+
at_ms: z.number().int().nonnegative(),
|
|
19
|
+
/** Event type. */
|
|
20
|
+
type: processEventTypeSchema,
|
|
21
|
+
/** Process ID. */
|
|
22
|
+
pid: z.number().int().positive(),
|
|
23
|
+
/** Parent process ID. */
|
|
24
|
+
ppid: z.number().int().nullable(),
|
|
25
|
+
/** Process name if known. */
|
|
26
|
+
process_name: z.string().nullable(),
|
|
27
|
+
/** Executable path if known. */
|
|
28
|
+
executable: z.string().nullable(),
|
|
29
|
+
/** Command line arguments. */
|
|
30
|
+
arguments: z.array(z.string()).default([]),
|
|
31
|
+
/** Previous PID (for reparent events). */
|
|
32
|
+
previous_pid: z.number().int().nullable(),
|
|
33
|
+
/** Previous PPID (for reparent events). */
|
|
34
|
+
previous_ppid: z.number().int().nullable(),
|
|
35
|
+
/** Exit code (for exit events). */
|
|
36
|
+
exit_code: z.number().int().nullable(),
|
|
37
|
+
/** Signal number (for signal events). */
|
|
38
|
+
signal: z.number().int().nullable(),
|
|
39
|
+
/** Thread ID (for thread events). */
|
|
40
|
+
tid: z.number().int().nullable(),
|
|
41
|
+
});
|
|
42
|
+
/** Process node in a reconstructed tree. */
|
|
43
|
+
export const processNodeSchema = z.strictObject({
|
|
44
|
+
pid: z.number().int().positive(),
|
|
45
|
+
ppid: z.number().int().nullable(),
|
|
46
|
+
process_name: z.string().nullable(),
|
|
47
|
+
executable: z.string().nullable(),
|
|
48
|
+
arguments: z.array(z.string()).default([]),
|
|
49
|
+
children: z.array(z.number().int()).default([]),
|
|
50
|
+
spawn_time_ms: z.number().int().nullable(),
|
|
51
|
+
exit_time_ms: z.number().int().nullable(),
|
|
52
|
+
exit_code: z.number().int().nullable(),
|
|
53
|
+
is_re_exec: z.boolean().default(false),
|
|
54
|
+
is_reparented: z.boolean().default(false),
|
|
55
|
+
previous_ppid: z.number().int().nullable(),
|
|
56
|
+
});
|
|
57
|
+
/** Result of event-backed process-tree reconstruction. */
|
|
58
|
+
export const processTreeReconstructionSchema = z.strictObject({
|
|
59
|
+
/** Root PID of the process tree. */
|
|
60
|
+
root_pid: z.number().int().positive(),
|
|
61
|
+
/** All reconstructed process nodes. */
|
|
62
|
+
processes: z.array(processNodeSchema).min(0).max(10_000),
|
|
63
|
+
/** Events that were consumed. */
|
|
64
|
+
events_consumed: z.number().int().nonnegative(),
|
|
65
|
+
/** Events that could not be matched. */
|
|
66
|
+
events_unmatched: z.number().int().nonnegative(),
|
|
67
|
+
/** Short-lived descendants that were missed by sampling. */
|
|
68
|
+
short_lived_descendants: z.number().int().nonnegative(),
|
|
69
|
+
/** Whether any re-exec was detected. */
|
|
70
|
+
has_re_exec: z.boolean().default(false),
|
|
71
|
+
/** Whether any reparenting was detected. */
|
|
72
|
+
has_reparenting: z.boolean().default(false),
|
|
73
|
+
});
|
|
74
|
+
/** Reconstruct a process tree from event-backed capture. */
|
|
75
|
+
export function reconstructProcessTree(events, rootPid) {
|
|
76
|
+
const nodes = new Map();
|
|
77
|
+
const children = new Map();
|
|
78
|
+
let eventsConsumed = 0;
|
|
79
|
+
let eventsUnmatched = 0;
|
|
80
|
+
let hasReExec = false;
|
|
81
|
+
let hasReparenting = false;
|
|
82
|
+
let shortLivedDescendants = 0;
|
|
83
|
+
const sorted = [...events].sort((a, b) => a.at_ms - b.at_ms);
|
|
84
|
+
for (const event of sorted) {
|
|
85
|
+
eventsConsumed++;
|
|
86
|
+
let node = nodes.get(event.pid);
|
|
87
|
+
switch (event.type) {
|
|
88
|
+
case "spawn": {
|
|
89
|
+
if (!node) {
|
|
90
|
+
node = {
|
|
91
|
+
pid: event.pid,
|
|
92
|
+
ppid: event.ppid,
|
|
93
|
+
process_name: event.process_name,
|
|
94
|
+
executable: event.executable,
|
|
95
|
+
arguments: event.arguments,
|
|
96
|
+
children: [],
|
|
97
|
+
spawn_time_ms: event.at_ms,
|
|
98
|
+
exit_time_ms: null,
|
|
99
|
+
exit_code: null,
|
|
100
|
+
is_re_exec: false,
|
|
101
|
+
is_reparented: false,
|
|
102
|
+
previous_ppid: null,
|
|
103
|
+
};
|
|
104
|
+
nodes.set(event.pid, node);
|
|
105
|
+
}
|
|
106
|
+
if (event.ppid !== null) {
|
|
107
|
+
const parentChildren = children.get(event.ppid) ?? [];
|
|
108
|
+
parentChildren.push(event.pid);
|
|
109
|
+
children.set(event.ppid, parentChildren);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "exit": {
|
|
114
|
+
if (node) {
|
|
115
|
+
node.exit_time_ms = event.at_ms;
|
|
116
|
+
node.exit_code = event.exit_code;
|
|
117
|
+
if (node.spawn_time_ms !== null &&
|
|
118
|
+
event.at_ms - node.spawn_time_ms < 100)
|
|
119
|
+
shortLivedDescendants++;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
eventsUnmatched++;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "re_exec": {
|
|
127
|
+
if (node) {
|
|
128
|
+
node.is_re_exec = true;
|
|
129
|
+
node.executable = event.executable ?? node.executable;
|
|
130
|
+
node.arguments = event.arguments;
|
|
131
|
+
hasReExec = true;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
eventsUnmatched++;
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "reparent": {
|
|
139
|
+
if (node) {
|
|
140
|
+
node.previous_ppid = node.ppid;
|
|
141
|
+
node.ppid = event.ppid;
|
|
142
|
+
node.is_reparented = true;
|
|
143
|
+
hasReparenting = true;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
eventsUnmatched++;
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "signal":
|
|
151
|
+
case "name_change":
|
|
152
|
+
case "thread_create":
|
|
153
|
+
case "thread_exit":
|
|
154
|
+
// These events are recorded but don't modify the tree structure
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Attach children to nodes
|
|
159
|
+
for (const [pid, childPids] of children) {
|
|
160
|
+
const node = nodes.get(pid);
|
|
161
|
+
if (node)
|
|
162
|
+
node.children = childPids;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
root_pid: rootPid,
|
|
166
|
+
processes: [...nodes.values()],
|
|
167
|
+
events_consumed: eventsConsumed,
|
|
168
|
+
events_unmatched: eventsUnmatched,
|
|
169
|
+
short_lived_descendants: shortLivedDescendants,
|
|
170
|
+
has_re_exec: hasReExec,
|
|
171
|
+
has_reparenting: hasReparenting,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Get all descendant PIDs of a given root PID. */
|
|
175
|
+
export function getDescendants(processes, rootPid) {
|
|
176
|
+
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
177
|
+
const visited = new Set();
|
|
178
|
+
const result = [];
|
|
179
|
+
const queue = [rootPid];
|
|
180
|
+
while (queue.length > 0) {
|
|
181
|
+
const pid = queue.shift();
|
|
182
|
+
if (visited.has(pid))
|
|
183
|
+
continue;
|
|
184
|
+
visited.add(pid);
|
|
185
|
+
const node = byPid.get(pid);
|
|
186
|
+
if (node) {
|
|
187
|
+
for (const child of node.children) {
|
|
188
|
+
if (!visited.has(child)) {
|
|
189
|
+
result.push(child);
|
|
190
|
+
queue.push(child);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
/** Check if a process tree contains a specific PID. */
|
|
198
|
+
export function containsProcess(processes, pid) {
|
|
199
|
+
return processes.some((p) => p.pid === pid);
|
|
200
|
+
}
|
|
201
|
+
/** Get the maximum tree depth from the root. */
|
|
202
|
+
export function maxTreeDepth(processes, rootPid) {
|
|
203
|
+
const byPid = new Map(processes.map((p) => [p.pid, p]));
|
|
204
|
+
function depth(pid, visited) {
|
|
205
|
+
if (visited.has(pid))
|
|
206
|
+
return 0;
|
|
207
|
+
visited.add(pid);
|
|
208
|
+
const node = byPid.get(pid);
|
|
209
|
+
if (!node || node.children.length === 0)
|
|
210
|
+
return 0;
|
|
211
|
+
return 1 + Math.max(...node.children.map((c) => depth(c, visited)));
|
|
212
|
+
}
|
|
213
|
+
return depth(rootPid, new Set());
|
|
214
|
+
}
|
|
@@ -163,6 +163,53 @@ const validateVerifiedResolution = (unknown, evidenceById) => {
|
|
|
163
163
|
if (qualifies !== true)
|
|
164
164
|
throw new TypeError(`Residual unknown ${unknown.unknown_id} has no qualifying resolution evidence`);
|
|
165
165
|
};
|
|
166
|
+
/**
|
|
167
|
+
* Validate one new residual-unknown revision against an already valid ledger.
|
|
168
|
+
* This keeps ordinary mutations proportional to the affected revision while
|
|
169
|
+
* full bundle imports continue to validate the complete graph.
|
|
170
|
+
*/
|
|
171
|
+
export const validateResidualUnknownAddition = (unknown, evidenceById, currentHeads) => {
|
|
172
|
+
const current = currentHeads.get(unknown.unknown_id);
|
|
173
|
+
if (current === undefined
|
|
174
|
+
? unknown.revision !== 1 || unknown.previous_revision_digest !== null
|
|
175
|
+
: unknown.revision !== current.revision + 1 ||
|
|
176
|
+
unknown.previous_revision_digest !== current.revision_digest)
|
|
177
|
+
throw new TypeError(`Residual unknown ${unknown.unknown_id} revision chain is broken`);
|
|
178
|
+
for (const evidenceId of referencedEvidenceIds(unknown))
|
|
179
|
+
if (!evidenceById.has(evidenceId))
|
|
180
|
+
throw new TypeError(`Residual unknown references missing evidence ${evidenceId}`);
|
|
181
|
+
for (const relationship of unknown.relationships)
|
|
182
|
+
if (relationship.unknown_id !== unknown.unknown_id &&
|
|
183
|
+
!currentHeads.has(relationship.unknown_id))
|
|
184
|
+
throw new TypeError(`Residual unknown references missing unknown ${relationship.unknown_id}`);
|
|
185
|
+
if (unknown.resolution?.disposition === "verified")
|
|
186
|
+
validateVerifiedResolution(unknown, evidenceById);
|
|
187
|
+
for (const relationship of unknown.relationships)
|
|
188
|
+
if (relationship.type === "depends-on" &&
|
|
189
|
+
reachesUnknown(relationship.unknown_id, unknown.unknown_id, currentHeads, unknown))
|
|
190
|
+
throw new TypeError("Residual unknown dependency graph contains a cycle");
|
|
191
|
+
};
|
|
192
|
+
const reachesUnknown = (start, goal, currentHeads, candidate) => {
|
|
193
|
+
const pending = [start];
|
|
194
|
+
const visited = new Set();
|
|
195
|
+
while (pending.length > 0) {
|
|
196
|
+
const id = pending.pop();
|
|
197
|
+
if (id === undefined)
|
|
198
|
+
break;
|
|
199
|
+
if (id === goal)
|
|
200
|
+
return true;
|
|
201
|
+
if (visited.has(id))
|
|
202
|
+
continue;
|
|
203
|
+
visited.add(id);
|
|
204
|
+
const unknown = id === candidate.unknown_id ? candidate : currentHeads.get(id);
|
|
205
|
+
if (unknown === undefined)
|
|
206
|
+
continue;
|
|
207
|
+
for (const relationship of unknown.relationships)
|
|
208
|
+
if (relationship.type === "depends-on")
|
|
209
|
+
pending.push(relationship.unknown_id);
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
};
|
|
166
213
|
const evidenceQualifies = (unknown, evidence) => {
|
|
167
214
|
if (unknown.required_authority !== null &&
|
|
168
215
|
evidence.authority !== unknown.required_authority)
|
|
@@ -27,7 +27,7 @@ const failures = [
|
|
|
27
27
|
[
|
|
28
28
|
75,
|
|
29
29
|
"hopper_exited_during_startup",
|
|
30
|
-
"Hopper exited before the REA bridge was ready.
|
|
30
|
+
"Hopper exited before the REA bridge was ready. In CI, run rea doctor --provider hopper --detail full --json; if Hopper showed a UI or license prompt, complete it in the runner user session, otherwise close stale Hopper sessions and retry.",
|
|
31
31
|
],
|
|
32
32
|
[
|
|
33
33
|
76,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { jsonValueSchema } from "./jsonValue.js";
|
|
2
3
|
import { AnalysisOutputError, HopperProtocolError } from "./errors.js";
|
|
3
4
|
import { err, ok } from "./result.js";
|
|
5
|
+
import { nativeApiBoundarySchema } from "./nativeApiBoundary.js";
|
|
4
6
|
/** Return a bounded byte distance between canonical addresses in one space. */
|
|
5
7
|
export const addressDistance = (start, end) => {
|
|
6
8
|
const left = addressCoordinate(start);
|
|
@@ -236,6 +238,7 @@ export const functionDossierSchema = z
|
|
|
236
238
|
instruction_scan: z
|
|
237
239
|
.object({ scanned: z.number().int().min(0), truncated: z.boolean() })
|
|
238
240
|
.strict(),
|
|
241
|
+
native_api: nativeApiBoundarySchema.optional(),
|
|
239
242
|
limitations: z.array(z.string()).default([]),
|
|
240
243
|
})
|
|
241
244
|
.strict();
|
|
@@ -243,7 +246,7 @@ export const functionDossierSchema = z
|
|
|
243
246
|
export const parseFunctionDossier = (value) => {
|
|
244
247
|
const parsed = functionDossierSchema.safeParse(value);
|
|
245
248
|
return parsed.success
|
|
246
|
-
? ok(parsed.data)
|
|
249
|
+
? ok(jsonValueSchema.parse(parsed.data))
|
|
247
250
|
: err(new AnalysisOutputError("analyze_function", "provider output did not match the dossier contract", { cause: parsed.error }));
|
|
248
251
|
};
|
|
249
252
|
/** Parse page entries and continuation metadata returned by list operations. */
|
|
@@ -31,7 +31,13 @@ const projectIssue = (issue, input) => {
|
|
|
31
31
|
expected: issue.values.filter(isSafeExpected),
|
|
32
32
|
},
|
|
33
33
|
];
|
|
34
|
-
return [
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
path,
|
|
37
|
+
reason: "invalid_value",
|
|
38
|
+
...(issue.message.length > 0 ? { message: issue.message } : {}),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
35
41
|
};
|
|
36
42
|
const valueAtPath = (input, path) => {
|
|
37
43
|
let current = input;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parse } from "@babel/parser";
|
|
2
1
|
import * as t from "@babel/types";
|
|
3
2
|
import { DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, failedJavaScriptSemanticIr, } from "./javascriptSemanticIr.js";
|
|
4
3
|
import { collectSemanticModuleLink, collectSemanticCallable, collectSemanticReferences, immutableSemanticBindings, immutableSemanticScopes, reachSemanticLimit, semanticModuleOrigin, semanticStaticPropertyName, } from "./javascriptSemanticProjection.js";
|
|
@@ -7,20 +6,17 @@ import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
|
7
6
|
import { collectSemanticReturns, resolveSemanticModuleCallables, } from "./javascriptSemanticReturns.js";
|
|
8
7
|
import { collectJavaScriptDerivedSemantics } from "./javascriptSemanticDerivedAnalysis.js";
|
|
9
8
|
import { compareCodePoints, propertyName, range, } from "./javascriptStaticAnalysisHelpers.js";
|
|
9
|
+
import { parseJavaScriptSource, } from "./javascriptSourceParser.js";
|
|
10
10
|
/** Recover bounded lexical bindings, aliases, constants, and module links. */
|
|
11
11
|
export const analyzeJavaScriptSemantics = (source, inputLimits = {}) => {
|
|
12
|
+
const file = parseJavaScriptSource(source);
|
|
13
|
+
return file === null
|
|
14
|
+
? failedJavaScriptSemanticIr()
|
|
15
|
+
: analyzeParsedJavaScriptSemantics(file, inputLimits);
|
|
16
|
+
};
|
|
17
|
+
/** Recover semantics from an already parsed JavaScript artifact. */
|
|
18
|
+
export const analyzeParsedJavaScriptSemantics = (file, inputLimits = {}) => {
|
|
12
19
|
const limits = { ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, ...inputLimits };
|
|
13
|
-
let file;
|
|
14
|
-
try {
|
|
15
|
-
file = parse(source, {
|
|
16
|
-
sourceType: "unambiguous",
|
|
17
|
-
errorRecovery: true,
|
|
18
|
-
plugins: ["jsx", "typescript"],
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return failedJavaScriptSemanticIr();
|
|
23
|
-
}
|
|
24
20
|
const state = createState(file.program, limits);
|
|
25
21
|
collectDefinitions(file.program, state);
|
|
26
22
|
const references = collectSemanticReferences(file.program, state);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
/** Parse JavaScript or TypeScript once without retaining unused comment nodes. */
|
|
3
|
+
export const parseJavaScriptSource = (source) => {
|
|
4
|
+
try {
|
|
5
|
+
return parse(source, {
|
|
6
|
+
sourceType: "unambiguous",
|
|
7
|
+
errorRecovery: true,
|
|
8
|
+
attachComment: false,
|
|
9
|
+
plugins: ["jsx", "typescript"],
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parse } from "@babel/parser";
|
|
2
1
|
import * as t from "@babel/types";
|
|
3
2
|
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
|
|
4
3
|
import { detectVendors, failedJavaScriptStaticAnalysis, registrationKey, sortedUnique, stringValue, } from "./javascriptStaticAnalysisHelpers.js";
|
|
@@ -6,19 +5,16 @@ import { addReference, addSourceMapDirectives, inspectCall, inspectRouteProperty
|
|
|
6
5
|
import { inspectBundlerRegistration, inspectEsbuildWrapper, } from "./javascriptStaticAnalysisBundler.js";
|
|
7
6
|
import { finalizeLocatedFindings } from "./javascriptStaticAnalysisFindings.js";
|
|
8
7
|
import { createJavaScriptAnalysisAccumulator, } from "./javascriptStaticAnalysisState.js";
|
|
8
|
+
import { parseJavaScriptSource, } from "./javascriptSourceParser.js";
|
|
9
9
|
/** Parse one bounded JavaScript artifact and recover static structure only. */
|
|
10
10
|
export const analyzeJavaScriptStaticSource = (source, limits) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return failedJavaScriptStaticAnalysis();
|
|
21
|
-
}
|
|
11
|
+
const file = parseJavaScriptSource(source);
|
|
12
|
+
return file === null
|
|
13
|
+
? failedJavaScriptStaticAnalysis()
|
|
14
|
+
: analyzeParsedJavaScriptStaticSource(source, file, limits);
|
|
15
|
+
};
|
|
16
|
+
/** Recover static structure from an already parsed JavaScript artifact. */
|
|
17
|
+
export const analyzeParsedJavaScriptStaticSource = (source, file, limits) => {
|
|
22
18
|
const accumulator = createJavaScriptAnalysisAccumulator();
|
|
23
19
|
traverseStaticSource(source, file, accumulator, limits);
|
|
24
20
|
addSourceMapDirectives(source, accumulator, limits.maxFindings);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Supported mobile artifact formats. */
|
|
3
|
+
export const mobileArtifactFormatSchema = z.enum([
|
|
4
|
+
"apk",
|
|
5
|
+
"aab",
|
|
6
|
+
"ipa",
|
|
7
|
+
"dex",
|
|
8
|
+
"native_lib",
|
|
9
|
+
]);
|
|
10
|
+
/** Mobile platform. */
|
|
11
|
+
export const mobilePlatformSchema = z.enum([
|
|
12
|
+
"android",
|
|
13
|
+
"ios",
|
|
14
|
+
"cross_platform",
|
|
15
|
+
]);
|
|
16
|
+
/** Android manifest entry. */
|
|
17
|
+
export const androidManifestSchema = z.strictObject({
|
|
18
|
+
package_name: z.string().min(1),
|
|
19
|
+
version_name: z.string().nullable(),
|
|
20
|
+
version_code: z.number().int().nullable(),
|
|
21
|
+
min_sdk: z.number().int().nullable(),
|
|
22
|
+
target_sdk: z.number().int().nullable(),
|
|
23
|
+
permissions: z.array(z.string()).default([]),
|
|
24
|
+
activities: z.array(z.string()).default([]),
|
|
25
|
+
services: z.array(z.string()).default([]),
|
|
26
|
+
receivers: z.array(z.string()).default([]),
|
|
27
|
+
providers: z.array(z.string()).default([]),
|
|
28
|
+
});
|
|
29
|
+
/** iOS app manifest (Info.plist). */
|
|
30
|
+
export const iosManifestSchema = z.strictObject({
|
|
31
|
+
bundle_identifier: z.string().min(1),
|
|
32
|
+
bundle_version: z.string().nullable(),
|
|
33
|
+
short_version: z.string().nullable(),
|
|
34
|
+
minimum_os_version: z.string().nullable(),
|
|
35
|
+
supported_platforms: z.array(z.string()).default([]),
|
|
36
|
+
ui_required_device_capabilities: z.array(z.string()).default([]),
|
|
37
|
+
app_permissions: z.array(z.string()).default([]),
|
|
38
|
+
});
|
|
39
|
+
/** Signing information. */
|
|
40
|
+
export const signingInfoSchema = z.strictObject({
|
|
41
|
+
is_signed: z.boolean().default(false),
|
|
42
|
+
signer_identity: z.string().nullable(),
|
|
43
|
+
certificate_fingerprint: z.string().nullable(),
|
|
44
|
+
signature_scheme: z.string().nullable(),
|
|
45
|
+
is_debuggable: z.boolean().default(false),
|
|
46
|
+
});
|
|
47
|
+
/** Native library entry. */
|
|
48
|
+
export const nativeLibrarySchema = z.strictObject({
|
|
49
|
+
path: z.string().min(1),
|
|
50
|
+
architecture: z.string().nullable(),
|
|
51
|
+
size: z.number().int().nonnegative(),
|
|
52
|
+
digest: z
|
|
53
|
+
.string()
|
|
54
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
55
|
+
.nullable(),
|
|
56
|
+
});
|
|
57
|
+
/** Resource entry within a mobile artifact. */
|
|
58
|
+
export const mobileResourceSchema = z.strictObject({
|
|
59
|
+
path: z.string().min(1),
|
|
60
|
+
type: z.enum([
|
|
61
|
+
"layout",
|
|
62
|
+
"drawable",
|
|
63
|
+
"string",
|
|
64
|
+
"asset",
|
|
65
|
+
"raw",
|
|
66
|
+
"native_lib",
|
|
67
|
+
"manifest",
|
|
68
|
+
"entitlements",
|
|
69
|
+
"provisioning",
|
|
70
|
+
"other",
|
|
71
|
+
]),
|
|
72
|
+
size: z.number().int().nonnegative(),
|
|
73
|
+
});
|
|
74
|
+
/** Static analysis result for a mobile artifact. */
|
|
75
|
+
export const mobileArtifactManifestSchema = z.strictObject({
|
|
76
|
+
format: mobileArtifactFormatSchema,
|
|
77
|
+
platform: mobilePlatformSchema,
|
|
78
|
+
app_name: z.string().nullable(),
|
|
79
|
+
android_manifest: androidManifestSchema.nullable(),
|
|
80
|
+
ios_manifest: iosManifestSchema.nullable(),
|
|
81
|
+
signing: signingInfoSchema,
|
|
82
|
+
resources: z.array(mobileResourceSchema).default([]),
|
|
83
|
+
native_libraries: z.array(nativeLibrarySchema).default([]),
|
|
84
|
+
total_size: z.number().int().nonnegative(),
|
|
85
|
+
architectures: z.array(z.string()).default([]),
|
|
86
|
+
});
|
|
87
|
+
/** Detect mobile artifact format from file extension. */
|
|
88
|
+
export function detectMobileFormat(path) {
|
|
89
|
+
const lower = path.toLowerCase();
|
|
90
|
+
if (lower.endsWith(".apk"))
|
|
91
|
+
return "apk";
|
|
92
|
+
if (lower.endsWith(".aab"))
|
|
93
|
+
return "aab";
|
|
94
|
+
if (lower.endsWith(".ipa"))
|
|
95
|
+
return "ipa";
|
|
96
|
+
if (lower.endsWith(".dex"))
|
|
97
|
+
return "dex";
|
|
98
|
+
if (lower.endsWith(".so") || lower.endsWith(".dylib") || lower.endsWith(".a"))
|
|
99
|
+
return "native_lib";
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/** Detect platform from format. */
|
|
103
|
+
export function detectPlatform(format) {
|
|
104
|
+
switch (format) {
|
|
105
|
+
case "apk":
|
|
106
|
+
case "aab":
|
|
107
|
+
case "dex":
|
|
108
|
+
return "android";
|
|
109
|
+
case "ipa":
|
|
110
|
+
return "ios";
|
|
111
|
+
case "native_lib":
|
|
112
|
+
return "cross_platform";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Classify a resource by its path. */
|
|
116
|
+
export function classifyMobileResource(path) {
|
|
117
|
+
const lower = path.toLowerCase();
|
|
118
|
+
if (lower.includes("androidmanifest.xml"))
|
|
119
|
+
return "manifest";
|
|
120
|
+
if (lower.includes(".entitlements"))
|
|
121
|
+
return "entitlements";
|
|
122
|
+
if (lower.includes("provisioning") || lower.includes(".mobileprovision"))
|
|
123
|
+
return "provisioning";
|
|
124
|
+
if (lower.includes("/layout/"))
|
|
125
|
+
return "layout";
|
|
126
|
+
if (lower.includes("/drawable/") || lower.includes("/mipmap/"))
|
|
127
|
+
return "drawable";
|
|
128
|
+
if (lower.includes("/values/"))
|
|
129
|
+
return "string";
|
|
130
|
+
if (lower.includes("assets/") || lower.includes("raw/"))
|
|
131
|
+
return "asset";
|
|
132
|
+
if (lower.endsWith(".so") || lower.endsWith(".dylib"))
|
|
133
|
+
return "native_lib";
|
|
134
|
+
if (lower.endsWith(".xml"))
|
|
135
|
+
return "layout";
|
|
136
|
+
return "other";
|
|
137
|
+
}
|
|
138
|
+
/** Get all native libraries for a specific architecture. */
|
|
139
|
+
export function nativeLibsByArchitecture(manifest, arch) {
|
|
140
|
+
return manifest.native_libraries.filter((l) => l.architecture === arch);
|
|
141
|
+
}
|
|
142
|
+
/** Check if an Android artifact has internet permission. */
|
|
143
|
+
export function hasInternetPermission(manifest) {
|
|
144
|
+
if (!manifest.android_manifest)
|
|
145
|
+
return false;
|
|
146
|
+
return manifest.android_manifest.permissions.some((p) => p.includes("INTERNET") || p.includes("android.permission.INTERNET"));
|
|
147
|
+
}
|
|
148
|
+
/** Get all unique architectures across native libraries. */
|
|
149
|
+
export function getAllArchitectures(manifest) {
|
|
150
|
+
const archs = new Set();
|
|
151
|
+
for (const lib of manifest.native_libraries) {
|
|
152
|
+
if (lib.architecture)
|
|
153
|
+
archs.add(lib.architecture);
|
|
154
|
+
}
|
|
155
|
+
for (const arch of manifest.architectures) {
|
|
156
|
+
archs.add(arch);
|
|
157
|
+
}
|
|
158
|
+
return [...archs];
|
|
159
|
+
}
|