rea-agents 0.2.1 → 0.4.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 +124 -20
- package/bridge/hopper_bridge.py +141 -13
- package/dist/application/AnalysisProvider.js +12 -1
- package/dist/application/ArtifactExtraction.js +166 -0
- package/dist/application/ArtifactGraphConstruction.js +257 -0
- package/dist/application/ArtifactInventory.js +253 -0
- package/dist/application/BinarySession.js +164 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +27 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +8 -7
- package/dist/application/EvidenceBundleCommands.js +29 -0
- package/dist/application/EvidenceBundleFiles.js +127 -0
- package/dist/application/EvidenceLedger.js +225 -18
- package/dist/application/FilesystemSnapshot.js +124 -0
- package/dist/application/LinuxHopper.js +186 -0
- package/dist/application/LoopbackReplay.js +195 -37
- package/dist/application/ProcessHarness.js +200 -191
- package/dist/application/ProcessNormalization.js +44 -0
- package/dist/application/ProcessOwnership.js +84 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +105 -0
- package/dist/application/ReferenceSourceImport.js +182 -0
- package/dist/application/ReferenceSourceImportEntries.js +122 -0
- package/dist/application/ReferenceSourceImportPolicy.js +73 -0
- package/dist/application/ReferenceSourceImportTypes.js +18 -0
- package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
- package/dist/application/Setup.js +192 -41
- package/dist/application/Uninstall.js +130 -0
- package/dist/application/runtime.js +8 -1
- package/dist/artifacts/ArtifactPaths.js +51 -0
- package/dist/artifacts/ArtifactProvider.js +130 -0
- package/dist/artifacts/ArtifactReader.js +9 -0
- package/dist/artifacts/AsarArtifactReader.js +62 -0
- package/dist/artifacts/DirectoryArtifactReader.js +107 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
- package/dist/artifacts/SafeOutputTree.js +199 -0
- package/dist/artifacts/StreamBytes.js +10 -0
- package/dist/artifacts/ZipArtifactReader.js +109 -0
- package/dist/cli.js +153 -6
- package/dist/config.js +35 -1
- package/dist/contracts/artifactComparisonExample.js +95 -0
- package/dist/contracts/artifactToolContracts.js +84 -0
- package/dist/contracts/enhancedInputs.js +4 -0
- package/dist/contracts/functionComparisonExample.js +57 -0
- package/dist/contracts/investigationExamples.js +118 -0
- package/dist/contracts/nativeToolContracts.js +52 -0
- package/dist/contracts/processCaptureExample.js +25 -0
- package/dist/contracts/toolContractExamples.js +69 -0
- package/dist/contracts/toolContracts.js +99 -36
- package/dist/contracts/toolOutputSchemas.js +154 -82
- package/dist/contracts/unknownContractExamples.js +33 -0
- package/dist/domain/artifactComparison.js +273 -0
- package/dist/domain/artifactGraph.js +194 -0
- package/dist/domain/artifactInventoryEvidence.js +150 -0
- package/dist/domain/binaryTarget.js +55 -0
- package/dist/domain/bundleComparison.js +266 -0
- package/dist/domain/callPath.js +346 -0
- package/dist/domain/changedBehavior.js +294 -0
- package/dist/domain/errors.js +192 -4
- package/dist/domain/evidence.js +41 -10
- package/dist/domain/evidenceBundle.js +187 -6
- package/dist/domain/functionComparison.js +201 -0
- package/dist/domain/functionComparisonNormalization.js +112 -0
- package/dist/domain/functionComparisonResults.js +54 -0
- package/dist/domain/functionComparisonSchemas.js +82 -0
- package/dist/domain/functionDossierEvidence.js +171 -0
- package/dist/domain/hopperValues.js +14 -7
- package/dist/domain/nativeInspection.js +142 -0
- package/dist/domain/processCapture.js +152 -54
- package/dist/domain/processComparison.js +106 -0
- package/dist/domain/reconstructionUnknowns.js +90 -0
- package/dist/domain/reconstructionVerification.js +285 -0
- package/dist/domain/reconstructionVerificationSchemas.js +126 -0
- package/dist/domain/referenceSourceClassification.js +496 -0
- package/dist/domain/referenceSourceGraph.js +376 -0
- package/dist/domain/referenceSourceImportParsing.js +235 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +40 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/identity.js +1 -0
- package/dist/main.js +5 -1
- package/dist/native/CommandRunner.js +156 -0
- package/dist/native/NativeMacOSProvider.js +306 -0
- package/dist/native/NativeMachoInspection.js +135 -0
- package/dist/native/parsers/codesign.js +55 -0
- package/dist/native/parsers/demangle.js +26 -0
- package/dist/native/parsers/dyldInfo.js +25 -0
- package/dist/native/parsers/lipo.js +67 -0
- package/dist/native/parsers/otool.js +193 -0
- package/dist/native/parsers/plist.js +23 -0
- package/dist/reference/ReferenceSourceReader.js +73 -0
- package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
- package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
- package/dist/reference/ReferenceSourceReaderFile.js +119 -0
- package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
- package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
- package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
- package/dist/server/createServer.js +31 -5
- package/dist/server/recordDerivedEvidence.js +10 -0
- package/dist/server/registerArtifactComparisonTool.js +62 -0
- package/dist/server/registerArtifactTools.js +6 -0
- package/dist/server/registerBundleComparisonTool.js +47 -0
- package/dist/server/registerEnhancedTools.js +64 -12
- package/dist/server/registerEvidenceTools.js +36 -0
- package/dist/server/registerFunctionComparisonTool.js +68 -0
- package/dist/server/registerInvestigationTools.js +224 -0
- package/dist/server/registerNativeTools.js +6 -0
- package/dist/server/registerOfficialTools.js +47 -14
- package/dist/server/registerProcessComparisonTool.js +106 -0
- package/dist/server/registerSessionTools.js +179 -70
- package/dist/server/sessionEvidence.js +28 -0
- package/dist/server/sessionToolPolicies.js +64 -0
- package/dist/server/toolRegistrationOptions.js +7 -0
- package/dist/server/toolResult.js +8 -5
- package/install.sh +198 -0
- package/package.json +19 -2
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
- /package/dist/{application/HopperToolPort.js → domain/referenceSourcePolicy.js} +0 -0
package/README.md
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/rea-agents)
|
|
12
12
|
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
|
13
|
-
[](#68-tools-for-investigation)
|
|
14
14
|
[](https://nodejs.org/)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
|
|
17
|
-
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [
|
|
17
|
+
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [68 tools](#68-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
|
18
18
|
|
|
19
19
|
<br />
|
|
20
20
|
|
|
21
|
-
<code>
|
|
21
|
+
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
|
|
22
22
|
|
|
23
23
|
</div>
|
|
24
24
|
|
|
@@ -41,8 +41,8 @@ npx skills add morluto/rea
|
|
|
41
41
|
Then ask:
|
|
42
42
|
|
|
43
43
|
```text
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
Use REA to understand how search works in the Notes app, show me the
|
|
45
|
+
evidence, and build a similar feature for my project.
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Notes is only an example. Name any app you want to understand, or ask the agent to start with an overview.
|
|
@@ -81,6 +81,16 @@ REA shows how it reached its conclusions. It does not claim to recover original
|
|
|
81
81
|
|
|
82
82
|
## Quick start
|
|
83
83
|
|
|
84
|
+
### One-command install — recommended
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The installer supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. It installs compatible Node.js 24/npm 11 prerequisites, the exact latest `rea-agents-*` release, the official Hopper package, detected client registrations, and the bundled skill, then verifies the result. macOS uses Homebrew; Linux uses the native package manager and may show a system authorization prompt. Pin a reproducible release with `REA_VERSION=0.3.0`; `REA_VERSION` is the only supported installer environment override.
|
|
91
|
+
|
|
92
|
+
REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It configures detected clients with documented local MCP files; Devin is reported as skipped because its public documentation does not define a local configuration file. You can safely rerun the installer or `rea setup --yes`. If REA's local analysis engine needs one-time activation, installation remains intact and setup reports the exact remaining action rather than claiming full readiness.
|
|
93
|
+
|
|
84
94
|
### With a coding agent — recommended
|
|
85
95
|
|
|
86
96
|
```bash
|
|
@@ -89,22 +99,36 @@ npx skills add morluto/rea
|
|
|
89
99
|
|
|
90
100
|
Ask your agent to set up REA. It will check your Mac, explain anything it needs to install, ask for approval, and guide you through system prompts. After setup, restart the agent if it asks you to load the full REA toolset.
|
|
91
101
|
|
|
92
|
-
|
|
102
|
+
Approve installation if needed, complete the one-time activation when prompted, then describe the app or feature you want to understand. REA handles the binary-analysis tools behind the scenes.
|
|
103
|
+
|
|
104
|
+
### From Terminal — no installation
|
|
93
105
|
|
|
94
106
|
```bash
|
|
95
107
|
npx -y rea-agents setup --yes
|
|
108
|
+
npx -y rea-agents doctor
|
|
109
|
+
npx -y rea-agents analyze /Applications/Notes.app
|
|
96
110
|
```
|
|
97
111
|
|
|
98
112
|
If macOS or an installer asks for confirmation, complete the prompt and run the same command again. Restart a configured coding agent so it loads REA.
|
|
99
113
|
|
|
100
|
-
###
|
|
114
|
+
### From Terminal — install the `rea` command
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
```bash
|
|
117
|
+
npm install --global rea-agents
|
|
118
|
+
rea setup --yes
|
|
119
|
+
rea doctor
|
|
120
|
+
rea analyze /Applications/Notes.app
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Choose either the no-install commands or the global installation. You do not need both.
|
|
124
|
+
|
|
125
|
+
### Requirements
|
|
104
126
|
|
|
105
|
-
|
|
127
|
+
- macOS 12 or newer
|
|
128
|
+
- Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
|
|
129
|
+
- Node.js 24.18.x with npm 11.16.x
|
|
106
130
|
|
|
107
|
-
You do not need to install
|
|
131
|
+
You do not need to choose or install binary-analysis tools yourself. REA setup handles them when needed. Deep binary analysis currently uses [Hopper](https://www.hopperapp.com/), a separate desktop application with its own license. REA can install the official macOS or Linux package, but you must approve installation and complete its one-time activation.
|
|
108
132
|
|
|
109
133
|
If something is not working, run:
|
|
110
134
|
|
|
@@ -112,12 +136,63 @@ If something is not working, run:
|
|
|
112
136
|
npx -y rea-agents doctor
|
|
113
137
|
```
|
|
114
138
|
|
|
139
|
+
`rea doctor --json` is read-only and distinguishes unsupported hosts, missing dependencies, a missing local analysis engine, configuration drift, and healthy checks. Fresh-install activation is reported by setup because Hopper does not expose a reliable noninteractive activation probe.
|
|
140
|
+
|
|
141
|
+
### Linux installation and troubleshooting
|
|
142
|
+
|
|
143
|
+
On Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux, setup downloads the matching official Hopper package, restricts downloads to Hopper's public origin, verifies the published size and checksum, and invokes `apt-get`, `dnf`, or `pacman`. When REA is not already running as root, `pkexec` presents the system authorization prompt. REA never invokes `sudo`.
|
|
144
|
+
|
|
145
|
+
The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper
|
|
149
|
+
rea doctor --json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If doctor reports a missing analysis engine even though the file exists, inspect shared-library resolution with:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
ldd /opt/hopper/bin/Hopper | grep 'not found'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Install the missing distribution packages and rerun `rea setup --yes`. Hopper is a desktop application: real analysis requires an active `DISPLAY` or `WAYLAND_DISPLAY`, plus one-time license activation. The curl installer places the `rea` command in `~/.local/bin` on Linux; add that directory to future shell `PATH` values if it is not already present.
|
|
159
|
+
|
|
160
|
+
REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
|
|
161
|
+
|
|
162
|
+
To remove only REA-owned MCP registrations and the managed skill:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
rea uninstall
|
|
166
|
+
rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Uninstall preserves Hopper, Homebrew, Node.js, evidence, captures, external evidence roots, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
|
|
170
|
+
|
|
115
171
|
### CLI or coding agent?
|
|
116
172
|
|
|
117
|
-
| If you want to… | Use
|
|
118
|
-
| --------------------------------------------------------- |
|
|
119
|
-
| Ask an agent to investigate an app and build a feature | Install the skill, then talk to your agent
|
|
120
|
-
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile`
|
|
173
|
+
| If you want to… | Use |
|
|
174
|
+
| --------------------------------------------------------- | ---------------------------------------------- |
|
|
175
|
+
| Ask an agent to investigate an app and build a feature | Install the skill, then talk to your agent |
|
|
176
|
+
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
|
|
177
|
+
| Validate or canonicalize an Evidence v2 bundle | `rea evidence-import` or `rea evidence-export` |
|
|
178
|
+
| Import source as historical reference | `rea import-reference-source` |
|
|
179
|
+
|
|
180
|
+
Filesystem evidence commands and MCP file tools are disabled until the operator approves absolute roots:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export REA_EVIDENCE_ROOTS_JSON='["/absolute/path/to/evidence"]'
|
|
184
|
+
rea evidence-import /absolute/path/to/evidence/bundle.json
|
|
185
|
+
rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Historical source import requires a separate allowlist and never treats source as current behavioral authority:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
export REA_REFERENCE_ROOTS_JSON='["/absolute/path/to/source"]'
|
|
192
|
+
rea import-reference-source /absolute/path/to/source
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Exports never replace an existing file unless `--overwrite` is explicit. Imports are size/depth bounded, validate every Evidence v2 ID and manifest, and never execute bundle content.
|
|
121
196
|
|
|
122
197
|
## One prompt, a full investigation
|
|
123
198
|
|
|
@@ -151,26 +226,38 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
|
|
151
226
|
- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
|
|
152
227
|
- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
|
|
153
228
|
|
|
154
|
-
##
|
|
229
|
+
## 68 tools for investigation
|
|
155
230
|
|
|
156
231
|
| Tool family | Count | Examples |
|
|
157
232
|
| ------------------------- | ----: | ----------------------------------------------------------------------------------------------------------------------- |
|
|
158
233
|
| Native inspection | 33 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations |
|
|
159
234
|
| Investigation workflows | 10 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, call graphs, Swift and Objective-C discovery |
|
|
160
|
-
|
|
|
235
|
+
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
|
236
|
+
| Artifact graph | 2 | deterministic directory, ZIP/APK/IPA, and ASAR inventory; explicitly selected extraction into an absent owned tree |
|
|
237
|
+
| Workspace and observation | 18 | target lifecycle, Evidence v2 bundles, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
|
161
238
|
|
|
162
|
-
The public interface describes what the agent is trying to learn. Providers decide how to answer. Hopper
|
|
239
|
+
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
|
|
163
240
|
|
|
164
241
|
## Current status
|
|
165
242
|
|
|
166
243
|
REA is already useful for native application investigation on macOS:
|
|
167
244
|
|
|
168
|
-
- Open Mach-O, ELF, PE, `.app`, and Hopper database targets.
|
|
245
|
+
- Open Mach-O, ELF, PE, `.app`, ZIP, APK, IPA, ASAR, plist, JavaScript, source-map, and Hopper database targets.
|
|
246
|
+
- Traverse content-addressed artifact graphs without extraction; materialize only approved occurrences into absent output roots.
|
|
169
247
|
- Build bounded function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
|
|
170
248
|
- Search and trace features across symbols, strings, metadata, references, and call paths.
|
|
171
249
|
- Record every successful result as deterministic Evidence v2 with artifact and provider identity, confidence, authority, limitations, and locations.
|
|
172
250
|
- Export and import evidence bundles across sessions.
|
|
173
251
|
- Capture approved PTY scenarios, child processes, filesystem changes, and loopback HTTP/WebSocket exchanges, then compare normalized captures.
|
|
252
|
+
- Compare complete artifact inventories by stable path, content, metadata, and relations; incomplete evidence never implies equivalence.
|
|
253
|
+
- Compare explicit function dossiers across text, calls, references, strings, and address-normalized CFG topology with per-facet unknowns.
|
|
254
|
+
- Compare canonical Evidence bundles by exact membership, explicit observation pairs, and residual-unknown histories without turning omissions into behavioral absence.
|
|
255
|
+
- Aggregate runtime comparisons into observed behavior changes while keeping static artifact/function differences labeled as candidates.
|
|
256
|
+
- Build bounded, Evidence-cited direct call paths by exact address without treating missing dossiers as graph leaves.
|
|
257
|
+
- Correlate exact static/runtime findings through explicit hypotheses without claiming causality from cochange.
|
|
258
|
+
- Verify finite behavioral and structural reconstruction specifications with pass, fail, and unknown kept distinct.
|
|
259
|
+
- Track residual unknowns through immutable CAS revisions, evidence-qualified resolution, contradictions, probes, and validated dependency relationships.
|
|
260
|
+
- With explicit `unknown_registry_approved: true`, record bounded trace/capture residuals, typed provider unavailability, and capture disagreements automatically.
|
|
174
261
|
|
|
175
262
|
Hopper is the first provider, not the boundary of the project. Some current workflows still require Hopper and macOS; every evidence record identifies the provider and limitations behind its result.
|
|
176
263
|
|
|
@@ -215,10 +302,14 @@ flowchart LR
|
|
|
215
302
|
REA --> Workspace["Investigation workspace<br/>evidence + artifacts + captures"]
|
|
216
303
|
Workspace --> Router["Capability router"]
|
|
217
304
|
Router --> Hopper["Hopper provider"]
|
|
305
|
+
Router --> Native["Native macOS provider"]
|
|
306
|
+
Router --> Artifact["Artifact graph provider"]
|
|
218
307
|
Router --> Process["Process capture provider"]
|
|
219
|
-
Router -. roadmap .-> More["
|
|
308
|
+
Router -. roadmap .-> More["Browser, dynamic,<br/>and additional static providers"]
|
|
220
309
|
Hopper --> Target["Target software"]
|
|
221
310
|
Process --> Target
|
|
311
|
+
Native --> Target
|
|
312
|
+
Artifact --> Target
|
|
222
313
|
```
|
|
223
314
|
|
|
224
315
|
The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger across an investigation.
|
|
@@ -251,6 +342,19 @@ REA derives explicit format and architecture arguments to prevent common FAT and
|
|
|
251
342
|
|
|
252
343
|
Closing a REA session shuts down its bridge and removes its private socket directory. It does not quit a Hopper application the user may be using.
|
|
253
344
|
|
|
345
|
+
## Advanced process-capture setup
|
|
346
|
+
|
|
347
|
+
Process capture is disabled by default. Enabling it requires
|
|
348
|
+
`REA_PROCESS_CAPTURE_ENABLED=true`, approved executable and working roots in
|
|
349
|
+
`REA_PROCESS_EXECUTABLE_ROOTS_JSON` and `REA_PROCESS_WORKING_ROOTS_JSON`, and an
|
|
350
|
+
environment allowlist in `REA_PROCESS_ALLOWED_ENV_JSON`. Because the current PTY
|
|
351
|
+
adapter uses host networking, it also requires
|
|
352
|
+
`REA_PROCESS_ALLOW_EXTERNAL_NETWORK=true`.
|
|
353
|
+
|
|
354
|
+
If the native PTY backend is unavailable, install Xcode command-line tools and
|
|
355
|
+
run `npm run rebuild:native`. Linux source builds require Python, `make`, and a
|
|
356
|
+
C++ toolchain. Compatible packaged binaries do not require this rebuild.
|
|
357
|
+
|
|
254
358
|
## Security model
|
|
255
359
|
|
|
256
360
|
REA does not provide a hosted analysis service. Hopper communication uses an authenticated private local socket. Dynamic capabilities are disabled by default and require both operator policy and explicit per-call approval. REA is not a security sandbox: providers and launched targets run with the current user's permissions, and each capability reports its side effects and limitations. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
package/bridge/hopper_bridge.py
CHANGED
|
@@ -9,11 +9,15 @@ import json
|
|
|
9
9
|
import hmac
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
|
+
import sre_parse
|
|
12
13
|
import socket
|
|
13
14
|
|
|
14
15
|
MAX_LINE_BYTES = 10 * 1024 * 1024
|
|
15
16
|
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
|
|
16
17
|
_selected_document = None
|
|
18
|
+
_search_inventory_cache = {}
|
|
19
|
+
MAX_SEARCH_PATTERN_LENGTH = 256
|
|
20
|
+
MAX_SEARCH_VALUE_LENGTH = 4096
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def _hex(value):
|
|
@@ -89,6 +93,17 @@ def _procedure_identity(procedure):
|
|
|
89
93
|
return {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure)}
|
|
90
94
|
|
|
91
95
|
|
|
96
|
+
def _procedure_locals(procedure):
|
|
97
|
+
"""Project opaque Hopper local-variable objects into an exact public shape."""
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
"description": str(local),
|
|
101
|
+
"provenance": "hopper-public-python-api",
|
|
102
|
+
}
|
|
103
|
+
for local in procedure.getLocalVariableList()
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
92
107
|
def _containing_procedure(document, address):
|
|
93
108
|
segment = document.getSegmentAtAddress(address)
|
|
94
109
|
if segment is None:
|
|
@@ -182,6 +197,105 @@ def _strings(document):
|
|
|
182
197
|
return result
|
|
183
198
|
|
|
184
199
|
|
|
200
|
+
def _invalidate_search_inventory(document):
|
|
201
|
+
"""Discard derived names after analysis metadata changes."""
|
|
202
|
+
document_id = id(document)
|
|
203
|
+
for key in list(_search_inventory_cache):
|
|
204
|
+
if key[0] == document_id:
|
|
205
|
+
del _search_inventory_cache[key]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _search_inventory(document, kind):
|
|
209
|
+
"""Cache an immutable, address-sorted inventory for an unchanged document."""
|
|
210
|
+
key = (id(document), kind)
|
|
211
|
+
inventory = _search_inventory_cache.get(key)
|
|
212
|
+
if inventory is None:
|
|
213
|
+
values = _procedure_map(document) if kind == "procedure" else _strings(document)
|
|
214
|
+
inventory = tuple(sorted(values.items(), key=lambda item: int(item[0], 16)))
|
|
215
|
+
_search_inventory_cache[key] = inventory
|
|
216
|
+
return inventory
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _validate_regex_node(node, inside_repeat=False):
|
|
220
|
+
"""Reject regex structures with disproportionate or non-local evaluation cost."""
|
|
221
|
+
forbidden = {
|
|
222
|
+
sre_parse.ASSERT,
|
|
223
|
+
sre_parse.ASSERT_NOT,
|
|
224
|
+
sre_parse.GROUPREF,
|
|
225
|
+
sre_parse.GROUPREF_EXISTS,
|
|
226
|
+
}
|
|
227
|
+
repeat_tokens = {sre_parse.MAX_REPEAT, sre_parse.MIN_REPEAT}
|
|
228
|
+
possessive = getattr(sre_parse, "POSSESSIVE_REPEAT", None)
|
|
229
|
+
if possessive is not None:
|
|
230
|
+
repeat_tokens.add(possessive)
|
|
231
|
+
for operation, argument in node:
|
|
232
|
+
if operation in forbidden:
|
|
233
|
+
raise ValueError("Regex lookarounds and backreferences are not supported")
|
|
234
|
+
if operation in repeat_tokens:
|
|
235
|
+
if inside_repeat:
|
|
236
|
+
raise ValueError("Nested regex repetitions are not supported")
|
|
237
|
+
minimum, maximum, child = argument
|
|
238
|
+
if maximum == sre_parse.MAXREPEAT or maximum > 1000:
|
|
239
|
+
raise ValueError("Unbounded or excessive regex repetitions are not supported")
|
|
240
|
+
_validate_regex_node(child, True)
|
|
241
|
+
elif operation == sre_parse.SUBPATTERN:
|
|
242
|
+
_validate_regex_node(argument[-1], inside_repeat)
|
|
243
|
+
elif operation == sre_parse.BRANCH:
|
|
244
|
+
for branch in argument[1]:
|
|
245
|
+
_validate_regex_node(branch, inside_repeat)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _search_page(document, kind, params):
|
|
249
|
+
pattern = params.get("pattern")
|
|
250
|
+
if not isinstance(pattern, str) or not pattern or len(pattern) > MAX_SEARCH_PATTERN_LENGTH:
|
|
251
|
+
raise ValueError("pattern must contain between 1 and 256 characters")
|
|
252
|
+
mode = params.get("mode", "literal")
|
|
253
|
+
if mode not in ("literal", "regex"):
|
|
254
|
+
raise ValueError("mode must be literal or regex")
|
|
255
|
+
case_sensitive = params.get("case_sensitive", False)
|
|
256
|
+
if not isinstance(case_sensitive, bool):
|
|
257
|
+
raise ValueError("case_sensitive must be a boolean")
|
|
258
|
+
offset = params.get("offset", 0)
|
|
259
|
+
limit = params.get("limit", 100)
|
|
260
|
+
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
|
261
|
+
raise ValueError("offset must be a non-negative integer")
|
|
262
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
|
|
263
|
+
raise ValueError("limit must be an integer between 1 and 100")
|
|
264
|
+
|
|
265
|
+
if mode == "literal":
|
|
266
|
+
needle = pattern if case_sensitive else pattern.casefold()
|
|
267
|
+
matches = lambda value: needle in (value if case_sensitive else value.casefold())
|
|
268
|
+
else:
|
|
269
|
+
try:
|
|
270
|
+
parsed = sre_parse.parse(pattern)
|
|
271
|
+
_validate_regex_node(parsed)
|
|
272
|
+
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
|
|
273
|
+
except re.error as error:
|
|
274
|
+
raise ValueError("Invalid regex pattern") from error
|
|
275
|
+
matches = lambda value: expression.search(value) is not None
|
|
276
|
+
|
|
277
|
+
matching = [item for item in _search_inventory(document, kind) if matches(item[1])]
|
|
278
|
+
selected = matching[offset:offset + limit]
|
|
279
|
+
total = len(matching)
|
|
280
|
+
next_offset = offset + len(selected)
|
|
281
|
+
has_more = next_offset < total
|
|
282
|
+
return {
|
|
283
|
+
"items": [
|
|
284
|
+
{
|
|
285
|
+
"address": address,
|
|
286
|
+
"value": value[:MAX_SEARCH_VALUE_LENGTH],
|
|
287
|
+
"value_truncated": len(value) > MAX_SEARCH_VALUE_LENGTH,
|
|
288
|
+
}
|
|
289
|
+
for address, value in selected
|
|
290
|
+
],
|
|
291
|
+
"offset": offset,
|
|
292
|
+
"limit": limit,
|
|
293
|
+
"total": total,
|
|
294
|
+
"next_offset": next_offset if has_more else None,
|
|
295
|
+
"has_more": has_more,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
185
299
|
def _page(values, offset, limit):
|
|
186
300
|
"""Return one deterministically address-sorted page without crossing an unbounded map."""
|
|
187
301
|
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
|
@@ -354,7 +468,7 @@ def _analyze_function(document, params):
|
|
|
354
468
|
def collection(name, items, scan_limited=False):
|
|
355
469
|
return _bounded(items, _collection_offset(params, name), limit, None, scan_limited)
|
|
356
470
|
return {
|
|
357
|
-
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "signature": procedure.signatureString(), "locals":
|
|
471
|
+
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
|
|
358
472
|
"pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
|
|
359
473
|
"assembly": _bounded(assembly_lines, assembly_offset, max_instructions),
|
|
360
474
|
"comments": collection("comments", comments, instruction_scan_truncated),
|
|
@@ -372,7 +486,7 @@ def _dispatch(method, params):
|
|
|
372
486
|
"""Dispatch only the closed operation set implemented by REA's public tools."""
|
|
373
487
|
global _selected_document
|
|
374
488
|
if method == "health":
|
|
375
|
-
return {"name": "REA Hopper bridge", "version": "1.0.0"}
|
|
489
|
+
return {"name": "REA Hopper bridge", "version": "1.0.0", "run_id": REA_RUN_ID}
|
|
376
490
|
if method == "shutdown":
|
|
377
491
|
return {"shutdown": True}
|
|
378
492
|
if method == "list_documents":
|
|
@@ -426,18 +540,30 @@ def _dispatch(method, params):
|
|
|
426
540
|
return _hex(result)
|
|
427
541
|
if method == "list_segments":
|
|
428
542
|
result = []
|
|
543
|
+
permission_limitation = _unavailable(
|
|
544
|
+
"Hopper's public Python API does not expose segment or section permissions"
|
|
545
|
+
)
|
|
429
546
|
for segment in document.getSegmentsList():
|
|
430
547
|
start = segment.getStartingAddress()
|
|
431
|
-
sections = [{
|
|
548
|
+
sections = [{
|
|
549
|
+
"name": section.getName(),
|
|
550
|
+
"start": _hex(section.getStartingAddress()),
|
|
551
|
+
"end": _hex(section.getStartingAddress() + section.getLength()),
|
|
552
|
+
"readable": None,
|
|
553
|
+
"writable": None,
|
|
554
|
+
"executable": None,
|
|
555
|
+
"permissions": permission_limitation,
|
|
556
|
+
"provenance": "hopper-public-python-api",
|
|
557
|
+
} for section in segment.getSectionsList()]
|
|
432
558
|
result.append({
|
|
433
559
|
"name": segment.getName(),
|
|
434
560
|
"start": _hex(start),
|
|
435
561
|
"end": _hex(start + segment.getLength()),
|
|
562
|
+
"readable": None,
|
|
436
563
|
"writable": None,
|
|
437
564
|
"executable": None,
|
|
438
|
-
"permissions":
|
|
439
|
-
|
|
440
|
-
),
|
|
565
|
+
"permissions": permission_limitation,
|
|
566
|
+
"provenance": "hopper-public-python-api",
|
|
441
567
|
"sections": sections,
|
|
442
568
|
})
|
|
443
569
|
return result
|
|
@@ -461,10 +587,8 @@ def _dispatch(method, params):
|
|
|
461
587
|
result = {key: result[key]} if key in result else {}
|
|
462
588
|
return _page(result, params.get("offset", 0), params.get("limit", 100))
|
|
463
589
|
if method in ("search_procedures", "search_strings"):
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
values = _procedure_map(document) if method == "search_procedures" else _strings(document)
|
|
467
|
-
return {key: value for key, value in values.items() if expression.search(value)}
|
|
590
|
+
kind = "procedure" if method == "search_procedures" else "string"
|
|
591
|
+
return _search_page(document, kind, params)
|
|
468
592
|
if method.startswith("procedure_"):
|
|
469
593
|
procedure = _procedure(document, params.get("procedure"))
|
|
470
594
|
if method == "procedure_address":
|
|
@@ -480,12 +604,16 @@ def _dispatch(method, params):
|
|
|
480
604
|
if method == "procedure_info":
|
|
481
605
|
blocks = list(procedure.basicBlockIterator())
|
|
482
606
|
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
|
|
483
|
-
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": procedure
|
|
607
|
+
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)}
|
|
484
608
|
if method == "set_address_name":
|
|
485
609
|
address = _address(document, params.get("address"))
|
|
486
|
-
|
|
610
|
+
result = document.setNameAtAddress(address, params["name"])
|
|
611
|
+
_invalidate_search_inventory(document)
|
|
612
|
+
return result
|
|
487
613
|
if method == "set_addresses_names":
|
|
488
|
-
|
|
614
|
+
result = {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
|
|
615
|
+
_invalidate_search_inventory(document)
|
|
616
|
+
return result
|
|
489
617
|
if method in ("set_comment", "set_inline_comment"):
|
|
490
618
|
address = _address(document, params.get("address"))
|
|
491
619
|
segment = _segment(document, address)
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
2
|
+
/** Build a validated atomic provider observation at an adapter boundary. */
|
|
3
|
+
export const createAnalysisExecution = (result, provider, options = {}) => ({
|
|
4
|
+
result: jsonValueSchema.parse(result),
|
|
5
|
+
rawResult: options.rawResult === undefined
|
|
6
|
+
? jsonValueSchema.parse(result)
|
|
7
|
+
: jsonValueSchema.parse(options.rawResult),
|
|
8
|
+
provider,
|
|
9
|
+
limitations: [...(options.limitations ?? [])],
|
|
10
|
+
locations: [...(options.locations ?? [])],
|
|
11
|
+
subject: options.subject ?? null,
|
|
12
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import { AsarArtifactReader } from "../artifacts/AsarArtifactReader.js";
|
|
3
|
+
import { ArtifactPathRegistry, normalizeArtifactPath, } from "../artifacts/ArtifactPaths.js";
|
|
4
|
+
import { ArtifactReaderFailure, } from "../artifacts/ArtifactReader.js";
|
|
5
|
+
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
|
|
6
|
+
import { SafeOutputTree } from "../artifacts/SafeOutputTree.js";
|
|
7
|
+
import { ZipArtifactReader } from "../artifacts/ZipArtifactReader.js";
|
|
8
|
+
import { MachOSliceArtifactReader } from "../artifacts/MachOSliceArtifactReader.js";
|
|
9
|
+
import { artifactExtractionResultSchema, } from "../domain/artifactGraph.js";
|
|
10
|
+
import { scanArtifactInventory } from "./ArtifactInventory.js";
|
|
11
|
+
import { digestCanonical, pageOf, toOutputLimits, } from "./ArtifactGraphConstruction.js";
|
|
12
|
+
const PAGE_SIZE = 500;
|
|
13
|
+
/** Extract selected inventory occurrences into an exclusively owned absent root. */
|
|
14
|
+
export const extractArtifact = async (input, signal) => {
|
|
15
|
+
validateSelection(input.occurrenceIds);
|
|
16
|
+
const sourcePath = await realpath(input.inputPath);
|
|
17
|
+
const selectedIds = new Set(input.occurrenceIds);
|
|
18
|
+
const inventory = await loadInventory(sourcePath, input.limits, selectedIds, signal);
|
|
19
|
+
const selected = input.occurrenceIds.map((id) => {
|
|
20
|
+
const occurrence = inventory.occurrences.get(id);
|
|
21
|
+
if (occurrence === undefined)
|
|
22
|
+
throw new ArtifactReaderFailure("unavailable", `Selected artifact occurrence was not found: ${id}`);
|
|
23
|
+
if ((occurrence.entry_kind !== "file" && occurrence.entry_kind !== "slice") ||
|
|
24
|
+
occurrence.artifact_id === null ||
|
|
25
|
+
occurrence.encrypted ||
|
|
26
|
+
occurrence.logical_path === ".")
|
|
27
|
+
throw new ArtifactReaderFailure("format", `Selected occurrence is not an extractable regular child file: ${id}`);
|
|
28
|
+
const node = inventory.nodes.get(occurrence.artifact_id);
|
|
29
|
+
if (node === undefined)
|
|
30
|
+
throw new ArtifactReaderFailure("integrity", `Selected occurrence has no inventory node: ${id}`);
|
|
31
|
+
return { occurrence, node };
|
|
32
|
+
});
|
|
33
|
+
return materializeSelection({
|
|
34
|
+
input,
|
|
35
|
+
sourcePath,
|
|
36
|
+
inventory,
|
|
37
|
+
selected,
|
|
38
|
+
signal,
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const materializeSelection = async ({ input, sourcePath, inventory, selected, signal, }) => {
|
|
42
|
+
const byPath = new Map(selected.map((item) => [item.occurrence.logical_path, item]));
|
|
43
|
+
const reader = await createReader(sourcePath, input.inputFormat);
|
|
44
|
+
const output = await SafeOutputTree.create(input.outputRoot, input.limits);
|
|
45
|
+
let readerClosed = false;
|
|
46
|
+
const extracted = [];
|
|
47
|
+
try {
|
|
48
|
+
const found = new Set();
|
|
49
|
+
const registry = new ArtifactPathRegistry();
|
|
50
|
+
let entryCount = 0;
|
|
51
|
+
for await (const entry of reader.entries(signal)) {
|
|
52
|
+
entryCount += 1;
|
|
53
|
+
if (entryCount > input.limits.maxEntries)
|
|
54
|
+
throw new ArtifactReaderFailure("limit", "Artifact entry limit exceeded during extraction");
|
|
55
|
+
const path = normalizeArtifactPath(entry.path, input.limits);
|
|
56
|
+
registry.add(path, entry.kind);
|
|
57
|
+
const selectedItem = byPath.get(path);
|
|
58
|
+
if (selectedItem === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
preflight(entry, input.limits);
|
|
61
|
+
const stream = await reader.open(entry, signal);
|
|
62
|
+
const written = await output.write(path, stream, selectedItem.node.sha256, signal);
|
|
63
|
+
extracted.push({
|
|
64
|
+
artifact_id: selectedItem.node.artifact_id,
|
|
65
|
+
relative_path: written.relativePath,
|
|
66
|
+
sha256: written.sha256,
|
|
67
|
+
bytes_written: written.bytesWritten,
|
|
68
|
+
created: true,
|
|
69
|
+
});
|
|
70
|
+
found.add(path);
|
|
71
|
+
}
|
|
72
|
+
if (found.size !== selected.length)
|
|
73
|
+
throw new ArtifactReaderFailure("integrity", "Selected inventory occurrence disappeared before extraction");
|
|
74
|
+
await reader.close();
|
|
75
|
+
readerClosed = true;
|
|
76
|
+
extracted.sort((left, right) => left.relative_path.localeCompare(right.relative_path, "en"));
|
|
77
|
+
const result = createExtractionResult(input, inventory, selected, extracted);
|
|
78
|
+
await output.commit();
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
if (!readerClosed)
|
|
83
|
+
await reader.close().catch(() => undefined);
|
|
84
|
+
await output.rollback();
|
|
85
|
+
throw cause;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const createExtractionResult = (input, inventory, selected, extracted) => {
|
|
89
|
+
const extractionSemantic = {
|
|
90
|
+
schema_version: 1,
|
|
91
|
+
source_manifest_id: inventory.manifest.manifest_id,
|
|
92
|
+
selected_occurrence_ids: selected
|
|
93
|
+
.map(({ occurrence }) => occurrence.occurrence_id)
|
|
94
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
95
|
+
files_sha256: digestCanonical(extracted),
|
|
96
|
+
output_root_alias: "$OUTPUT_ROOT",
|
|
97
|
+
};
|
|
98
|
+
return artifactExtractionResultSchema.parse({
|
|
99
|
+
manifest: inventory.manifest,
|
|
100
|
+
extraction_manifest: {
|
|
101
|
+
...extractionSemantic,
|
|
102
|
+
extraction_id: `aex_${digestCanonical(extractionSemantic)}`,
|
|
103
|
+
},
|
|
104
|
+
output_root: "$OUTPUT_ROOT",
|
|
105
|
+
artifacts: pageOf(extracted, input.offset, input.limit),
|
|
106
|
+
containment_verified: true,
|
|
107
|
+
cleanup: { attempted: false, verified: true, residual_paths: [] },
|
|
108
|
+
limits: toOutputLimits(input.limits),
|
|
109
|
+
provenance: [],
|
|
110
|
+
limitations: [
|
|
111
|
+
"Only caller-selected regular file occurrences were materialized.",
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const loadInventory = async (path, limits, selectedIds, signal) => {
|
|
116
|
+
const snapshot = await scanArtifactInventory(path, limits, signal);
|
|
117
|
+
const occurrences = new Map();
|
|
118
|
+
const neededNodes = new Set();
|
|
119
|
+
collectOccurrences(snapshot.occurrences, selectedIds, occurrences, neededNodes);
|
|
120
|
+
const nodes = new Map();
|
|
121
|
+
collectNodes(snapshot.nodes, neededNodes, nodes);
|
|
122
|
+
return { manifest: snapshot.manifest, occurrences, nodes };
|
|
123
|
+
};
|
|
124
|
+
const collectOccurrences = (items, selected, output, neededNodes) => {
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
if (!selected.has(item.occurrence_id))
|
|
127
|
+
continue;
|
|
128
|
+
output.set(item.occurrence_id, item);
|
|
129
|
+
if (item.artifact_id !== null)
|
|
130
|
+
neededNodes.add(item.artifact_id);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const collectNodes = (items, selected, output) => {
|
|
134
|
+
for (const item of items)
|
|
135
|
+
if (selected.has(item.artifact_id))
|
|
136
|
+
output.set(item.artifact_id, item);
|
|
137
|
+
};
|
|
138
|
+
const createReader = async (path, format) => {
|
|
139
|
+
if ((await lstat(path)).isDirectory())
|
|
140
|
+
return new DirectoryArtifactReader(path);
|
|
141
|
+
if (format === "asar")
|
|
142
|
+
return new AsarArtifactReader(path);
|
|
143
|
+
if (format === "ipa" || format === "apk" || format === "zip")
|
|
144
|
+
return new ZipArtifactReader(path, format);
|
|
145
|
+
if (format === "mach-o")
|
|
146
|
+
return new MachOSliceArtifactReader(path);
|
|
147
|
+
throw new ArtifactReaderFailure("unavailable", `Artifact format has no extraction reader: ${format}`);
|
|
148
|
+
};
|
|
149
|
+
const validateSelection = (ids) => {
|
|
150
|
+
if (ids.length === 0 || ids.length > PAGE_SIZE)
|
|
151
|
+
throw new ArtifactReaderFailure("limit", "Extraction requires 1 to 500 explicitly selected occurrences");
|
|
152
|
+
if (new Set(ids).size !== ids.length)
|
|
153
|
+
throw new ArtifactReaderFailure("path", "Extraction occurrence selection contains duplicates");
|
|
154
|
+
};
|
|
155
|
+
const preflight = (entry, limits) => {
|
|
156
|
+
if ((entry.kind !== "file" && entry.kind !== "slice") || entry.encrypted)
|
|
157
|
+
throw new ArtifactReaderFailure("format", `Selected artifact entry cannot be read: ${entry.path}`);
|
|
158
|
+
if (entry.declaredSize !== null && entry.declaredSize > limits.maxEntryBytes)
|
|
159
|
+
throw new ArtifactReaderFailure("limit", `Selected artifact exceeds byte limit: ${entry.path}`);
|
|
160
|
+
if (entry.declaredSize !== null &&
|
|
161
|
+
entry.compressedSize !== null &&
|
|
162
|
+
(entry.compressedSize === 0
|
|
163
|
+
? entry.declaredSize > 0
|
|
164
|
+
: entry.declaredSize / entry.compressedSize > limits.maxCompressionRatio))
|
|
165
|
+
throw new ArtifactReaderFailure("limit", `Selected artifact exceeds compression ratio limit: ${entry.path}`);
|
|
166
|
+
};
|