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.
Files changed (88) hide show
  1. package/README.md +14 -4
  2. package/bridge/ghidra/ReaGhidraBridge.java +477 -4
  3. package/bridge/hopper_bridge.py +54 -4
  4. package/dist/application/AnalysisContextQueries.js +130 -0
  5. package/dist/application/AnalysisSnapshotCache.js +4 -2
  6. package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
  7. package/dist/application/BinarySession.js +5 -4
  8. package/dist/application/BinarySessionComposition.js +21 -0
  9. package/dist/application/BinarySessionRecords.js +79 -9
  10. package/dist/application/CapabilityInventory.js +16 -2
  11. package/dist/application/ClientRegistrationStatus.js +4 -1
  12. package/dist/application/DirectAnalysis.js +6 -2
  13. package/dist/application/EnhancedTools.js +37 -7
  14. package/dist/application/EvidenceLedger.js +45 -16
  15. package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
  16. package/dist/application/LazyAnalysisProvider.js +77 -0
  17. package/dist/application/LoopbackReplay.js +1 -1
  18. package/dist/application/NativeApiInspection.js +80 -0
  19. package/dist/application/SessionProviderRouter.js +6 -6
  20. package/dist/application/SetupClientConfiguration.js +4 -2
  21. package/dist/application/runtime.js +26 -9
  22. package/dist/browser/PlaywrightBrowserScenarioProvider.js +8 -2
  23. package/dist/catalogIdentity.js +3 -3
  24. package/dist/cli/artifactCommands.js +5 -3
  25. package/dist/cli/coreAnalysisCommands.js +41 -0
  26. package/dist/cliCommandNames.js +1 -0
  27. package/dist/contracts/artifactToolContracts.js +8 -3
  28. package/dist/contracts/enhancedInputs.js +10 -0
  29. package/dist/contracts/functionWorkflowToolContracts.js +30 -0
  30. package/dist/contracts/promptContracts.js +14 -15
  31. package/dist/contracts/toolContractExamples.js +3 -0
  32. package/dist/contracts/toolContracts.js +21 -4
  33. package/dist/contracts/toolEffects.js +4 -0
  34. package/dist/contracts/toolOutputSchemaGroups.js +50 -10
  35. package/dist/domain/analysisErrorPresentation.js +8 -2
  36. package/dist/domain/analysisErrorProjection.js +1 -0
  37. package/dist/domain/bytecodeProvider.js +179 -0
  38. package/dist/domain/conformancePackage.js +169 -0
  39. package/dist/domain/conformanceReplay.js +104 -0
  40. package/dist/domain/conformanceTrustGate.js +193 -0
  41. package/dist/domain/customProtocolCapture.js +137 -0
  42. package/dist/domain/eventProcessTree.js +214 -0
  43. package/dist/domain/evidenceBundle.js +47 -0
  44. package/dist/domain/hopperStartupFailure.js +1 -1
  45. package/dist/domain/hopperValues.js +4 -1
  46. package/dist/domain/inputIssueProjection.js +7 -1
  47. package/dist/domain/javascriptSemanticAnalysis.js +8 -12
  48. package/dist/domain/javascriptSourceParser.js +15 -0
  49. package/dist/domain/javascriptStaticAnalysis.js +8 -12
  50. package/dist/domain/mobileApplicationInvestigation.js +159 -0
  51. package/dist/domain/nativeApiBoundary.js +130 -0
  52. package/dist/domain/objcSwiftMetadata.js +150 -0
  53. package/dist/domain/packageAnalysis.js +123 -0
  54. package/dist/domain/peInspection.js +196 -0
  55. package/dist/domain/protocolCapture.js +243 -0
  56. package/dist/dotnet/ManagedStaticProvider.js +34 -5
  57. package/dist/generatedMcpToolCatalog.js +37 -0
  58. package/dist/generatedPackageMetadata.js +4 -4
  59. package/dist/ghidra/GhidraDefaults.js +1 -1
  60. package/dist/ghidra/GhidraFunctionValues.js +27 -1
  61. package/dist/ghidra/GhidraProviderCapabilities.js +40 -38
  62. package/dist/hopper/HopperClient.js +26 -2
  63. package/dist/hopper/HopperProvider.js +42 -30
  64. package/dist/hopper/HopperRequestQueue.js +32 -4
  65. package/dist/hopper/HopperResponseStream.js +2 -2
  66. package/dist/hopper/protocol.js +63 -21
  67. package/dist/main/transport.js +2 -5
  68. package/dist/main.js +14 -12
  69. package/dist/mcpDoctor.js +2 -2
  70. package/dist/mcpStartupPolicy.js +14 -0
  71. package/dist/server/LazyToolCatalog.js +134 -0
  72. package/dist/server/createServer.js +64 -171
  73. package/dist/server/hydrateServerTools.js +191 -0
  74. package/dist/server/registerEnhancedTools.js +13 -3
  75. package/dist/server/registerEvidenceResources.js +50 -101
  76. package/dist/server/registerInvestigationTools.js +1 -2
  77. package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
  78. package/dist/server/registerReconstructionObligationLedgerResource.js +0 -7
  79. package/dist/server/registerReconstructionReadinessResource.js +0 -7
  80. package/dist/server/registerSessionRecordTools.js +82 -12
  81. package/dist/server/registerSessionStatusTool.js +1 -1
  82. package/dist/server/registerSessionTools.js +21 -1
  83. package/dist/server/toolRegistrationOptions.js +21 -6
  84. package/dist/server/toolResult.js +12 -0
  85. package/package.json +21 -10
  86. package/scripts/package-runner-bootstrap.mjs +72 -0
  87. package/scripts/rea.mjs +11 -1
  88. 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. Run rea doctor and retry the analysis.",
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 [{ path, reason: "invalid_value" }];
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
- let file;
12
- try {
13
- file = parse(source, {
14
- sourceType: "unambiguous",
15
- errorRecovery: true,
16
- plugins: ["jsx", "typescript"],
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
+ }