rea-agents 0.3.0 → 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 +149 -2
- 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/referenceSourcePolicy.js +1 -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 +18 -1
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { AnalysisOutputError, HopperProtocolError } from "./errors.js";
|
|
3
3
|
import { err, ok } from "./result.js";
|
|
4
4
|
const procedureMapSchema = z.record(z.string(), z.string());
|
|
5
5
|
const addressedNamesSchema = z.array(z.object({ address: z.string(), name: z.string() }));
|
|
@@ -8,6 +8,9 @@ const segmentSchema = z.object({
|
|
|
8
8
|
name: z.string().default(""),
|
|
9
9
|
start: z.string().default(""),
|
|
10
10
|
end: z.string().default(""),
|
|
11
|
+
readable: z.boolean().nullable().default(null),
|
|
12
|
+
writable: z.boolean().nullable().default(null),
|
|
13
|
+
executable: z.boolean().nullable().default(null),
|
|
11
14
|
});
|
|
12
15
|
const addressedPageSchema = z.object({
|
|
13
16
|
items: z.array(z.object({ address: z.string(), value: z.string() })),
|
|
@@ -20,6 +23,12 @@ const unavailableSchema = z
|
|
|
20
23
|
const procedureIdentitySchema = z
|
|
21
24
|
.object({ address: z.string(), name: z.string() })
|
|
22
25
|
.strict();
|
|
26
|
+
const localVariableSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
description: z.string(),
|
|
29
|
+
provenance: z.literal("hopper-public-python-api"),
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
23
32
|
const boundedSchema = (item) => z
|
|
24
33
|
.object({
|
|
25
34
|
items: z.array(item),
|
|
@@ -61,14 +70,14 @@ const referenceEdgeSchema = z
|
|
|
61
70
|
kind: unavailableSchema,
|
|
62
71
|
})
|
|
63
72
|
.strict();
|
|
64
|
-
const functionDossierSchema = z
|
|
73
|
+
export const functionDossierSchema = z
|
|
65
74
|
.object({
|
|
66
75
|
procedure: z
|
|
67
76
|
.object({
|
|
68
77
|
address: z.string(),
|
|
69
78
|
name: z.string(),
|
|
70
79
|
signature: z.string().nullable(),
|
|
71
|
-
locals: z.array(
|
|
80
|
+
locals: z.array(localVariableSchema),
|
|
72
81
|
})
|
|
73
82
|
.strict(),
|
|
74
83
|
pseudocode: z
|
|
@@ -81,7 +90,7 @@ const functionDossierSchema = z
|
|
|
81
90
|
})
|
|
82
91
|
.strict()
|
|
83
92
|
.superRefine((value, context) => {
|
|
84
|
-
if (value.returned_chars !== value.text.length) {
|
|
93
|
+
if (value.returned_chars !== [...value.text].length) {
|
|
85
94
|
context.addIssue({
|
|
86
95
|
code: "custom",
|
|
87
96
|
message: "returned_chars must equal the text length",
|
|
@@ -146,9 +155,7 @@ export const parseFunctionDossier = (value) => {
|
|
|
146
155
|
const parsed = functionDossierSchema.safeParse(value);
|
|
147
156
|
return parsed.success
|
|
148
157
|
? ok(parsed.data)
|
|
149
|
-
: err(new
|
|
150
|
-
cause: parsed.error,
|
|
151
|
-
}));
|
|
158
|
+
: err(new AnalysisOutputError("analyze_function", "provider output did not match the dossier contract", { cause: parsed.error }));
|
|
152
159
|
};
|
|
153
160
|
/** Parse page entries and continuation metadata returned by list operations. */
|
|
154
161
|
export const parseAddressedPage = (value) => {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const hexAddress = z.string().regex(/^0x[a-fA-F0-9]+$/u);
|
|
3
|
+
const coverage = (item) => z.object({
|
|
4
|
+
items: z.array(item),
|
|
5
|
+
total: z.number().int().min(0).nullable(),
|
|
6
|
+
exhaustive: z.boolean(),
|
|
7
|
+
limitations: z.array(z.string()),
|
|
8
|
+
});
|
|
9
|
+
const nativeCommandInvocationSchema = z.object({
|
|
10
|
+
tool: z.string().min(1),
|
|
11
|
+
command: z.array(z.string()),
|
|
12
|
+
tool_version: z.string().nullable(),
|
|
13
|
+
version_reason: z.string().nullable(),
|
|
14
|
+
executable_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
15
|
+
exit: z.object({
|
|
16
|
+
code: z.number().int().nullable(),
|
|
17
|
+
signal: z.string().nullable(),
|
|
18
|
+
}),
|
|
19
|
+
stdout_bytes: z.number().int().min(0),
|
|
20
|
+
stderr_bytes: z.number().int().min(0),
|
|
21
|
+
stdout_truncated: z.boolean(),
|
|
22
|
+
stderr_truncated: z.boolean(),
|
|
23
|
+
});
|
|
24
|
+
const architectureSchema = z.object({
|
|
25
|
+
name: z.string(),
|
|
26
|
+
cpu_type: z.string().nullable(),
|
|
27
|
+
cpu_subtype: z.string().nullable(),
|
|
28
|
+
file_offset: z.number().int().min(0).nullable(),
|
|
29
|
+
size: z.number().int().min(0).nullable(),
|
|
30
|
+
alignment: z.number().int().min(0).nullable(),
|
|
31
|
+
});
|
|
32
|
+
const permissionSchema = z.object({
|
|
33
|
+
read: z.boolean().nullable(),
|
|
34
|
+
write: z.boolean().nullable(),
|
|
35
|
+
execute: z.boolean().nullable(),
|
|
36
|
+
raw: z.string().nullable(),
|
|
37
|
+
});
|
|
38
|
+
const sectionSchema = z.object({
|
|
39
|
+
segment: z.string(),
|
|
40
|
+
name: z.string(),
|
|
41
|
+
address: hexAddress.nullable(),
|
|
42
|
+
size: z.number().int().min(0).nullable(),
|
|
43
|
+
file_offset: z.number().int().min(0).nullable(),
|
|
44
|
+
alignment: z.number().int().min(0).nullable(),
|
|
45
|
+
flags: z.array(z.string()),
|
|
46
|
+
});
|
|
47
|
+
const segmentSchema = z.object({
|
|
48
|
+
name: z.string(),
|
|
49
|
+
vm_address: hexAddress.nullable(),
|
|
50
|
+
vm_size: z.number().int().min(0).nullable(),
|
|
51
|
+
file_offset: z.number().int().min(0).nullable(),
|
|
52
|
+
file_size: z.number().int().min(0).nullable(),
|
|
53
|
+
maximum_permissions: permissionSchema,
|
|
54
|
+
initial_permissions: permissionSchema,
|
|
55
|
+
sections: coverage(sectionSchema),
|
|
56
|
+
});
|
|
57
|
+
const symbolSchema = z.object({
|
|
58
|
+
name: z.string(),
|
|
59
|
+
address: hexAddress.nullable(),
|
|
60
|
+
weak: z.boolean().nullable(),
|
|
61
|
+
reexport: z.boolean().nullable(),
|
|
62
|
+
source: z.string().nullable(),
|
|
63
|
+
});
|
|
64
|
+
const dependencySchema = z.object({
|
|
65
|
+
path: z.string(),
|
|
66
|
+
kind: z.string(),
|
|
67
|
+
current_version: z.string().nullable(),
|
|
68
|
+
compatibility_version: z.string().nullable(),
|
|
69
|
+
});
|
|
70
|
+
const loadCommandSchema = z.object({
|
|
71
|
+
index: z.number().int().min(0),
|
|
72
|
+
kind: z.string(),
|
|
73
|
+
file_offset: z.number().int().min(0).nullable(),
|
|
74
|
+
fields: z.record(z.string(), z.union([z.string(), z.number(), z.null()])),
|
|
75
|
+
});
|
|
76
|
+
const buildMetadataSchema = z.object({
|
|
77
|
+
platform: z.string().nullable(),
|
|
78
|
+
minimum_os: z.string().nullable(),
|
|
79
|
+
sdk: z.string().nullable(),
|
|
80
|
+
tools: z.array(z.object({ name: z.string(), version: z.string() })),
|
|
81
|
+
});
|
|
82
|
+
export const inspectMachoSchema = z.object({
|
|
83
|
+
format: z.literal("mach-o"),
|
|
84
|
+
endian: z.enum(["little", "big"]).nullable(),
|
|
85
|
+
word_size: z.union([z.literal(32), z.literal(64)]).nullable(),
|
|
86
|
+
file_type: z.string().nullable(),
|
|
87
|
+
flags: z.array(z.string()),
|
|
88
|
+
uuid: z.string().nullable(),
|
|
89
|
+
entrypoints: coverage(z.object({ file_offset: z.number().int().min(0) })),
|
|
90
|
+
architectures: coverage(architectureSchema),
|
|
91
|
+
build_metadata: coverage(buildMetadataSchema),
|
|
92
|
+
load_commands: coverage(loadCommandSchema),
|
|
93
|
+
dependencies: coverage(dependencySchema),
|
|
94
|
+
imports: coverage(symbolSchema),
|
|
95
|
+
exports: coverage(symbolSchema),
|
|
96
|
+
segments: coverage(segmentSchema),
|
|
97
|
+
provenance: z.array(nativeCommandInvocationSchema),
|
|
98
|
+
limitations: z.array(z.string()),
|
|
99
|
+
});
|
|
100
|
+
export const inspectSignatureSchema = z.object({
|
|
101
|
+
signed: z.boolean(),
|
|
102
|
+
identifier: z.string().nullable(),
|
|
103
|
+
team_identifier: z.string().nullable(),
|
|
104
|
+
format: z.string().nullable(),
|
|
105
|
+
cdhashes: z.array(z.string()),
|
|
106
|
+
hash_algorithms: z.array(z.string()),
|
|
107
|
+
authorities: z.array(z.string()),
|
|
108
|
+
designated_requirement: z.string().nullable(),
|
|
109
|
+
entitlements: z.unknown().nullable(),
|
|
110
|
+
timestamp: z.string().nullable(),
|
|
111
|
+
hardened_runtime: z.boolean().nullable(),
|
|
112
|
+
provenance: z.array(nativeCommandInvocationSchema),
|
|
113
|
+
limitations: z.array(z.string()),
|
|
114
|
+
});
|
|
115
|
+
export const inspectPlistSchema = z.object({
|
|
116
|
+
format: z.enum(["xml", "binary", "json", "unknown"]),
|
|
117
|
+
value: z.unknown(),
|
|
118
|
+
bundle: z.object({
|
|
119
|
+
identifier: z.string().nullable(),
|
|
120
|
+
executable: z.string().nullable(),
|
|
121
|
+
name: z.string().nullable(),
|
|
122
|
+
version: z.string().nullable(),
|
|
123
|
+
short_version: z.string().nullable(),
|
|
124
|
+
}),
|
|
125
|
+
source_path: z.string(),
|
|
126
|
+
provenance: z.array(nativeCommandInvocationSchema),
|
|
127
|
+
limitations: z.array(z.string()),
|
|
128
|
+
});
|
|
129
|
+
export const listArchitecturesSchema = z.object({
|
|
130
|
+
architectures: coverage(architectureSchema),
|
|
131
|
+
provenance: z.array(nativeCommandInvocationSchema),
|
|
132
|
+
limitations: z.array(z.string()),
|
|
133
|
+
});
|
|
134
|
+
export const demangleSwiftSchema = z.object({
|
|
135
|
+
symbols: z.array(z.object({
|
|
136
|
+
input: z.string(),
|
|
137
|
+
output: z.string(),
|
|
138
|
+
status: z.enum(["demangled", "unchanged", "invalid"]),
|
|
139
|
+
})),
|
|
140
|
+
provenance: z.array(nativeCommandInvocationSchema),
|
|
141
|
+
limitations: z.array(z.string()),
|
|
142
|
+
});
|
|
@@ -2,18 +2,41 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
const positiveBudget = z.number().int().positive();
|
|
4
4
|
const timedEventBase = { at_ms: z.number().int().nonnegative() };
|
|
5
|
+
const environmentName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
|
|
6
|
+
const reservedEnvironment = new Set([
|
|
7
|
+
"HOME",
|
|
8
|
+
"TERM",
|
|
9
|
+
"REA_PROCESS_RUN_ID",
|
|
10
|
+
"REA_REPLAY_HTTP_URL",
|
|
11
|
+
"REA_REPLAY_WEBSOCKET_URL",
|
|
12
|
+
]);
|
|
13
|
+
const normalizationSchema = z.object({
|
|
14
|
+
paths: z.boolean(),
|
|
15
|
+
pids: z.boolean(),
|
|
16
|
+
ports: z.boolean(),
|
|
17
|
+
time_bucket_ms: positiveBudget.max(60_000),
|
|
18
|
+
patterns: z.array(z.object({
|
|
19
|
+
pattern: z.string().max(500),
|
|
20
|
+
replacement: z.string().max(100),
|
|
21
|
+
})),
|
|
22
|
+
});
|
|
5
23
|
/** Exact boundary schema for bounded dynamic process scenarios. */
|
|
6
24
|
export const processScenarioSchema = z
|
|
7
25
|
.object({
|
|
8
26
|
approved: z
|
|
9
27
|
.literal(true)
|
|
10
28
|
.describe("Explicit per-call acknowledgement that this operation launches the target"),
|
|
29
|
+
unknown_registry_approved: z
|
|
30
|
+
.literal(true)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Explicit approval to record capture residuals durably"),
|
|
11
33
|
executable: z.string().startsWith("/"),
|
|
12
34
|
arguments: z.array(z.string()).max(256).default([]),
|
|
13
35
|
working_directory: z.string().startsWith("/"),
|
|
14
|
-
environment: z.record(
|
|
15
|
-
inherit_environment: z.array(
|
|
16
|
-
secret_aliases: z.array(
|
|
36
|
+
environment: z.record(environmentName, z.string()).default({}),
|
|
37
|
+
inherit_environment: z.array(environmentName).max(64).default([]),
|
|
38
|
+
secret_aliases: z.array(environmentName).max(64).default([]),
|
|
39
|
+
network_access: z.literal("host").default("host"),
|
|
17
40
|
filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
|
|
18
41
|
events: z
|
|
19
42
|
.array(z.discriminatedUnion("type", [
|
|
@@ -46,6 +69,10 @@ export const processScenarioSchema = z
|
|
|
46
69
|
files: positiveBudget.max(100_000).default(10_000),
|
|
47
70
|
file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
|
|
48
71
|
processes: positiveBudget.max(10_000).default(1_000),
|
|
72
|
+
protocol_events: positiveBudget.max(100_000).default(10_000),
|
|
73
|
+
protocol_body_bytes: positiveBudget.max(10_000_000).default(1_000_000),
|
|
74
|
+
connections: positiveBudget.max(1_000).default(100),
|
|
75
|
+
filesystem_depth: z.number().int().min(1).max(64).default(16),
|
|
49
76
|
})
|
|
50
77
|
.default({
|
|
51
78
|
output_bytes: 1_000_000,
|
|
@@ -53,6 +80,10 @@ export const processScenarioSchema = z
|
|
|
53
80
|
files: 10_000,
|
|
54
81
|
file_bytes: 10_000_000,
|
|
55
82
|
processes: 1_000,
|
|
83
|
+
protocol_events: 10_000,
|
|
84
|
+
protocol_body_bytes: 1_000_000,
|
|
85
|
+
connections: 100,
|
|
86
|
+
filesystem_depth: 16,
|
|
56
87
|
}),
|
|
57
88
|
normalization: z
|
|
58
89
|
.object({
|
|
@@ -83,6 +114,12 @@ export const processScenarioSchema = z
|
|
|
83
114
|
path: z.string().startsWith("/"),
|
|
84
115
|
status: z.number().int().min(100).max(599),
|
|
85
116
|
body: z.string().max(1_000_000),
|
|
117
|
+
request_headers: z.record(z.string(), z.string()).default({}),
|
|
118
|
+
request_body: z.string().max(1_000_000).optional(),
|
|
119
|
+
response_headers: z.record(z.string(), z.string()).default({}),
|
|
120
|
+
delay_ms: z.number().int().nonnegative().max(30_000).default(0),
|
|
121
|
+
disconnect: z.boolean().default(false),
|
|
122
|
+
max_calls: z.number().int().min(1).max(100).default(1),
|
|
86
123
|
}))
|
|
87
124
|
.max(100)
|
|
88
125
|
.default([]),
|
|
@@ -90,8 +127,29 @@ export const processScenarioSchema = z
|
|
|
90
127
|
.array(z.string().max(1_000_000))
|
|
91
128
|
.max(100)
|
|
92
129
|
.default([]),
|
|
130
|
+
websocket_connections: z
|
|
131
|
+
.array(z.object({
|
|
132
|
+
messages: z
|
|
133
|
+
.array(z.object({
|
|
134
|
+
data: z.string().max(1_000_000),
|
|
135
|
+
delay_ms: z
|
|
136
|
+
.number()
|
|
137
|
+
.int()
|
|
138
|
+
.nonnegative()
|
|
139
|
+
.max(30_000)
|
|
140
|
+
.default(0),
|
|
141
|
+
}))
|
|
142
|
+
.max(100),
|
|
143
|
+
disconnect_after: z.boolean().default(false),
|
|
144
|
+
}))
|
|
145
|
+
.max(100)
|
|
146
|
+
.default([]),
|
|
93
147
|
})
|
|
94
|
-
.default({
|
|
148
|
+
.default({
|
|
149
|
+
http: [],
|
|
150
|
+
websocket_messages: [],
|
|
151
|
+
websocket_connections: [],
|
|
152
|
+
}),
|
|
95
153
|
})
|
|
96
154
|
.strict()
|
|
97
155
|
.superRefine((scenario, context) => {
|
|
@@ -114,6 +172,34 @@ export const processScenarioSchema = z
|
|
|
114
172
|
});
|
|
115
173
|
}
|
|
116
174
|
}
|
|
175
|
+
const explicit = new Set(Object.keys(scenario.environment));
|
|
176
|
+
for (const name of [...explicit, ...scenario.inherit_environment]) {
|
|
177
|
+
if (reservedEnvironment.has(name)) {
|
|
178
|
+
context.addIssue({
|
|
179
|
+
code: "custom",
|
|
180
|
+
message: `${name} is reserved by the process adapter`,
|
|
181
|
+
path: ["environment", name],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const name of scenario.inherit_environment) {
|
|
186
|
+
if (explicit.has(name)) {
|
|
187
|
+
context.addIssue({
|
|
188
|
+
code: "custom",
|
|
189
|
+
message: "an environment name cannot be explicit and inherited",
|
|
190
|
+
path: ["inherit_environment"],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const event of scenario.events) {
|
|
195
|
+
if (event.at_ms > scenario.timeout_ms) {
|
|
196
|
+
context.addIssue({
|
|
197
|
+
code: "custom",
|
|
198
|
+
message: "event occurs after the scenario timeout",
|
|
199
|
+
path: ["events"],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
117
203
|
});
|
|
118
204
|
/** Parse untrusted process scenario input and apply safe default budgets. */
|
|
119
205
|
export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
|
|
@@ -123,6 +209,11 @@ const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
|
|
|
123
209
|
export const authorizeProcessScenario = (scenario, policy) => {
|
|
124
210
|
if (!policy.enabled)
|
|
125
211
|
return { allowed: false, reason: "process capture is disabled" };
|
|
212
|
+
if (scenario.network_access === "host" && !policy.allowExternalNetwork)
|
|
213
|
+
return {
|
|
214
|
+
allowed: false,
|
|
215
|
+
reason: "host network access is not approved by operator policy",
|
|
216
|
+
};
|
|
126
217
|
if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
|
|
127
218
|
return { allowed: false, reason: "executable is outside approved roots" };
|
|
128
219
|
}
|
|
@@ -152,7 +243,8 @@ export const authorizeProcessScenario = (scenario, policy) => {
|
|
|
152
243
|
};
|
|
153
244
|
/** Exact serialized shape of a bounded process capture. */
|
|
154
245
|
export const processCaptureSchema = z.object({
|
|
155
|
-
schema_version: z.literal(
|
|
246
|
+
schema_version: z.literal(2),
|
|
247
|
+
normalization: normalizationSchema,
|
|
156
248
|
frames: z.array(z.object({
|
|
157
249
|
sequence: z.number().int().nonnegative(),
|
|
158
250
|
at_ms: z.number().int().nonnegative(),
|
|
@@ -161,6 +253,7 @@ export const processCaptureSchema = z.object({
|
|
|
161
253
|
exit: z.object({
|
|
162
254
|
code: z.number().int().nullable(),
|
|
163
255
|
signal: z.number().int().nullable(),
|
|
256
|
+
reason: z.enum(["exited", "timeout", "idle_timeout"]),
|
|
164
257
|
}),
|
|
165
258
|
process_samples: z.array(z.object({
|
|
166
259
|
at_ms: z.number().int().nonnegative(),
|
|
@@ -170,11 +263,18 @@ export const processCaptureSchema = z.object({
|
|
|
170
263
|
})),
|
|
171
264
|
protocol_events: z.array(z.object({
|
|
172
265
|
sequence: z.number().int().nonnegative(),
|
|
266
|
+
at_ms: z.number().int().nonnegative(),
|
|
173
267
|
protocol: z.enum(["http", "websocket"]),
|
|
174
268
|
direction: z.enum(["request", "response", "received", "sent"]),
|
|
175
269
|
method: z.string().nullable(),
|
|
176
270
|
path: z.string().nullable(),
|
|
177
271
|
data: z.string(),
|
|
272
|
+
outcome: z.enum([
|
|
273
|
+
"matched",
|
|
274
|
+
"unmatched",
|
|
275
|
+
"script_exhausted",
|
|
276
|
+
"disconnected",
|
|
277
|
+
]),
|
|
178
278
|
})),
|
|
179
279
|
files_before: z.array(z.object({
|
|
180
280
|
path: z.string(),
|
|
@@ -198,55 +298,53 @@ export const processCaptureSchema = z.object({
|
|
|
198
298
|
.nullable(),
|
|
199
299
|
symlink_target: z.string().nullable(),
|
|
200
300
|
})),
|
|
301
|
+
filesystem_effects: z.array(z.object({
|
|
302
|
+
path: z.string(),
|
|
303
|
+
status: z.enum(["created", "modified", "deleted", "unchanged"]),
|
|
304
|
+
before: z
|
|
305
|
+
.object({
|
|
306
|
+
path: z.string(),
|
|
307
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
308
|
+
mode: z.number().int().nonnegative(),
|
|
309
|
+
size: z.number().int().nonnegative(),
|
|
310
|
+
sha256: z
|
|
311
|
+
.string()
|
|
312
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
313
|
+
.nullable(),
|
|
314
|
+
symlink_target: z.string().nullable(),
|
|
315
|
+
})
|
|
316
|
+
.nullable(),
|
|
317
|
+
after: z
|
|
318
|
+
.object({
|
|
319
|
+
path: z.string(),
|
|
320
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
321
|
+
mode: z.number().int().nonnegative(),
|
|
322
|
+
size: z.number().int().nonnegative(),
|
|
323
|
+
sha256: z
|
|
324
|
+
.string()
|
|
325
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
326
|
+
.nullable(),
|
|
327
|
+
symlink_target: z.string().nullable(),
|
|
328
|
+
})
|
|
329
|
+
.nullable(),
|
|
330
|
+
})),
|
|
201
331
|
truncated: z.boolean(),
|
|
202
332
|
limitations: z.array(z.string()),
|
|
333
|
+
residual_unknowns: z.array(z.object({
|
|
334
|
+
scope: z.enum([
|
|
335
|
+
"terminal",
|
|
336
|
+
"exit",
|
|
337
|
+
"process",
|
|
338
|
+
"filesystem",
|
|
339
|
+
"protocol",
|
|
340
|
+
"cleanup",
|
|
341
|
+
"network",
|
|
342
|
+
]),
|
|
343
|
+
reason: z.string(),
|
|
344
|
+
})),
|
|
345
|
+
cleanup: z.object({
|
|
346
|
+
owned_process_group: z.literal("verified"),
|
|
347
|
+
temporary_root: z.literal("removed"),
|
|
348
|
+
}),
|
|
203
349
|
});
|
|
204
|
-
|
|
205
|
-
/** Compare bounded captures without claiming equality for incomplete observations. */
|
|
206
|
-
export const compareProcessCaptures = (left, right) => {
|
|
207
|
-
if (left.truncated || right.truncated) {
|
|
208
|
-
return {
|
|
209
|
-
status: "truncated",
|
|
210
|
-
terminal: "truncated",
|
|
211
|
-
exit: "truncated",
|
|
212
|
-
filesystem: "truncated",
|
|
213
|
-
protocol: "truncated",
|
|
214
|
-
process: "truncated",
|
|
215
|
-
limitations: ["At least one capture is truncated."],
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
const terminal = left.frames.map(({ data }) => data).join("") ===
|
|
219
|
-
right.frames.map(({ data }) => data).join("")
|
|
220
|
-
? "unchanged"
|
|
221
|
-
: "changed";
|
|
222
|
-
const exit = JSON.stringify(left.exit) === JSON.stringify(right.exit)
|
|
223
|
-
? "unchanged"
|
|
224
|
-
: "changed";
|
|
225
|
-
const filesystem = stableFiles(left.files_after) === stableFiles(right.files_after)
|
|
226
|
-
? "unchanged"
|
|
227
|
-
: "changed";
|
|
228
|
-
const protocol = JSON.stringify(left.protocol_events) ===
|
|
229
|
-
JSON.stringify(right.protocol_events)
|
|
230
|
-
? "unchanged"
|
|
231
|
-
: "changed";
|
|
232
|
-
const process = JSON.stringify(left.process_samples.map(({ command }) => command)) ===
|
|
233
|
-
JSON.stringify(right.process_samples.map(({ command }) => command))
|
|
234
|
-
? "unchanged"
|
|
235
|
-
: "changed";
|
|
236
|
-
const status = terminal === "unchanged" &&
|
|
237
|
-
exit === "unchanged" &&
|
|
238
|
-
filesystem === "unchanged" &&
|
|
239
|
-
protocol === "unchanged" &&
|
|
240
|
-
process === "unchanged"
|
|
241
|
-
? "unchanged"
|
|
242
|
-
: "changed";
|
|
243
|
-
return {
|
|
244
|
-
status,
|
|
245
|
-
terminal,
|
|
246
|
-
exit,
|
|
247
|
-
filesystem,
|
|
248
|
-
protocol,
|
|
249
|
-
process,
|
|
250
|
-
limitations: [...left.limitations, ...right.limitations],
|
|
251
|
-
};
|
|
252
|
-
};
|
|
350
|
+
export { compareProcessCaptures, comparisonStatusSchema, processCaptureComparisonSchema, } from "./processComparison.js";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Comparison classification that never equates incomplete evidence. */
|
|
3
|
+
export const comparisonStatusSchema = z.enum([
|
|
4
|
+
"unchanged",
|
|
5
|
+
"added",
|
|
6
|
+
"removed",
|
|
7
|
+
"changed",
|
|
8
|
+
"truncated",
|
|
9
|
+
"unknown",
|
|
10
|
+
]);
|
|
11
|
+
/** Pure normalized comparison between two captures. */
|
|
12
|
+
export const processCaptureComparisonSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
status: comparisonStatusSchema,
|
|
15
|
+
terminal: comparisonStatusSchema,
|
|
16
|
+
exit: comparisonStatusSchema,
|
|
17
|
+
filesystem: comparisonStatusSchema,
|
|
18
|
+
protocol: comparisonStatusSchema,
|
|
19
|
+
process: comparisonStatusSchema,
|
|
20
|
+
limitations: z.array(z.string()),
|
|
21
|
+
})
|
|
22
|
+
.superRefine((comparison, context) => {
|
|
23
|
+
const dimensions = [
|
|
24
|
+
comparison.terminal,
|
|
25
|
+
comparison.exit,
|
|
26
|
+
comparison.filesystem,
|
|
27
|
+
comparison.protocol,
|
|
28
|
+
comparison.process,
|
|
29
|
+
];
|
|
30
|
+
const expected = dimensions.includes("truncated")
|
|
31
|
+
? "truncated"
|
|
32
|
+
: dimensions.some((status) => ["added", "removed", "changed"].includes(status))
|
|
33
|
+
? "changed"
|
|
34
|
+
: dimensions.includes("unknown")
|
|
35
|
+
? "unknown"
|
|
36
|
+
: "unchanged";
|
|
37
|
+
if (comparison.status !== expected)
|
|
38
|
+
context.addIssue({
|
|
39
|
+
code: "custom",
|
|
40
|
+
message: "Process comparison status contradicts its dimensions",
|
|
41
|
+
path: ["status"],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
const stableFiles = (capture) => JSON.stringify([...capture.files_after].sort((left, right) => left.path.localeCompare(right.path)));
|
|
45
|
+
const classifyCollection = (left, right) => {
|
|
46
|
+
if (JSON.stringify(left) === JSON.stringify(right))
|
|
47
|
+
return "unchanged";
|
|
48
|
+
if (left.length === 0)
|
|
49
|
+
return "added";
|
|
50
|
+
if (right.length === 0)
|
|
51
|
+
return "removed";
|
|
52
|
+
return "changed";
|
|
53
|
+
};
|
|
54
|
+
const hasUnknown = (capture, scope) => capture.residual_unknowns.some((item) => item.scope === scope);
|
|
55
|
+
const comparisonStatus = (dimensions) => {
|
|
56
|
+
if (dimensions.some((status) => ["changed", "added", "removed"].includes(status)))
|
|
57
|
+
return "changed";
|
|
58
|
+
return dimensions.includes("unknown") ? "unknown" : "unchanged";
|
|
59
|
+
};
|
|
60
|
+
/** Compare bounded captures without equating missing or incompatible evidence. */
|
|
61
|
+
export const compareProcessCaptures = (left, right) => {
|
|
62
|
+
if (left.truncated || right.truncated) {
|
|
63
|
+
return {
|
|
64
|
+
status: "truncated",
|
|
65
|
+
terminal: "truncated",
|
|
66
|
+
exit: "truncated",
|
|
67
|
+
filesystem: "truncated",
|
|
68
|
+
protocol: "truncated",
|
|
69
|
+
process: "truncated",
|
|
70
|
+
limitations: ["At least one capture is truncated."],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const sameNormalization = JSON.stringify(left.normalization) === JSON.stringify(right.normalization);
|
|
74
|
+
const classify = (scope, leftValues, rightValues) => !sameNormalization || hasUnknown(left, scope) || hasUnknown(right, scope)
|
|
75
|
+
? "unknown"
|
|
76
|
+
: classifyCollection(leftValues, rightValues);
|
|
77
|
+
const terminal = classify("terminal", left.frames.map(({ data }) => data), right.frames.map(({ data }) => data));
|
|
78
|
+
const exit = hasUnknown(left, "exit") || hasUnknown(right, "exit")
|
|
79
|
+
? "unknown"
|
|
80
|
+
: JSON.stringify(left.exit) === JSON.stringify(right.exit)
|
|
81
|
+
? "unchanged"
|
|
82
|
+
: "changed";
|
|
83
|
+
const filesystem = !hasUnknown(left, "filesystem") &&
|
|
84
|
+
!hasUnknown(right, "filesystem") &&
|
|
85
|
+
sameNormalization &&
|
|
86
|
+
stableFiles(left) === stableFiles(right)
|
|
87
|
+
? "unchanged"
|
|
88
|
+
: classify("filesystem", left.filesystem_effects, right.filesystem_effects);
|
|
89
|
+
const protocol = classify("protocol", left.protocol_events, right.protocol_events);
|
|
90
|
+
const process = classify("process", left.process_samples, right.process_samples);
|
|
91
|
+
return {
|
|
92
|
+
status: comparisonStatus([terminal, exit, filesystem, protocol, process]),
|
|
93
|
+
terminal,
|
|
94
|
+
exit,
|
|
95
|
+
filesystem,
|
|
96
|
+
protocol,
|
|
97
|
+
process,
|
|
98
|
+
limitations: [
|
|
99
|
+
...left.limitations,
|
|
100
|
+
...right.limitations,
|
|
101
|
+
...left.residual_unknowns.map(({ reason }) => reason),
|
|
102
|
+
...right.residual_unknowns.map(({ reason }) => reason),
|
|
103
|
+
...(sameNormalization ? [] : ["Capture normalization rules differ."]),
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
};
|