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,306 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { parse as parseXmlPlist } from "plist";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createAnalysisExecution, } from "../application/AnalysisProvider.js";
|
|
6
|
+
import { NATIVE_TOOL_CONTRACTS, } from "../contracts/nativeToolContracts.js";
|
|
7
|
+
import { AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisOutputError, AnalysisTimeoutError, ProviderAdapterError, } from "../domain/errors.js";
|
|
8
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
9
|
+
import { demangleSwiftSchema, inspectPlistSchema, inspectSignatureSchema, listArchitecturesSchema, } from "../domain/nativeInspection.js";
|
|
10
|
+
import { err, ok } from "../domain/result.js";
|
|
11
|
+
import { NativeCommandFailure, XcrunCommandRunner, } from "./CommandRunner.js";
|
|
12
|
+
import { parseCodeSignature } from "./parsers/codesign.js";
|
|
13
|
+
import { parseDemangledSymbols } from "./parsers/demangle.js";
|
|
14
|
+
import { parseLipoArchitectures } from "./parsers/lipo.js";
|
|
15
|
+
import { parsePlistJson } from "./parsers/plist.js";
|
|
16
|
+
import { architectureLocations, inspectNativeMacho, } from "./NativeMachoInspection.js";
|
|
17
|
+
const IDENTITY = Object.freeze({
|
|
18
|
+
id: "native-macos",
|
|
19
|
+
name: "macOS native inspection utilities",
|
|
20
|
+
version: null,
|
|
21
|
+
});
|
|
22
|
+
const TIMEOUT_MS = 30_000;
|
|
23
|
+
const OUTPUT_LIMIT = 4 * 1024 * 1024;
|
|
24
|
+
/** Read-only semantic provider composed from Xcode command-line utilities. */
|
|
25
|
+
export class NativeMacOSProvider {
|
|
26
|
+
runner;
|
|
27
|
+
#capabilities;
|
|
28
|
+
constructor(runner = new XcrunCommandRunner(), platform = process.platform) {
|
|
29
|
+
this.runner = runner;
|
|
30
|
+
const available = platform === "darwin";
|
|
31
|
+
this.#capabilities = Object.freeze(NATIVE_TOOL_CONTRACTS.map((contract) => {
|
|
32
|
+
const availability = available
|
|
33
|
+
? { available: true, reason: null }
|
|
34
|
+
: {
|
|
35
|
+
available: false,
|
|
36
|
+
reason: "Native macOS utilities require macOS.",
|
|
37
|
+
};
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
provider: IDENTITY,
|
|
40
|
+
operation: contract.name,
|
|
41
|
+
inputContractVersion: 1,
|
|
42
|
+
outputContractVersion: 1,
|
|
43
|
+
...availability,
|
|
44
|
+
pagination: "none",
|
|
45
|
+
exhaustive: contract.name !== "inspect_macho",
|
|
46
|
+
effects: Object.freeze({
|
|
47
|
+
mutatesArtifact: false,
|
|
48
|
+
launchesProcess: true,
|
|
49
|
+
mayShowUi: false,
|
|
50
|
+
mayAccessNetwork: false,
|
|
51
|
+
mayWriteFilesystem: false,
|
|
52
|
+
changesPermissions: false,
|
|
53
|
+
requiresRoot: false,
|
|
54
|
+
}),
|
|
55
|
+
limits: Object.freeze({
|
|
56
|
+
maxResults: contract.name === "demangle_swift" ? 500 : null,
|
|
57
|
+
maxPayloadBytes: OUTPUT_LIMIT,
|
|
58
|
+
timeoutMs: TIMEOUT_MS,
|
|
59
|
+
}),
|
|
60
|
+
limitations: Object.freeze([
|
|
61
|
+
"Availability and textual formats depend on the installed macOS/Xcode toolchain.",
|
|
62
|
+
]),
|
|
63
|
+
});
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
identity() {
|
|
67
|
+
return IDENTITY;
|
|
68
|
+
}
|
|
69
|
+
capabilities() {
|
|
70
|
+
return this.#capabilities;
|
|
71
|
+
}
|
|
72
|
+
createClient(target) {
|
|
73
|
+
return new NativeMacOSClient(target, this.runner);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
class NativeMacOSClient {
|
|
77
|
+
target;
|
|
78
|
+
runner;
|
|
79
|
+
constructor(target, runner) {
|
|
80
|
+
this.target = target;
|
|
81
|
+
this.runner = runner;
|
|
82
|
+
}
|
|
83
|
+
async execute(operation, parameters, options) {
|
|
84
|
+
if (operation === "health")
|
|
85
|
+
return ok(createAnalysisExecution(null, IDENTITY));
|
|
86
|
+
if (!isNativeOperation(operation))
|
|
87
|
+
return err(new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, "Operation is not implemented by native macOS tools."));
|
|
88
|
+
try {
|
|
89
|
+
const observation = await this.#dispatch(operation, parameters, options?.signal);
|
|
90
|
+
return observation.ok
|
|
91
|
+
? ok(createAnalysisExecution(observation.value.result, IDENTITY, {
|
|
92
|
+
rawResult: { provenance: observation.value.provenance },
|
|
93
|
+
limitations: observation.value.limitations,
|
|
94
|
+
locations: observation.value.locations,
|
|
95
|
+
}))
|
|
96
|
+
: observation;
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
return err(new AnalysisOutputError(operation, "Native output parsing failed", {
|
|
100
|
+
cause,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
close() {
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
#dispatch(operation, parameters, signal) {
|
|
108
|
+
switch (operation) {
|
|
109
|
+
case "inspect_macho":
|
|
110
|
+
return this.#inspectMacho(signal);
|
|
111
|
+
case "inspect_signature":
|
|
112
|
+
return this.#inspectSignature(signal);
|
|
113
|
+
case "inspect_plist":
|
|
114
|
+
return this.#inspectPlist(parameters, signal);
|
|
115
|
+
case "list_architectures":
|
|
116
|
+
return this.#listArchitectures(signal);
|
|
117
|
+
case "demangle_swift":
|
|
118
|
+
return this.#demangle(parameters, signal);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async #listArchitectures(signal) {
|
|
122
|
+
const capture = await this.#run("list_architectures", "lipo", ["-detailed_info", this.target.path], { signal });
|
|
123
|
+
if (!capture.ok)
|
|
124
|
+
return capture;
|
|
125
|
+
const provenance = [invocation(capture.value, this.target.path)];
|
|
126
|
+
const result = listArchitecturesSchema.parse({
|
|
127
|
+
architectures: {
|
|
128
|
+
items: parseLipoArchitectures(capture.value.stdout),
|
|
129
|
+
total: parseLipoArchitectures(capture.value.stdout).length,
|
|
130
|
+
exhaustive: true,
|
|
131
|
+
limitations: [],
|
|
132
|
+
},
|
|
133
|
+
provenance,
|
|
134
|
+
limitations: [],
|
|
135
|
+
});
|
|
136
|
+
return ok({
|
|
137
|
+
result: jsonValueSchema.parse(result),
|
|
138
|
+
provenance,
|
|
139
|
+
limitations: [],
|
|
140
|
+
locations: architectureLocations(result.architectures.items),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async #demangle(parameters, signal) {
|
|
144
|
+
const symbols = z.array(z.string()).safeParse(parameters.symbols);
|
|
145
|
+
if (!symbols.success)
|
|
146
|
+
return err(new AnalysisOutputError("demangle_swift", "symbols were not parsed strings"));
|
|
147
|
+
const capture = await this.#run("demangle_swift", "swift-demangle", ["--compact", ...symbols.data], { signal });
|
|
148
|
+
if (!capture.ok)
|
|
149
|
+
return capture;
|
|
150
|
+
const provenance = [invocation(capture.value, this.target.path)];
|
|
151
|
+
const result = demangleSwiftSchema.parse({
|
|
152
|
+
symbols: parseDemangledSymbols(symbols.data, capture.value.stdout),
|
|
153
|
+
provenance,
|
|
154
|
+
limitations: [],
|
|
155
|
+
});
|
|
156
|
+
return ok({
|
|
157
|
+
result: jsonValueSchema.parse(result),
|
|
158
|
+
provenance,
|
|
159
|
+
limitations: [],
|
|
160
|
+
locations: [],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async #inspectSignature(signal) {
|
|
164
|
+
const display = await this.#run("inspect_signature", "codesign", ["-d", "--verbose=4", this.target.path], { signal, acceptNonZero: true });
|
|
165
|
+
if (!display.ok)
|
|
166
|
+
return display;
|
|
167
|
+
const requirements = await this.#run("inspect_signature", "codesign", ["-d", "-r-", this.target.path], { signal, acceptNonZero: true });
|
|
168
|
+
if (!requirements.ok)
|
|
169
|
+
return requirements;
|
|
170
|
+
const entitlements = await this.#run("inspect_signature", "codesign", ["-d", "--entitlements", ":-", this.target.path], { signal, acceptNonZero: true });
|
|
171
|
+
if (!entitlements.ok)
|
|
172
|
+
return entitlements;
|
|
173
|
+
const unsigned = /not signed at all|code object is not signed/iu.test(`${display.value.stdout}\n${display.value.stderr}`);
|
|
174
|
+
const parsed = parseCodeSignature(`${display.value.stdout}\n${display.value.stderr}`, unsigned);
|
|
175
|
+
const requirementText = /designated\s*=>\s*(.+)$/mu.exec(`${requirements.value.stdout}\n${requirements.value.stderr}`)?.[1] ?? null;
|
|
176
|
+
const entitlementValue = parseEntitlements(`${entitlements.value.stdout}\n${entitlements.value.stderr}`);
|
|
177
|
+
const provenance = [
|
|
178
|
+
display.value,
|
|
179
|
+
requirements.value,
|
|
180
|
+
entitlements.value,
|
|
181
|
+
].map((capture) => invocation(capture, this.target.path));
|
|
182
|
+
const result = inspectSignatureSchema.parse({
|
|
183
|
+
...parsed,
|
|
184
|
+
designated_requirement: requirementText,
|
|
185
|
+
entitlements: entitlementValue,
|
|
186
|
+
provenance,
|
|
187
|
+
});
|
|
188
|
+
return ok({
|
|
189
|
+
result: jsonValueSchema.parse(result),
|
|
190
|
+
provenance,
|
|
191
|
+
limitations: result.limitations,
|
|
192
|
+
locations: [],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async #inspectPlist(parameters, signal) {
|
|
196
|
+
const requested = parameters.relative_path;
|
|
197
|
+
if (typeof requested !== "string")
|
|
198
|
+
return err(new AnalysisOutputError("inspect_plist", "relative_path was not a string"));
|
|
199
|
+
const plist = await resolvePlistPath(this.target, requested);
|
|
200
|
+
if (!plist.ok)
|
|
201
|
+
return plist;
|
|
202
|
+
const classified = await this.#run("inspect_plist", "file", ["-b", plist.value], { signal });
|
|
203
|
+
if (!classified.ok)
|
|
204
|
+
return classified;
|
|
205
|
+
const capture = await this.#run("inspect_plist", "plutil", ["-convert", "json", "-o", "-", "--", plist.value], { signal });
|
|
206
|
+
if (!capture.ok)
|
|
207
|
+
return capture;
|
|
208
|
+
const parsed = parsePlistJson(capture.value.stdout);
|
|
209
|
+
const provenance = [classified.value, capture.value].map((item) => invocation(item, plist.value, "$PLIST"));
|
|
210
|
+
const result = inspectPlistSchema.parse({
|
|
211
|
+
format: /binary property list/iu.test(classified.value.stdout)
|
|
212
|
+
? "binary"
|
|
213
|
+
: /XML|text/iu.test(classified.value.stdout)
|
|
214
|
+
? "xml"
|
|
215
|
+
: "unknown",
|
|
216
|
+
...parsed,
|
|
217
|
+
source_path: requested,
|
|
218
|
+
provenance,
|
|
219
|
+
limitations: [],
|
|
220
|
+
});
|
|
221
|
+
return ok({
|
|
222
|
+
result: jsonValueSchema.parse(result),
|
|
223
|
+
provenance,
|
|
224
|
+
limitations: [],
|
|
225
|
+
locations: [{ kind: "artifact-path", path: requested }],
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async #inspectMacho(signal) {
|
|
229
|
+
if (this.target.format !== "mach-o")
|
|
230
|
+
return err(new AnalysisCapabilityUnavailableError(IDENTITY.id, "inspect_macho", "Active artifact is not Mach-O."));
|
|
231
|
+
return inspectNativeMacho({
|
|
232
|
+
target: this.target,
|
|
233
|
+
...(signal === undefined ? {} : { signal }),
|
|
234
|
+
run: (tool, arguments_, commandSignal) => this.#run("inspect_macho", tool, arguments_, {
|
|
235
|
+
signal: commandSignal,
|
|
236
|
+
}),
|
|
237
|
+
invocation: (capture) => invocation(capture, this.target.path),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async #run(operation, tool, arguments_, options = {}) {
|
|
241
|
+
const captured = await this.runner.run(tool, arguments_, {
|
|
242
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
243
|
+
timeoutMs: TIMEOUT_MS,
|
|
244
|
+
maxOutputBytes: OUTPUT_LIMIT,
|
|
245
|
+
acceptNonZero: options.acceptNonZero ?? false,
|
|
246
|
+
});
|
|
247
|
+
if (captured.ok)
|
|
248
|
+
return captured;
|
|
249
|
+
return err(translateCommandFailure(operation, captured.error));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const isNativeOperation = (operation) => NATIVE_TOOL_CONTRACTS.some(({ name }) => name === operation);
|
|
253
|
+
const translateCommandFailure = (operation, failure) => {
|
|
254
|
+
if (failure.reason === "unavailable")
|
|
255
|
+
return new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, `${failure.tool} is unavailable through xcrun.`);
|
|
256
|
+
if (failure.reason === "cancelled")
|
|
257
|
+
return new AnalysisCancelledError(operation);
|
|
258
|
+
if (failure.reason === "timeout")
|
|
259
|
+
return new AnalysisTimeoutError(operation, TIMEOUT_MS);
|
|
260
|
+
return new ProviderAdapterError(IDENTITY.id, operation, { cause: failure });
|
|
261
|
+
};
|
|
262
|
+
const invocation = (capture, artifactPath, alias = "$ARTIFACT") => ({
|
|
263
|
+
tool: capture.tool,
|
|
264
|
+
command: [
|
|
265
|
+
capture.executable,
|
|
266
|
+
...capture.arguments.map((argument) => argument === artifactPath ? alias : argument),
|
|
267
|
+
],
|
|
268
|
+
tool_version: capture.toolVersion,
|
|
269
|
+
version_reason: capture.versionReason,
|
|
270
|
+
executable_sha256: capture.executableSha256,
|
|
271
|
+
exit: { code: capture.exitCode, signal: capture.signal },
|
|
272
|
+
stdout_bytes: capture.stdoutBytes,
|
|
273
|
+
stderr_bytes: capture.stderrBytes,
|
|
274
|
+
stdout_truncated: capture.stdoutTruncated,
|
|
275
|
+
stderr_truncated: capture.stderrTruncated,
|
|
276
|
+
});
|
|
277
|
+
const parseEntitlements = (output) => {
|
|
278
|
+
const start = output.indexOf("<?xml");
|
|
279
|
+
const end = output.lastIndexOf("</plist>");
|
|
280
|
+
if (start < 0 || end < start)
|
|
281
|
+
return null;
|
|
282
|
+
return jsonValueSchema.parse(parseXmlPlist(output.slice(start, end + "</plist>".length)));
|
|
283
|
+
};
|
|
284
|
+
const resolvePlistPath = async (target, requested) => {
|
|
285
|
+
try {
|
|
286
|
+
const source = target.sourcePath ?? target.path;
|
|
287
|
+
const sourceMetadata = await stat(source);
|
|
288
|
+
const root = sourceMetadata.isDirectory() ? source : dirname(target.path);
|
|
289
|
+
const candidate = resolve(root, requested);
|
|
290
|
+
if (!within(root, candidate))
|
|
291
|
+
return err(new ProviderAdapterError(IDENTITY.id, "inspect_plist"));
|
|
292
|
+
const canonicalRoot = await realpath(root);
|
|
293
|
+
const canonical = await realpath(candidate);
|
|
294
|
+
if (!within(canonicalRoot, canonical))
|
|
295
|
+
return err(new ProviderAdapterError(IDENTITY.id, "inspect_plist"));
|
|
296
|
+
return ok(canonical);
|
|
297
|
+
}
|
|
298
|
+
catch (cause) {
|
|
299
|
+
return err(new ProviderAdapterError(IDENTITY.id, "inspect_plist", { cause }));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const within = (root, candidate) => {
|
|
303
|
+
const child = relative(root, candidate);
|
|
304
|
+
return (child === "" ||
|
|
305
|
+
(!child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child)));
|
|
306
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { inspectMachoSchema, } from "../domain/nativeInspection.js";
|
|
2
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
3
|
+
import { err, ok } from "../domain/result.js";
|
|
4
|
+
import { parseDyldSymbols } from "./parsers/dyldInfo.js";
|
|
5
|
+
import { parseLipoArchitectures } from "./parsers/lipo.js";
|
|
6
|
+
import { parseOtoolLoadCommands } from "./parsers/otool.js";
|
|
7
|
+
const COMMANDS = [
|
|
8
|
+
["file", ["-b"]],
|
|
9
|
+
["lipo", ["-detailed_info"]],
|
|
10
|
+
["otool", ["-l"]],
|
|
11
|
+
["nm", ["-gjU"]],
|
|
12
|
+
["dyld_info", ["-imports"]],
|
|
13
|
+
["dyld_info", ["-exports"]],
|
|
14
|
+
["dwarfdump", ["--uuid"]],
|
|
15
|
+
["vtool", ["-show-build"]],
|
|
16
|
+
];
|
|
17
|
+
/** Inspect one Mach-O with bounded native commands and normalized output. */
|
|
18
|
+
export const inspectNativeMacho = async (context) => {
|
|
19
|
+
const captures = [];
|
|
20
|
+
for (const [tool, prefix] of COMMANDS) {
|
|
21
|
+
const captured = await context.run(tool, [...prefix, context.target.path], context.signal);
|
|
22
|
+
if (!captured.ok)
|
|
23
|
+
return err(captured.error);
|
|
24
|
+
captures.push(captured.value);
|
|
25
|
+
}
|
|
26
|
+
const result = normalizeMacho(captures, context.invocation);
|
|
27
|
+
return ok({
|
|
28
|
+
result: jsonValueSchema.parse(result),
|
|
29
|
+
provenance: result.provenance,
|
|
30
|
+
limitations: result.limitations,
|
|
31
|
+
locations: fileOffsetLocations(result),
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const normalizeMacho = (captures, toInvocation) => {
|
|
35
|
+
const byTool = captureLookup(captures);
|
|
36
|
+
const architectures = parseLipoArchitectures(byTool("lipo").stdout);
|
|
37
|
+
const load = parseOtoolLoadCommands(byTool("otool").stdout);
|
|
38
|
+
const imports = parseDyldSymbols(byTool("dyld_info", 0).stdout, "imports");
|
|
39
|
+
const dyldExports = parseDyldSymbols(byTool("dyld_info", 1).stdout, "exports");
|
|
40
|
+
const exports = uniqueSymbols([
|
|
41
|
+
...dyldExports,
|
|
42
|
+
...parseNmExports(byTool("nm").stdout),
|
|
43
|
+
]);
|
|
44
|
+
const uuid = /UUID:\s*([A-Fa-f0-9-]+)/u.exec(byTool("dwarfdump").stdout)?.[1] ??
|
|
45
|
+
load.uuid;
|
|
46
|
+
const provenance = captures.map(toInvocation);
|
|
47
|
+
const limitations = [
|
|
48
|
+
"Imports and exports combine dyld_info and nm; stripped or toolchain-hidden symbols may be absent.",
|
|
49
|
+
"vtool output is retained as provenance but only otool build metadata is normalized.",
|
|
50
|
+
];
|
|
51
|
+
return inspectMachoSchema.parse({
|
|
52
|
+
format: "mach-o",
|
|
53
|
+
endian: parseEndian(byTool("file").stdout),
|
|
54
|
+
word_size: parseWordSize(byTool("file").stdout),
|
|
55
|
+
file_type: load.fileType,
|
|
56
|
+
flags: load.flags,
|
|
57
|
+
uuid: uuid ?? null,
|
|
58
|
+
entrypoints: covered(load.entrypoints, true),
|
|
59
|
+
architectures: covered(architectures, true),
|
|
60
|
+
build_metadata: covered(load.builds, true),
|
|
61
|
+
load_commands: covered(load.commands, true),
|
|
62
|
+
dependencies: covered(load.dependencies, true),
|
|
63
|
+
imports: covered(imports, false, [
|
|
64
|
+
"dyld_info textual imports may omit chained or toolchain-unsupported metadata.",
|
|
65
|
+
]),
|
|
66
|
+
exports: covered(exports, false, [
|
|
67
|
+
"Merged nm/dyld_info results may be incomplete for stripped binaries.",
|
|
68
|
+
]),
|
|
69
|
+
segments: covered(load.segments.map((segment) => ({
|
|
70
|
+
...segment,
|
|
71
|
+
sections: covered(segment.sections, true),
|
|
72
|
+
})), true),
|
|
73
|
+
provenance,
|
|
74
|
+
limitations,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const captureLookup = (captures) => (tool, occurrence = 0) => {
|
|
78
|
+
const capture = captures.filter((item) => item.tool === tool)[occurrence];
|
|
79
|
+
if (capture === undefined)
|
|
80
|
+
throw new TypeError(`Missing ${tool} capture`);
|
|
81
|
+
return capture;
|
|
82
|
+
};
|
|
83
|
+
const parseNmExports = (output) => output
|
|
84
|
+
.split(/\r?\n/u)
|
|
85
|
+
.filter((name) => name.length > 0)
|
|
86
|
+
.map((name) => ({
|
|
87
|
+
name,
|
|
88
|
+
address: null,
|
|
89
|
+
weak: null,
|
|
90
|
+
reexport: null,
|
|
91
|
+
source: "nm",
|
|
92
|
+
}));
|
|
93
|
+
const parseEndian = (output) => /little-endian/iu.test(output)
|
|
94
|
+
? "little"
|
|
95
|
+
: /big-endian/iu.test(output)
|
|
96
|
+
? "big"
|
|
97
|
+
: null;
|
|
98
|
+
const parseWordSize = (output) => /64-bit/iu.test(output) ? 64 : /32-bit/iu.test(output) ? 32 : null;
|
|
99
|
+
const covered = (items, exhaustive, limitations = []) => ({
|
|
100
|
+
items: [...items],
|
|
101
|
+
total: exhaustive ? items.length : null,
|
|
102
|
+
exhaustive,
|
|
103
|
+
limitations: [...limitations],
|
|
104
|
+
});
|
|
105
|
+
const uniqueSymbols = (items) => {
|
|
106
|
+
const unique = new Map();
|
|
107
|
+
for (const item of items)
|
|
108
|
+
unique.set(item.name, item);
|
|
109
|
+
return [...unique.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
110
|
+
};
|
|
111
|
+
const fileOffsetLocations = (macho) => {
|
|
112
|
+
const locations = architectureLocations(macho.architectures.items);
|
|
113
|
+
for (const segment of macho.segments.items) {
|
|
114
|
+
if (segment.file_offset === null || segment.file_size === null)
|
|
115
|
+
continue;
|
|
116
|
+
locations.push({
|
|
117
|
+
kind: "file-offset-range",
|
|
118
|
+
start: segment.file_offset,
|
|
119
|
+
end: segment.file_offset + segment.file_size,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return locations;
|
|
123
|
+
};
|
|
124
|
+
/** Project architecture slices into evidence file-offset locations. */
|
|
125
|
+
export const architectureLocations = (architectures) => {
|
|
126
|
+
const locations = [];
|
|
127
|
+
for (const { file_offset: offset, size } of architectures) {
|
|
128
|
+
if (offset === null)
|
|
129
|
+
continue;
|
|
130
|
+
locations.push(size === null
|
|
131
|
+
? { kind: "file-offset", offset }
|
|
132
|
+
: { kind: "file-offset-range", start: offset, end: offset + size });
|
|
133
|
+
}
|
|
134
|
+
return locations;
|
|
135
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { inspectSignatureSchema, } from "../../domain/nativeInspection.js";
|
|
2
|
+
/** Parse bounded `codesign -d` diagnostics, which Apple emits on stderr. */
|
|
3
|
+
export const parseCodeSignature = (output, unsigned) => {
|
|
4
|
+
const values = new Map();
|
|
5
|
+
const authorities = [];
|
|
6
|
+
const cdhashes = [];
|
|
7
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
8
|
+
if (line.startsWith("CodeDirectory ")) {
|
|
9
|
+
values.set("CodeDirectory", line.slice("CodeDirectory ".length));
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const separator = line.indexOf("=");
|
|
13
|
+
if (separator < 1)
|
|
14
|
+
continue;
|
|
15
|
+
const key = line.slice(0, separator).trim();
|
|
16
|
+
const value = line.slice(separator + 1).trim();
|
|
17
|
+
if (key === "Authority")
|
|
18
|
+
authorities.push(value);
|
|
19
|
+
else if (key === "CDHash")
|
|
20
|
+
cdhashes.push(value);
|
|
21
|
+
else
|
|
22
|
+
values.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
const parsed = inspectSignatureSchema.omit({ provenance: true }).parse({
|
|
25
|
+
signed: !unsigned,
|
|
26
|
+
identifier: values.get("Identifier") ?? null,
|
|
27
|
+
team_identifier: nullableCodeSignValue(values.get("TeamIdentifier")),
|
|
28
|
+
format: values.get("Format") ?? null,
|
|
29
|
+
cdhashes,
|
|
30
|
+
hash_algorithms: splitAlgorithms(values.get("Hash choices")),
|
|
31
|
+
authorities,
|
|
32
|
+
designated_requirement: values.get("designated") ?? null,
|
|
33
|
+
entitlements: null,
|
|
34
|
+
timestamp: values.get("Timestamp") ?? null,
|
|
35
|
+
hardened_runtime: parseRuntime(values.get("CodeDirectory")),
|
|
36
|
+
limitations: unsigned
|
|
37
|
+
? ["Artifact is not signed."]
|
|
38
|
+
: [
|
|
39
|
+
"Entitlements and designated requirements require separate bounded commands.",
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
return parsed;
|
|
43
|
+
};
|
|
44
|
+
const nullableCodeSignValue = (value) => value === undefined || value === "not set" ? null : value;
|
|
45
|
+
const splitAlgorithms = (value) => value === undefined
|
|
46
|
+
? []
|
|
47
|
+
: value
|
|
48
|
+
.split(/[,+\s]+/u)
|
|
49
|
+
.map((part) => part.trim())
|
|
50
|
+
.filter((part) => part.length > 0);
|
|
51
|
+
const parseRuntime = (value) => {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return null;
|
|
54
|
+
return /\bruntime\b/u.test(value);
|
|
55
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const resultSchema = z.object({
|
|
3
|
+
input: z.string(),
|
|
4
|
+
output: z.string(),
|
|
5
|
+
status: z.enum(["demangled", "unchanged", "invalid"]),
|
|
6
|
+
});
|
|
7
|
+
/** Preserve input ordering while parsing `swift-demangle --compact` lines. */
|
|
8
|
+
export const parseDemangledSymbols = (inputs, output) => {
|
|
9
|
+
const lines = output.trimEnd().split(/\r?\n/u);
|
|
10
|
+
if (lines.length !== inputs.length)
|
|
11
|
+
throw new TypeError("swift-demangle output count does not match input");
|
|
12
|
+
return inputs.map((input, index) => {
|
|
13
|
+
const value = lines[index];
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
throw new TypeError("swift-demangle omitted an output line");
|
|
16
|
+
return resultSchema.parse({
|
|
17
|
+
input,
|
|
18
|
+
output: value,
|
|
19
|
+
status: value === input
|
|
20
|
+
? input.startsWith("$s") || input.startsWith("_$s")
|
|
21
|
+
? "invalid"
|
|
22
|
+
: "unchanged"
|
|
23
|
+
: "demangled",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Parse bounded imports/exports from dyld_info's line-oriented tables. */
|
|
2
|
+
export const parseDyldSymbols = (output, mode) => output.split(/\r?\n/u).flatMap((rawLine) => {
|
|
3
|
+
const line = rawLine.trim();
|
|
4
|
+
if (line.length === 0 ||
|
|
5
|
+
/^(?:imports|exports|binding|address|segment|ordinal)\b/iu.test(line))
|
|
6
|
+
return [];
|
|
7
|
+
const tokens = line.split(/\s+/u);
|
|
8
|
+
const name = tokens.at(-1);
|
|
9
|
+
if (name === undefined || !/^(?:_|\$s|objc_|swift_)/u.test(name))
|
|
10
|
+
return [];
|
|
11
|
+
const addressToken = tokens.find((token) => /^0x[a-fA-F0-9]+$/u.test(token));
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
name,
|
|
15
|
+
address: addressToken ?? null,
|
|
16
|
+
weak: /\bweak\b/iu.test(line) ? true : null,
|
|
17
|
+
reexport: /\bre-?export\b/iu.test(line)
|
|
18
|
+
? true
|
|
19
|
+
: mode === "exports"
|
|
20
|
+
? false
|
|
21
|
+
: null,
|
|
22
|
+
source: mode === "imports" ? (tokens.at(-2) ?? null) : null,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const architectureSchema = z.object({
|
|
3
|
+
name: z.string().min(1),
|
|
4
|
+
cpu_type: z.string().nullable(),
|
|
5
|
+
cpu_subtype: z.string().nullable(),
|
|
6
|
+
file_offset: z.number().int().min(0).nullable(),
|
|
7
|
+
size: z.number().int().min(0).nullable(),
|
|
8
|
+
alignment: z.number().int().min(0).nullable(),
|
|
9
|
+
});
|
|
10
|
+
/** Parse `lipo -detailed_info` into deterministic slice metadata. */
|
|
11
|
+
export const parseLipoArchitectures = (output) => {
|
|
12
|
+
const architectures = [];
|
|
13
|
+
let current;
|
|
14
|
+
const flush = () => {
|
|
15
|
+
if (current === undefined)
|
|
16
|
+
return;
|
|
17
|
+
const name = current.architecture ?? current["Non-fat file"];
|
|
18
|
+
if (name !== undefined)
|
|
19
|
+
architectures.push(architectureSchema.parse({
|
|
20
|
+
name: name.includes(" is architecture: ")
|
|
21
|
+
? (name.split(" is architecture: ").at(-1) ?? name)
|
|
22
|
+
: name,
|
|
23
|
+
cpu_type: current.cputype ?? null,
|
|
24
|
+
cpu_subtype: current.cpusubtype ?? null,
|
|
25
|
+
file_offset: integer(current.offset),
|
|
26
|
+
size: integer(current.size),
|
|
27
|
+
alignment: alignment(current.align),
|
|
28
|
+
}));
|
|
29
|
+
current = undefined;
|
|
30
|
+
};
|
|
31
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
32
|
+
const line = rawLine.trim();
|
|
33
|
+
if (line.startsWith("Non-fat file:")) {
|
|
34
|
+
flush();
|
|
35
|
+
current = { "Non-fat file": line };
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (line.startsWith("architecture ")) {
|
|
39
|
+
flush();
|
|
40
|
+
current = { architecture: line.slice("architecture ".length).trim() };
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const match = /^(cputype|cpusubtype|offset|size|align)\s+(.+)$/u.exec(line);
|
|
44
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
45
|
+
current ??= {};
|
|
46
|
+
current[match[1]] = match[2].trim();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
flush();
|
|
50
|
+
if (architectures.length === 0)
|
|
51
|
+
throw new TypeError("lipo output contained no architectures");
|
|
52
|
+
return architectures;
|
|
53
|
+
};
|
|
54
|
+
const integer = (value) => {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return null;
|
|
57
|
+
const parsed = Number.parseInt(value, 10);
|
|
58
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
59
|
+
};
|
|
60
|
+
const alignment = (value) => {
|
|
61
|
+
if (value === undefined)
|
|
62
|
+
return null;
|
|
63
|
+
const exponent = /2\^(\d+)/u.exec(value)?.[1];
|
|
64
|
+
if (exponent !== undefined)
|
|
65
|
+
return 2 ** Number.parseInt(exponent, 10);
|
|
66
|
+
return integer(value);
|
|
67
|
+
};
|