rea-agents 0.3.0 → 0.5.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 +139 -21
- package/bridge/hopper_bridge.py +156 -14
- 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 +174 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +44 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +10 -7
- package/dist/application/EvidenceBundleCommands.js +51 -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 +105 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +116 -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 +229 -21
- package/dist/cliEvidenceCommands.js +68 -0
- package/dist/cliLogging.js +21 -0
- 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 +156 -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 +199 -5
- 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 +35 -8
- 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/referenceSourcePolicy.js +1 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +39 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/hopper/protocol.js +13 -2
- 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 +18 -1
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { streamChunkToBuffer } from "./StreamBytes.js";
|
|
6
|
+
import { ArtifactPathRegistry, normalizeArtifactPath, } from "./ArtifactPaths.js";
|
|
7
|
+
import { ArtifactReaderFailure, } from "./ArtifactReader.js";
|
|
8
|
+
/** Symlink-resistant materialization in an exclusively owned, initially absent tree. */
|
|
9
|
+
export class SafeOutputTree {
|
|
10
|
+
#registry = new ArtifactPathRegistry();
|
|
11
|
+
#limits;
|
|
12
|
+
#outputRoot;
|
|
13
|
+
#stagingRoot;
|
|
14
|
+
#totalBytes = 0;
|
|
15
|
+
#published = false;
|
|
16
|
+
#cleanup = {
|
|
17
|
+
attempted: false,
|
|
18
|
+
verified: false,
|
|
19
|
+
residualPaths: [],
|
|
20
|
+
};
|
|
21
|
+
constructor(outputRoot, stagingRoot, limits) {
|
|
22
|
+
this.#outputRoot = outputRoot;
|
|
23
|
+
this.#stagingRoot = stagingRoot;
|
|
24
|
+
this.#limits = limits;
|
|
25
|
+
}
|
|
26
|
+
/** Exclusively create the absent destination as this operation's owned tree. */
|
|
27
|
+
static async create(outputRoot, limits) {
|
|
28
|
+
if (!isAbsolute(outputRoot))
|
|
29
|
+
throw new ArtifactReaderFailure("path", "Extraction output root must be absolute");
|
|
30
|
+
const requested = resolve(outputRoot);
|
|
31
|
+
const name = basename(requested);
|
|
32
|
+
if (name === "." || name === "..")
|
|
33
|
+
throw new ArtifactReaderFailure("path", "Invalid extraction output root");
|
|
34
|
+
const parent = await realpath(dirname(requested)).catch((cause) => {
|
|
35
|
+
throw new ArtifactReaderFailure("unavailable", "Extraction output parent is unavailable", { cause });
|
|
36
|
+
});
|
|
37
|
+
const canonicalOutput = join(parent, name);
|
|
38
|
+
await mkdir(canonicalOutput, { mode: 0o700 }).catch((cause) => {
|
|
39
|
+
if (isAlreadyExists(cause))
|
|
40
|
+
throw new ArtifactReaderFailure("path", "Extraction output root already exists", { cause });
|
|
41
|
+
throw cause;
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const stagingHandle = await open(canonicalOutput, constants.O_RDONLY | constants.O_DIRECTORY);
|
|
45
|
+
try {
|
|
46
|
+
await stagingHandle.chmod(0o700);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await stagingHandle.close();
|
|
50
|
+
}
|
|
51
|
+
return new SafeOutputTree(canonicalOutput, canonicalOutput, limits);
|
|
52
|
+
}
|
|
53
|
+
catch (cause) {
|
|
54
|
+
await rm(canonicalOutput, { recursive: true, force: true });
|
|
55
|
+
throw cause;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
get outputRoot() {
|
|
59
|
+
return this.#outputRoot;
|
|
60
|
+
}
|
|
61
|
+
get cleanup() {
|
|
62
|
+
return this.#cleanup;
|
|
63
|
+
}
|
|
64
|
+
/** Stream one selected regular file with exact byte and digest verification. */
|
|
65
|
+
async write(relativePath, source, expectedSha256, signal) {
|
|
66
|
+
this.#assertWritable();
|
|
67
|
+
const path = normalizeArtifactPath(relativePath, this.#limits);
|
|
68
|
+
this.#registry.add(path, "file");
|
|
69
|
+
const destination = await this.#prepareParent(path);
|
|
70
|
+
const handle = await open(destination, constants.O_CREAT |
|
|
71
|
+
constants.O_EXCL |
|
|
72
|
+
constants.O_WRONLY |
|
|
73
|
+
constants.O_NOFOLLOW, 0o600).catch((cause) => {
|
|
74
|
+
throw new ArtifactReaderFailure("path", `Could not exclusively create extraction path: ${path}`, { cause });
|
|
75
|
+
});
|
|
76
|
+
const hash = createHash("sha256");
|
|
77
|
+
let bytes = 0;
|
|
78
|
+
try {
|
|
79
|
+
for await (const raw of source) {
|
|
80
|
+
abortIfNeeded(signal);
|
|
81
|
+
const chunk = streamChunkToBuffer(raw);
|
|
82
|
+
bytes += chunk.length;
|
|
83
|
+
if (bytes > this.#limits.maxEntryBytes)
|
|
84
|
+
throw new ArtifactReaderFailure("limit", `Observed entry bytes exceed limit: ${path}`);
|
|
85
|
+
if (this.#totalBytes + bytes > this.#limits.maxTotalBytes)
|
|
86
|
+
throw new ArtifactReaderFailure("limit", "Observed extraction bytes exceed cumulative limit");
|
|
87
|
+
hash.update(chunk);
|
|
88
|
+
await writeAll(handle, chunk);
|
|
89
|
+
}
|
|
90
|
+
const sha256 = hash.digest("hex");
|
|
91
|
+
if (sha256 !== expectedSha256)
|
|
92
|
+
throw new ArtifactReaderFailure("integrity", `Extracted content disagrees with inventory: ${path}`);
|
|
93
|
+
await handle.sync();
|
|
94
|
+
await handle.close();
|
|
95
|
+
const readback = await hashFile(destination, bytes, signal);
|
|
96
|
+
if (readback.sha256 !== sha256 || readback.bytes !== bytes)
|
|
97
|
+
throw new ArtifactReaderFailure("integrity", `Durable readback verification failed: ${path}`);
|
|
98
|
+
this.#totalBytes += bytes;
|
|
99
|
+
return { relativePath: path, sha256, bytesWritten: bytes };
|
|
100
|
+
}
|
|
101
|
+
catch (cause) {
|
|
102
|
+
await handle.close().catch(() => undefined);
|
|
103
|
+
throw cause;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Sync the owned output tree and prevent further writes through this instance. */
|
|
107
|
+
async commit() {
|
|
108
|
+
this.#assertWritable();
|
|
109
|
+
const parent = await open(dirname(this.#outputRoot), constants.O_RDONLY | constants.O_DIRECTORY);
|
|
110
|
+
const staging = await open(this.#stagingRoot, constants.O_RDONLY | constants.O_DIRECTORY);
|
|
111
|
+
try {
|
|
112
|
+
await staging.sync();
|
|
113
|
+
await parent.sync();
|
|
114
|
+
this.#published = true;
|
|
115
|
+
}
|
|
116
|
+
catch (cause) {
|
|
117
|
+
throw new ArtifactReaderFailure("path", "Could not durably sync extraction output", { cause });
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await Promise.allSettled([parent.close(), staging.close()]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Remove only this operation's unsealed tree and verify absence. */
|
|
124
|
+
async rollback() {
|
|
125
|
+
if (this.#published)
|
|
126
|
+
return this.#cleanup;
|
|
127
|
+
await rm(this.#stagingRoot, { recursive: true, force: true });
|
|
128
|
+
const absent = await isAbsent(this.#stagingRoot);
|
|
129
|
+
this.#cleanup = {
|
|
130
|
+
attempted: true,
|
|
131
|
+
verified: absent,
|
|
132
|
+
residualPaths: absent ? [] : [basename(this.#stagingRoot)],
|
|
133
|
+
};
|
|
134
|
+
if (!absent)
|
|
135
|
+
throw new ArtifactReaderFailure("integrity", "Extraction output cleanup could not be verified");
|
|
136
|
+
return this.#cleanup;
|
|
137
|
+
}
|
|
138
|
+
async #prepareParent(relativePath) {
|
|
139
|
+
const parts = relativePath.split("/");
|
|
140
|
+
const fileName = parts.pop();
|
|
141
|
+
if (fileName === undefined)
|
|
142
|
+
throw new ArtifactReaderFailure("path", "Invalid extraction path");
|
|
143
|
+
let current = this.#stagingRoot;
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
current = join(current, part);
|
|
146
|
+
await mkdir(current, { mode: 0o700 }).catch((cause) => {
|
|
147
|
+
if (!isAlreadyExists(cause))
|
|
148
|
+
throw cause;
|
|
149
|
+
});
|
|
150
|
+
const metadata = await lstat(current);
|
|
151
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
152
|
+
throw new ArtifactReaderFailure("path", `Unsafe extraction parent: ${relativePath}`);
|
|
153
|
+
}
|
|
154
|
+
return join(current, fileName);
|
|
155
|
+
}
|
|
156
|
+
#assertWritable() {
|
|
157
|
+
if (this.#published)
|
|
158
|
+
throw new ArtifactReaderFailure("integrity", "Extraction tree is already committed");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const hashFile = async (path, maximum, signal) => {
|
|
162
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
163
|
+
const hash = createHash("sha256");
|
|
164
|
+
let bytes = 0;
|
|
165
|
+
try {
|
|
166
|
+
for await (const raw of handle.createReadStream({ autoClose: false })) {
|
|
167
|
+
abortIfNeeded(signal);
|
|
168
|
+
const chunk = streamChunkToBuffer(raw);
|
|
169
|
+
bytes += chunk.length;
|
|
170
|
+
if (bytes > maximum)
|
|
171
|
+
throw new ArtifactReaderFailure("integrity", "Readback exceeded the bytes written");
|
|
172
|
+
hash.update(chunk);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
await handle.close();
|
|
177
|
+
}
|
|
178
|
+
return { sha256: hash.digest("hex"), bytes };
|
|
179
|
+
};
|
|
180
|
+
const writeAll = async (handle, chunk) => {
|
|
181
|
+
let offset = 0;
|
|
182
|
+
while (offset < chunk.length) {
|
|
183
|
+
const { bytesWritten } = await handle.write(chunk, offset, chunk.length - offset);
|
|
184
|
+
if (bytesWritten === 0)
|
|
185
|
+
throw new ArtifactReaderFailure("unavailable", "Extraction output stopped accepting bytes");
|
|
186
|
+
offset += bytesWritten;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const isAbsent = async (path) => lstat(path).then(() => false, (cause) => {
|
|
190
|
+
if (isNotFound(cause))
|
|
191
|
+
return true;
|
|
192
|
+
throw cause;
|
|
193
|
+
});
|
|
194
|
+
const isNotFound = (cause) => cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
195
|
+
const isAlreadyExists = (cause) => cause instanceof Error && "code" in cause && cause.code === "EEXIST";
|
|
196
|
+
const abortIfNeeded = (signal) => {
|
|
197
|
+
if (signal?.aborted === true)
|
|
198
|
+
throw new ArtifactReaderFailure("cancelled", "Artifact extraction cancelled");
|
|
199
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Parse a Node readable chunk without unchecked coercion. */
|
|
2
|
+
export const streamChunkToBuffer = (value) => {
|
|
3
|
+
if (Buffer.isBuffer(value))
|
|
4
|
+
return value;
|
|
5
|
+
if (value instanceof Uint8Array)
|
|
6
|
+
return Buffer.from(value);
|
|
7
|
+
if (typeof value === "string")
|
|
8
|
+
return Buffer.from(value);
|
|
9
|
+
throw new TypeError("Readable stream emitted a non-byte chunk");
|
|
10
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { Reader, ZipReader } from "@zip.js/zip.js";
|
|
5
|
+
import { ArtifactReaderFailure, } from "./ArtifactReader.js";
|
|
6
|
+
class NodeFileReader extends Reader {
|
|
7
|
+
path;
|
|
8
|
+
#handle;
|
|
9
|
+
constructor(path) {
|
|
10
|
+
super(path);
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
async init() {
|
|
14
|
+
this.#handle = await open(this.path, "r");
|
|
15
|
+
this.size = (await this.#handle.stat()).size;
|
|
16
|
+
}
|
|
17
|
+
async readUint8Array(index, length) {
|
|
18
|
+
const handle = this.#handle;
|
|
19
|
+
if (handle === undefined)
|
|
20
|
+
throw new Error("ZIP reader is not initialized");
|
|
21
|
+
const bytes = Buffer.alloc(Math.min(length, this.size - index));
|
|
22
|
+
const read = await handle.read(bytes, 0, bytes.length, index);
|
|
23
|
+
return bytes.subarray(0, read.bytesRead);
|
|
24
|
+
}
|
|
25
|
+
async closeHandle() {
|
|
26
|
+
await this.#handle?.close();
|
|
27
|
+
this.#handle = undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Lazy Zip64-capable reader with CRC and overlap verification on every read. */
|
|
31
|
+
export class ZipArtifactReader {
|
|
32
|
+
format;
|
|
33
|
+
#source;
|
|
34
|
+
#reader;
|
|
35
|
+
#entries = new Map();
|
|
36
|
+
constructor(path, format) {
|
|
37
|
+
this.format = format;
|
|
38
|
+
this.#source = new NodeFileReader(path);
|
|
39
|
+
this.#reader = new ZipReader(this.#source, {
|
|
40
|
+
checkSignature: true,
|
|
41
|
+
checkOverlappingEntry: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async *entries(signal) {
|
|
45
|
+
for await (const entry of this.#reader.getEntriesGenerator()) {
|
|
46
|
+
abortIfNeeded(signal);
|
|
47
|
+
this.#entries.set(entry.filename, entry);
|
|
48
|
+
const symlink = isSymlink(entry);
|
|
49
|
+
yield {
|
|
50
|
+
path: entry.filename,
|
|
51
|
+
kind: entry.directory ? "directory" : symlink ? "symlink" : "file",
|
|
52
|
+
declaredSize: entry.directory ? null : entry.uncompressedSize,
|
|
53
|
+
compressedSize: entry.directory ? null : entry.compressedSize,
|
|
54
|
+
executable: entry.executable,
|
|
55
|
+
encrypted: entry.encrypted,
|
|
56
|
+
byteOffset: null,
|
|
57
|
+
declaredSha256: null,
|
|
58
|
+
limitations: symlink ? ["Archive symlink target was not read."] : [],
|
|
59
|
+
adapterKey: entry.filename,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
open(entry, signal) {
|
|
64
|
+
abortIfNeeded(signal);
|
|
65
|
+
const stored = this.#entries.get(entry.adapterKey);
|
|
66
|
+
if (stored === undefined || stored.directory || isSymlink(stored))
|
|
67
|
+
return Promise.reject(new ArtifactReaderFailure("format", "ZIP entry is not a regular file"));
|
|
68
|
+
if (stored.encrypted)
|
|
69
|
+
return Promise.reject(new ArtifactReaderFailure("unavailable", "Encrypted ZIP entry is unsupported"));
|
|
70
|
+
return Promise.resolve(extractStream(stored, signal));
|
|
71
|
+
}
|
|
72
|
+
async close() {
|
|
73
|
+
await this.#reader.close().catch(() => undefined);
|
|
74
|
+
await this.#source.closeHandle();
|
|
75
|
+
}
|
|
76
|
+
provenance() {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const extractStream = (entry, signal) => {
|
|
81
|
+
const output = new PassThrough();
|
|
82
|
+
const writable = new WritableStream({
|
|
83
|
+
write: async (chunk) => {
|
|
84
|
+
abortIfNeeded(signal);
|
|
85
|
+
if (!output.write(Buffer.from(chunk)))
|
|
86
|
+
await once(output, "drain");
|
|
87
|
+
},
|
|
88
|
+
close: () => {
|
|
89
|
+
output.end();
|
|
90
|
+
},
|
|
91
|
+
abort: (reason) => {
|
|
92
|
+
output.destroy(toError(reason));
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
void entry
|
|
96
|
+
.getData(writable, {
|
|
97
|
+
checkSignature: true,
|
|
98
|
+
checkOverlappingEntry: true,
|
|
99
|
+
onprogress: () => abortIfNeeded(signal),
|
|
100
|
+
})
|
|
101
|
+
.catch((cause) => output.destroy(toError(cause)));
|
|
102
|
+
return output;
|
|
103
|
+
};
|
|
104
|
+
const isSymlink = (entry) => entry.unixMode !== undefined && (entry.unixMode & 0o170000) === 0o120000;
|
|
105
|
+
const abortIfNeeded = (signal) => {
|
|
106
|
+
if (signal?.aborted === true)
|
|
107
|
+
throw new ArtifactReaderFailure("cancelled", "ZIP operation cancelled");
|
|
108
|
+
};
|
|
109
|
+
const toError = (cause) => cause instanceof Error ? cause : new Error("ZIP extraction failed");
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Cli, z } from "incur";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { runDoctor } from "./application/Doctor.js";
|
|
4
|
-
import { runDirectAnalysis } from "./application/DirectAnalysis.js";
|
|
4
|
+
import { runDirectAnalysis, runProviderAnalysis, runSessionStatus, } from "./application/DirectAnalysis.js";
|
|
5
5
|
import { runSetup } from "./application/Setup.js";
|
|
6
|
+
import { runUninstall } from "./application/Uninstall.js";
|
|
6
7
|
import { PRODUCT_IDENTITY } from "./identity.js";
|
|
7
8
|
import { createLogger, parseLogLevel } from "./logger.js";
|
|
9
|
+
import { logCliCommand } from "./cliLogging.js";
|
|
10
|
+
import { parseConfig } from "./config.js";
|
|
11
|
+
import { importReferenceSource } from "./application/ReferenceSourceImport.js";
|
|
12
|
+
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
|
|
8
13
|
/**
|
|
9
14
|
* Build the one-shot Incur CLI without starting Hopper at import time.
|
|
10
15
|
* Analysis commands acquire and close their own sessions; bare `mcp` and
|
|
@@ -15,7 +20,7 @@ export const createCli = () => {
|
|
|
15
20
|
? "silent"
|
|
16
21
|
: parseLogLevel(process.env.REA_LOG_LEVEL));
|
|
17
22
|
const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
|
|
18
|
-
version: "0.
|
|
23
|
+
version: process.env.REA_PACKAGE_VERSION ?? "0.0.0-development",
|
|
19
24
|
description: "Reverse engineer anything from your terminal or coding agent.",
|
|
20
25
|
mcp: {
|
|
21
26
|
command: PRODUCT_IDENTITY.mcpCommand,
|
|
@@ -31,6 +36,23 @@ export const createCli = () => {
|
|
|
31
36
|
],
|
|
32
37
|
},
|
|
33
38
|
});
|
|
39
|
+
registerCoreCommands(cli, logger);
|
|
40
|
+
registerFunctionCommand(cli, logger);
|
|
41
|
+
registerSearchCommand(cli, logger);
|
|
42
|
+
registerXrefsCommand(cli, logger);
|
|
43
|
+
registerTraceCommand(cli, logger);
|
|
44
|
+
registerCapabilityCommands(cli, logger);
|
|
45
|
+
registerNativeCommands(cli, logger);
|
|
46
|
+
registerArtifactCommands(cli, logger);
|
|
47
|
+
registerEvidenceCommands(cli, logger);
|
|
48
|
+
registerReferenceSourceCommand(cli, logger);
|
|
49
|
+
return cli;
|
|
50
|
+
};
|
|
51
|
+
const registerCoreCommands = (cli, logger) => {
|
|
52
|
+
const overviewOptions = z.object({
|
|
53
|
+
detail: z.enum(["concise", "detailed"]).default("concise"),
|
|
54
|
+
limit: z.number().int().min(1).max(50).default(10),
|
|
55
|
+
});
|
|
34
56
|
cli.command("setup", {
|
|
35
57
|
description: "Install requirements and configure coding agents",
|
|
36
58
|
options: z.object({
|
|
@@ -49,12 +71,32 @@ export const createCli = () => {
|
|
|
49
71
|
}),
|
|
50
72
|
run: ({ options }) => logCliCommand(logger, "doctor", () => runDoctor(options.target)),
|
|
51
73
|
});
|
|
74
|
+
cli.command("uninstall", {
|
|
75
|
+
description: "Remove REA-owned agent configuration and skill files",
|
|
76
|
+
options: z.object({
|
|
77
|
+
purgeData: z
|
|
78
|
+
.boolean()
|
|
79
|
+
.default(false)
|
|
80
|
+
.describe("Also remove REA caches and state"),
|
|
81
|
+
}),
|
|
82
|
+
alias: { purgeData: "purge-data" },
|
|
83
|
+
run: ({ options }) => logCliCommand(logger, "uninstall", () => runUninstall(options.purgeData)),
|
|
84
|
+
});
|
|
52
85
|
cli.command("analyze", {
|
|
53
86
|
description: "Get an overview of an app",
|
|
54
87
|
args: z.object({
|
|
55
88
|
path: z.string().describe("App, program, or Hopper database path"),
|
|
56
89
|
}),
|
|
57
|
-
|
|
90
|
+
options: overviewOptions,
|
|
91
|
+
run: ({ args, options }) => logCliCommand(logger, "analyze", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
|
|
92
|
+
});
|
|
93
|
+
cli.command("inspect", {
|
|
94
|
+
description: "Inspect an app overview with evidence",
|
|
95
|
+
args: z.object({
|
|
96
|
+
path: z.string().describe("App, program, or Hopper database path"),
|
|
97
|
+
}),
|
|
98
|
+
options: overviewOptions,
|
|
99
|
+
run: ({ args, options }) => logCliCommand(logger, "inspect", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
|
|
58
100
|
});
|
|
59
101
|
cli.command("decompile", {
|
|
60
102
|
description: "Read one part of an app as code",
|
|
@@ -64,25 +106,191 @@ export const createCli = () => {
|
|
|
64
106
|
}),
|
|
65
107
|
run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
|
|
66
108
|
});
|
|
67
|
-
return cli;
|
|
68
109
|
};
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
const registerXrefsCommand = (cli, logger) => {
|
|
111
|
+
cli.command("xrefs", {
|
|
112
|
+
description: "List bounded references to an analyzed address",
|
|
113
|
+
args: z.object({
|
|
114
|
+
path: z.string().describe("App or program path"),
|
|
115
|
+
address: z.string().describe("Hexadecimal address"),
|
|
116
|
+
}),
|
|
117
|
+
run: ({ args }) => logCliCommand(logger, "xrefs", () => runDirectAnalysis(args.path, "xrefs", { address: args.address }, logger)),
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
const registerTraceCommand = (cli, logger) => {
|
|
121
|
+
cli.command("trace", {
|
|
122
|
+
description: "Trace a bounded literal feature through analyzed references",
|
|
123
|
+
args: z.object({
|
|
124
|
+
path: z.string().describe("App or program path"),
|
|
125
|
+
query: z.string().min(1).describe("Literal feature query"),
|
|
126
|
+
}),
|
|
127
|
+
options: z.object({
|
|
128
|
+
caseSensitive: z.boolean().default(false),
|
|
129
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
130
|
+
maxOperations: z.number().int().min(1).max(100).default(20),
|
|
131
|
+
}),
|
|
132
|
+
alias: {
|
|
133
|
+
caseSensitive: "case-sensitive",
|
|
134
|
+
maxOperations: "max-operations",
|
|
135
|
+
},
|
|
136
|
+
run: ({ args, options }) => logCliCommand(logger, "trace", () => runDirectAnalysis(args.path, "trace_feature", {
|
|
137
|
+
query: args.query,
|
|
138
|
+
case_sensitive: options.caseSensitive,
|
|
139
|
+
limit: options.limit,
|
|
140
|
+
max_operations: options.maxOperations,
|
|
141
|
+
}, logger)),
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const registerCapabilityCommands = (cli, logger) => {
|
|
145
|
+
for (const command of ["capabilities", "providers"]) {
|
|
146
|
+
cli.command(command, {
|
|
147
|
+
description: command === "capabilities"
|
|
148
|
+
? "List provider capabilities and side effects"
|
|
149
|
+
: "List configured analysis providers",
|
|
150
|
+
run: () => logCliCommand(logger, command, () => runSessionStatus(logger)),
|
|
151
|
+
});
|
|
79
152
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
153
|
+
};
|
|
154
|
+
const registerFunctionCommand = (cli, logger) => {
|
|
155
|
+
cli.command("function", {
|
|
156
|
+
description: "Analyze one bounded function with evidence",
|
|
157
|
+
args: z.object({
|
|
158
|
+
path: z.string().describe("App or program path"),
|
|
159
|
+
address: z.string().describe("Procedure name or address"),
|
|
160
|
+
}),
|
|
161
|
+
options: z.object({
|
|
162
|
+
includeAssembly: z.boolean().default(false),
|
|
163
|
+
limit: z.number().int().min(1).max(500).default(100),
|
|
164
|
+
maxPseudocodeChars: z.number().int().min(1).max(100_000).default(20_000),
|
|
165
|
+
maxInstructions: z.number().int().min(1).max(5_000).default(500),
|
|
166
|
+
}),
|
|
167
|
+
alias: {
|
|
168
|
+
includeAssembly: "include-assembly",
|
|
169
|
+
maxPseudocodeChars: "max-pseudocode-chars",
|
|
170
|
+
maxInstructions: "max-instructions",
|
|
171
|
+
},
|
|
172
|
+
run: ({ args, options }) => logCliCommand(logger, "function", () => runDirectAnalysis(args.path, "analyze_function", {
|
|
173
|
+
procedure: args.address,
|
|
174
|
+
include_assembly: options.includeAssembly,
|
|
175
|
+
limit: options.limit,
|
|
176
|
+
max_pseudocode_chars: options.maxPseudocodeChars,
|
|
177
|
+
max_instructions: options.maxInstructions,
|
|
178
|
+
}, logger)),
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
const registerSearchCommand = (cli, logger) => {
|
|
182
|
+
cli.command("search", {
|
|
183
|
+
description: "Search bounded analyzed strings or procedure names",
|
|
184
|
+
args: z.object({
|
|
185
|
+
path: z.string().describe("App or program path"),
|
|
186
|
+
pattern: z.string().min(1).describe("Literal text or regex pattern"),
|
|
187
|
+
}),
|
|
188
|
+
options: z.object({
|
|
189
|
+
kind: z.enum(["strings", "procedures"]).default("strings"),
|
|
190
|
+
mode: z.enum(["literal", "regex"]).default("literal"),
|
|
191
|
+
caseSensitive: z.boolean().default(false),
|
|
192
|
+
offset: z.number().int().min(0).default(0),
|
|
193
|
+
limit: z.number().int().min(1).max(100).default(100),
|
|
194
|
+
}),
|
|
195
|
+
alias: { caseSensitive: "case-sensitive" },
|
|
196
|
+
run: ({ args, options }) => logCliCommand(logger, "search", () => runDirectAnalysis(args.path, options.kind === "strings" ? "search_strings" : "search_procedures", {
|
|
197
|
+
pattern: args.pattern,
|
|
198
|
+
mode: options.mode,
|
|
199
|
+
case_sensitive: options.caseSensitive,
|
|
200
|
+
offset: options.offset,
|
|
201
|
+
limit: options.limit,
|
|
202
|
+
}, logger)),
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
const registerReferenceSourceCommand = (cli, logger) => {
|
|
206
|
+
cli.command("import-reference-source", {
|
|
207
|
+
description: "Import a bounded source tree as historical reference only",
|
|
208
|
+
args: z.object({
|
|
209
|
+
root: z
|
|
210
|
+
.string()
|
|
211
|
+
.describe("Source root allowed by REA_REFERENCE_ROOTS_JSON"),
|
|
212
|
+
}),
|
|
213
|
+
run: ({ args }) => logCliCommand(logger, "import-reference-source", async () => {
|
|
214
|
+
const config = parseConfig(process.env);
|
|
215
|
+
if (!config.ok)
|
|
216
|
+
return { error: config.error._tag, message: config.error.message };
|
|
217
|
+
const imported = await importReferenceSource({
|
|
218
|
+
root: args.root,
|
|
219
|
+
caller: "rea-cli",
|
|
220
|
+
policy: config.value.referenceSourcePolicy,
|
|
221
|
+
importer: PRODUCT_IDENTITY.packageName,
|
|
222
|
+
importerVersion: null,
|
|
223
|
+
});
|
|
224
|
+
return imported.ok
|
|
225
|
+
? imported.value
|
|
226
|
+
: { error: imported.error.code, message: imported.error.message };
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
const registerArtifactCommands = (cli, logger) => {
|
|
231
|
+
cli.command("inventory-artifact", {
|
|
232
|
+
description: "Build a bounded deterministic artifact graph",
|
|
233
|
+
args: z.object({
|
|
234
|
+
path: z.string().describe("Application or package path"),
|
|
235
|
+
}),
|
|
236
|
+
options: z.object({
|
|
237
|
+
offset: z.number().int().min(0).default(0),
|
|
238
|
+
limit: z.number().int().min(1).max(500).default(100),
|
|
239
|
+
}),
|
|
240
|
+
run: ({ args, options }) => logCliCommand(logger, "inventory-artifact", () => runProviderAnalysis(args.path, "inventory_artifact", {
|
|
241
|
+
node_offset: options.offset,
|
|
242
|
+
node_limit: options.limit,
|
|
243
|
+
occurrence_offset: options.offset,
|
|
244
|
+
occurrence_limit: options.limit,
|
|
245
|
+
edge_offset: options.offset,
|
|
246
|
+
edge_limit: options.limit,
|
|
247
|
+
}, logger)),
|
|
248
|
+
});
|
|
249
|
+
cli.command("extract-artifact", {
|
|
250
|
+
description: "Extract explicitly selected artifact occurrences safely",
|
|
251
|
+
args: z.object({
|
|
252
|
+
path: z.string().describe("Application or package path"),
|
|
253
|
+
outputRoot: z.string().describe("Absent absolute output root"),
|
|
254
|
+
occurrenceIds: z
|
|
255
|
+
.array(z.string().regex(/^occ_[a-f0-9]{64}$/u))
|
|
256
|
+
.min(1)
|
|
257
|
+
.max(500),
|
|
258
|
+
}),
|
|
259
|
+
alias: { outputRoot: "output-root", occurrenceIds: "occurrence-ids" },
|
|
260
|
+
run: ({ args }) => logCliCommand(logger, "extract-artifact", () => runProviderAnalysis(args.path, "extract_artifact", {
|
|
261
|
+
approved: true,
|
|
262
|
+
output_root: args.outputRoot,
|
|
263
|
+
occurrence_ids: args.occurrenceIds,
|
|
264
|
+
}, logger)),
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const registerNativeCommands = (cli, logger) => {
|
|
268
|
+
for (const [command, tool] of [
|
|
269
|
+
["inspect-macho", "inspect_macho"],
|
|
270
|
+
["inspect-signature", "inspect_signature"],
|
|
271
|
+
["list-architectures", "list_architectures"],
|
|
272
|
+
]) {
|
|
273
|
+
cli.command(command, {
|
|
274
|
+
description: `Run ${tool} without launching Hopper`,
|
|
275
|
+
args: z.object({ path: z.string().describe("Mach-O or app path") }),
|
|
276
|
+
run: ({ args }) => logCliCommand(logger, command, () => runProviderAnalysis(args.path, tool, {}, logger)),
|
|
277
|
+
});
|
|
87
278
|
}
|
|
279
|
+
cli.command("inspect-plist", {
|
|
280
|
+
description: "Parse app plist metadata without launching Hopper",
|
|
281
|
+
args: z.object({ path: z.string().describe("App or Mach-O path") }),
|
|
282
|
+
options: z.object({
|
|
283
|
+
relativePath: z.string().default("Contents/Info.plist"),
|
|
284
|
+
}),
|
|
285
|
+
alias: { relativePath: "relative-path" },
|
|
286
|
+
run: ({ args, options }) => logCliCommand(logger, "inspect-plist", () => runProviderAnalysis(args.path, "inspect_plist", { relative_path: options.relativePath }, logger)),
|
|
287
|
+
});
|
|
288
|
+
cli.command("demangle-swift", {
|
|
289
|
+
description: "Demangle a bounded Swift symbol batch without Hopper",
|
|
290
|
+
args: z.object({
|
|
291
|
+
path: z.string().describe("Artifact path used for evidence identity"),
|
|
292
|
+
symbols: z.array(z.string().min(1)).min(1).max(500),
|
|
293
|
+
}),
|
|
294
|
+
run: ({ args }) => logCliCommand(logger, "demangle-swift", () => runProviderAnalysis(args.path, "demangle_swift", { symbols: args.symbols }, logger)),
|
|
295
|
+
});
|
|
88
296
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Cli, z } from "incur";
|
|
2
|
+
import { compareEvidenceBundlesCommand, exportEvidenceBundleCommand, importEvidenceBundleCommand, } from "./application/EvidenceBundleCommands.js";
|
|
3
|
+
import { parseConfig } from "./config.js";
|
|
4
|
+
import { logCliCommand } from "./cliLogging.js";
|
|
5
|
+
/** Register filesystem-gated Evidence v2 commands. */
|
|
6
|
+
export const registerEvidenceCommands = (cli, logger) => {
|
|
7
|
+
cli.command("evidence-import", {
|
|
8
|
+
description: "Validate and import a bounded local Evidence v2 bundle",
|
|
9
|
+
args: z.object({
|
|
10
|
+
path: z.string().describe("Evidence bundle JSON path"),
|
|
11
|
+
}),
|
|
12
|
+
run: ({ args }) => logCliCommand(logger, "evidence-import", async () => {
|
|
13
|
+
const config = parseConfig(process.env);
|
|
14
|
+
if (!config.ok)
|
|
15
|
+
return { error: config.error._tag, message: config.error.message };
|
|
16
|
+
const imported = await importEvidenceBundleCommand(args.path, config.value.evidenceFilePolicy);
|
|
17
|
+
return imported.ok
|
|
18
|
+
? imported.value
|
|
19
|
+
: { error: imported.error._tag, message: imported.error.message };
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
cli.command("evidence-export", {
|
|
23
|
+
description: "Validate and atomically export canonical Evidence v2 JSON",
|
|
24
|
+
args: z.object({
|
|
25
|
+
source: z.string().describe("Existing evidence bundle JSON path"),
|
|
26
|
+
output: z.string().describe("Canonical output JSON path"),
|
|
27
|
+
}),
|
|
28
|
+
options: z.object({
|
|
29
|
+
overwrite: z.boolean().default(false).describe("Replace output file"),
|
|
30
|
+
}),
|
|
31
|
+
run: ({ args, options }) => logCliCommand(logger, "evidence-export", async () => {
|
|
32
|
+
const config = parseConfig(process.env);
|
|
33
|
+
if (!config.ok)
|
|
34
|
+
return { error: config.error._tag, message: config.error.message };
|
|
35
|
+
const exported = await exportEvidenceBundleCommand(args.source, args.output, options.overwrite, config.value.evidenceFilePolicy);
|
|
36
|
+
return exported.ok
|
|
37
|
+
? exported.value
|
|
38
|
+
: { error: exported.error._tag, message: exported.error.message };
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
cli.command("compare", {
|
|
42
|
+
aliases: ["compare-bundles"],
|
|
43
|
+
description: "Compare two canonical Evidence v2 bundles",
|
|
44
|
+
args: z.object({
|
|
45
|
+
left: z.string().describe("Left Evidence bundle JSON path"),
|
|
46
|
+
right: z.string().describe("Right Evidence bundle JSON path"),
|
|
47
|
+
}),
|
|
48
|
+
options: z.object({
|
|
49
|
+
offset: z.number().int().min(0).default(0),
|
|
50
|
+
limit: z.number().int().min(1).max(500).default(100),
|
|
51
|
+
}),
|
|
52
|
+
run: ({ args, options }) => logCliCommand(logger, "compare", async () => {
|
|
53
|
+
const config = parseConfig(process.env);
|
|
54
|
+
if (!config.ok)
|
|
55
|
+
return { error: config.error._tag, message: config.error.message };
|
|
56
|
+
const compared = await compareEvidenceBundlesCommand({
|
|
57
|
+
leftPath: args.left,
|
|
58
|
+
rightPath: args.right,
|
|
59
|
+
offset: options.offset,
|
|
60
|
+
limit: options.limit,
|
|
61
|
+
policy: config.value.evidenceFilePolicy,
|
|
62
|
+
});
|
|
63
|
+
return compared.ok
|
|
64
|
+
? compared.value
|
|
65
|
+
: { error: compared.error._tag, message: compared.error.message };
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
};
|