rea-agents 1.0.0 → 1.2.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 +49 -18
- package/dist/application/ArtifactInventory.js +80 -44
- package/dist/application/CommandShimReplay.js +136 -0
- package/dist/application/Doctor.js +4 -4
- package/dist/application/EvidenceBundleFiles.js +13 -4
- package/dist/application/MacHopper.js +175 -0
- package/dist/application/ProcessCaptureAuthority.js +25 -0
- package/dist/application/ProcessCaptureCapability.js +23 -0
- package/dist/application/ProcessCaptureError.js +5 -0
- package/dist/application/ProcessCaptureLifecycle.js +269 -0
- package/dist/application/ProcessCheckpoints.js +129 -0
- package/dist/application/ProcessCli.js +61 -0
- package/dist/application/ProcessEvidence.js +38 -0
- package/dist/application/ProcessHarness.js +252 -272
- package/dist/application/ProcessNormalization.js +10 -1
- package/dist/application/ProcessOwnership.js +39 -1
- package/dist/application/ProcessSampling.js +39 -23
- package/dist/application/Setup.js +104 -100
- package/dist/application/SetupSkill.js +44 -0
- package/dist/application/TerminalRenderer.js +92 -0
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +18 -6
- package/dist/artifacts/ArtifactReader.js +3 -1
- package/dist/artifacts/AsarArtifactReader.js +8 -1
- package/dist/artifacts/DirectoryArtifactReader.js +1 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +1 -0
- package/dist/artifacts/NativeDmgArtifactReader.js +151 -0
- package/dist/artifacts/ZipArtifactReader.js +1 -0
- package/dist/cli.js +60 -26
- package/dist/cliProcessCommands.js +19 -0
- package/dist/config.js +2 -0
- package/dist/contracts/artifactToolContracts.js +1 -0
- package/dist/contracts/investigationExamples.js +3 -3
- package/dist/contracts/processCaptureExample.js +50 -7
- package/dist/contracts/toolContracts.js +3 -2
- package/dist/domain/changedBehavior.js +2 -2
- package/dist/domain/errors.js +11 -3
- package/dist/domain/processCapture.js +99 -293
- package/dist/domain/processCaptureValidation.js +124 -0
- package/dist/domain/processComparison.js +176 -32
- package/dist/domain/processScenario.js +370 -0
- package/dist/domain/reconstructionVerification.js +3 -3
- package/dist/domain/runtimeVersion.js +8 -0
- package/dist/domain/staticRuntimeCorrelation.js +3 -2
- package/dist/identity.js +1 -0
- package/dist/server/registerProcessComparisonTool.js +28 -8
- package/dist/server/registerSessionTools.js +3 -25
- package/dist/server/sessionToolPolicies.js +1 -6
- package/install.sh +79 -164
- package/package.json +7 -4
- package/scripts/prepare-node-pty.mjs +35 -0
- package/skills/rea-analysis/SKILL.md +19 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { validateProcessCapture } from "./processCapture.js";
|
|
2
3
|
/** Comparison classification that never equates incomplete evidence. */
|
|
3
4
|
export const comparisonStatusSchema = z.enum([
|
|
4
5
|
"unchanged",
|
|
@@ -13,19 +14,44 @@ export const processCaptureComparisonSchema = z
|
|
|
13
14
|
.object({
|
|
14
15
|
status: comparisonStatusSchema,
|
|
15
16
|
terminal: comparisonStatusSchema,
|
|
17
|
+
interaction: comparisonStatusSchema,
|
|
16
18
|
exit: comparisonStatusSchema,
|
|
17
19
|
filesystem: comparisonStatusSchema,
|
|
18
20
|
protocol: comparisonStatusSchema,
|
|
19
21
|
process: comparisonStatusSchema,
|
|
22
|
+
shim: comparisonStatusSchema,
|
|
23
|
+
first_divergence: z.discriminatedUnion("status", [
|
|
24
|
+
z.object({ status: z.literal("none") }),
|
|
25
|
+
z.object({ status: z.literal("unknown"), reason: z.string() }),
|
|
26
|
+
z.object({
|
|
27
|
+
status: z.literal("found"),
|
|
28
|
+
dimension: z.enum([
|
|
29
|
+
"terminal",
|
|
30
|
+
"interaction",
|
|
31
|
+
"exit",
|
|
32
|
+
"filesystem",
|
|
33
|
+
"protocol",
|
|
34
|
+
"process",
|
|
35
|
+
"shim",
|
|
36
|
+
]),
|
|
37
|
+
index: z.number().int().nonnegative(),
|
|
38
|
+
left_at_ms: z.number().int().nonnegative().nullable(),
|
|
39
|
+
right_at_ms: z.number().int().nonnegative().nullable(),
|
|
40
|
+
left: z.unknown().nullable(),
|
|
41
|
+
right: z.unknown().nullable(),
|
|
42
|
+
}),
|
|
43
|
+
]),
|
|
20
44
|
limitations: z.array(z.string()),
|
|
21
45
|
})
|
|
22
46
|
.superRefine((comparison, context) => {
|
|
23
47
|
const dimensions = [
|
|
24
48
|
comparison.terminal,
|
|
49
|
+
comparison.interaction,
|
|
25
50
|
comparison.exit,
|
|
26
51
|
comparison.filesystem,
|
|
27
52
|
comparison.protocol,
|
|
28
53
|
comparison.process,
|
|
54
|
+
comparison.shim,
|
|
29
55
|
];
|
|
30
56
|
const expected = dimensions.includes("truncated")
|
|
31
57
|
? "truncated"
|
|
@@ -41,7 +67,28 @@ export const processCaptureComparisonSchema = z
|
|
|
41
67
|
path: ["status"],
|
|
42
68
|
});
|
|
43
69
|
});
|
|
44
|
-
const
|
|
70
|
+
const terminalObservations = (capture) => [
|
|
71
|
+
...capture.frames.map((frame) => ({ kind: "raw", ...frame })),
|
|
72
|
+
...capture.rendered_frames.map((frame) => ({
|
|
73
|
+
kind: "rendered",
|
|
74
|
+
...frame,
|
|
75
|
+
})),
|
|
76
|
+
].sort((left, right) => {
|
|
77
|
+
const time = left.at_ms - right.at_ms;
|
|
78
|
+
if (time !== 0)
|
|
79
|
+
return time;
|
|
80
|
+
if (left.kind !== right.kind)
|
|
81
|
+
return left.kind === "raw" ? -1 : 1;
|
|
82
|
+
return left.sequence - right.sequence;
|
|
83
|
+
});
|
|
84
|
+
const filesystemObservations = (capture) => [
|
|
85
|
+
...capture.filesystem_checkpoints,
|
|
86
|
+
{
|
|
87
|
+
name: "final",
|
|
88
|
+
files: capture.files_after,
|
|
89
|
+
effects: capture.filesystem_effects,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
45
92
|
const classifyCollection = (left, right) => {
|
|
46
93
|
if (JSON.stringify(left) === JSON.stringify(right))
|
|
47
94
|
return "unchanged";
|
|
@@ -57,44 +104,141 @@ const comparisonStatus = (dimensions) => {
|
|
|
57
104
|
return "changed";
|
|
58
105
|
return dimensions.includes("unknown") ? "unknown" : "unchanged";
|
|
59
106
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
107
|
+
const eventTime = (value) => {
|
|
108
|
+
if (typeof value === "object" &&
|
|
109
|
+
value !== null &&
|
|
110
|
+
"at_ms" in value &&
|
|
111
|
+
typeof value.at_ms === "number" &&
|
|
112
|
+
Number.isSafeInteger(value.at_ms) &&
|
|
113
|
+
value.at_ms >= 0)
|
|
114
|
+
return value.at_ms;
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
const firstCollectionDivergence = (dimension, left, right) => {
|
|
118
|
+
const length = Math.max(left.length, right.length);
|
|
119
|
+
for (let index = 0; index < length; index += 1) {
|
|
120
|
+
const leftValue = left[index] ?? null;
|
|
121
|
+
const rightValue = right[index] ?? null;
|
|
122
|
+
if (JSON.stringify(leftValue) === JSON.stringify(rightValue))
|
|
123
|
+
continue;
|
|
63
124
|
return {
|
|
64
|
-
status: "
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
125
|
+
status: "found",
|
|
126
|
+
dimension,
|
|
127
|
+
index,
|
|
128
|
+
left_at_ms: eventTime(leftValue),
|
|
129
|
+
right_at_ms: eventTime(rightValue),
|
|
130
|
+
left: leftValue,
|
|
131
|
+
right: rightValue,
|
|
71
132
|
};
|
|
72
133
|
}
|
|
73
|
-
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
const chooseFirstDivergence = (candidates) => {
|
|
137
|
+
const found = candidates.filter((candidate) => candidate !== null);
|
|
138
|
+
return (found.sort((left, right) => {
|
|
139
|
+
const leftTime = Math.min(left.left_at_ms ?? Number.MAX_SAFE_INTEGER, left.right_at_ms ?? Number.MAX_SAFE_INTEGER);
|
|
140
|
+
const rightTime = Math.min(right.left_at_ms ?? Number.MAX_SAFE_INTEGER, right.right_at_ms ?? Number.MAX_SAFE_INTEGER);
|
|
141
|
+
return leftTime - rightTime;
|
|
142
|
+
})[0] ?? { status: "none" });
|
|
143
|
+
};
|
|
144
|
+
const assertComparable = (left, right, options) => {
|
|
145
|
+
const invalid = [
|
|
146
|
+
...validateProcessCapture(left),
|
|
147
|
+
...validateProcessCapture(right),
|
|
148
|
+
];
|
|
149
|
+
if (invalid.length > 0)
|
|
150
|
+
throw new TypeError(`Invalid Process Capture v4: ${invalid[0].path}`);
|
|
151
|
+
if (left.schema_version !== right.schema_version ||
|
|
152
|
+
left.manifest.comparison_contract_sha256 !==
|
|
153
|
+
right.manifest.comparison_contract_sha256)
|
|
154
|
+
throw new TypeError("Process captures have incompatible comparison contracts");
|
|
155
|
+
if (options.maxCaptureAgeMs === undefined)
|
|
156
|
+
return;
|
|
157
|
+
const maxCaptureAgeMs = options.maxCaptureAgeMs;
|
|
158
|
+
const now = (options.now ?? Date.now)();
|
|
159
|
+
if ([left, right].some(({ manifest }) => now - Date.parse(manifest.completed_at) > maxCaptureAgeMs))
|
|
160
|
+
throw new TypeError("Process capture exceeds max_capture_age_ms");
|
|
161
|
+
};
|
|
162
|
+
const truncatedComparison = () => ({
|
|
163
|
+
status: "truncated",
|
|
164
|
+
terminal: "truncated",
|
|
165
|
+
interaction: "truncated",
|
|
166
|
+
exit: "truncated",
|
|
167
|
+
filesystem: "truncated",
|
|
168
|
+
protocol: "truncated",
|
|
169
|
+
process: "truncated",
|
|
170
|
+
shim: "truncated",
|
|
171
|
+
first_divergence: {
|
|
172
|
+
status: "unknown",
|
|
173
|
+
reason: "At least one capture is truncated.",
|
|
174
|
+
},
|
|
175
|
+
limitations: ["At least one capture is truncated."],
|
|
176
|
+
});
|
|
177
|
+
const compareDimensions = (left, right, sameNormalization) => {
|
|
74
178
|
const classify = (scope, leftValues, rightValues) => !sameNormalization || hasUnknown(left, scope) || hasUnknown(right, scope)
|
|
75
179
|
? "unknown"
|
|
76
180
|
: 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
181
|
return {
|
|
92
|
-
|
|
93
|
-
terminal,
|
|
94
|
-
exit,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
182
|
+
terminal: classify("terminal", terminalObservations(left), terminalObservations(right)),
|
|
183
|
+
interaction: classify("terminal", left.interaction_events, right.interaction_events),
|
|
184
|
+
exit: hasUnknown(left, "exit") || hasUnknown(right, "exit")
|
|
185
|
+
? "unknown"
|
|
186
|
+
: JSON.stringify({ exit: left.exit, settlement: left.settlement }) ===
|
|
187
|
+
JSON.stringify({ exit: right.exit, settlement: right.settlement })
|
|
188
|
+
? "unchanged"
|
|
189
|
+
: "changed",
|
|
190
|
+
filesystem: classify("filesystem", filesystemObservations(left), filesystemObservations(right)),
|
|
191
|
+
protocol: classify("protocol", left.protocol_events, right.protocol_events),
|
|
192
|
+
process: classify("process", left.process_samples, right.process_samples),
|
|
193
|
+
shim: sameNormalization
|
|
194
|
+
? classifyCollection(left.shim_events, right.shim_events)
|
|
195
|
+
: "unknown",
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
/** Compare bounded captures without equating missing or incompatible evidence. */
|
|
199
|
+
/**
|
|
200
|
+
* Compare two Process Capture v4 observations without treating absence as proof.
|
|
201
|
+
*
|
|
202
|
+
* Observed differences outrank residual unknowns. A capture may therefore be
|
|
203
|
+
* definitively changed in one dimension while completeness remains unknown.
|
|
204
|
+
*/
|
|
205
|
+
export const compareProcessCaptures = (left, right, options = {}) => {
|
|
206
|
+
assertComparable(left, right, options);
|
|
207
|
+
if (left.truncated || right.truncated)
|
|
208
|
+
return truncatedComparison();
|
|
209
|
+
const sameNormalization = JSON.stringify(left.normalization) === JSON.stringify(right.normalization);
|
|
210
|
+
const dimensions = compareDimensions(left, right, sameNormalization);
|
|
211
|
+
const observedFirstDivergence = chooseFirstDivergence([
|
|
212
|
+
firstCollectionDivergence("terminal", terminalObservations(left), terminalObservations(right)),
|
|
213
|
+
firstCollectionDivergence("interaction", left.interaction_events, right.interaction_events),
|
|
214
|
+
firstCollectionDivergence("exit", [{ ...left.exit, settlement: left.settlement }], [{ ...right.exit, settlement: right.settlement }]),
|
|
215
|
+
firstCollectionDivergence("filesystem", filesystemObservations(left), filesystemObservations(right)),
|
|
216
|
+
firstCollectionDivergence("protocol", left.protocol_events, right.protocol_events),
|
|
217
|
+
firstCollectionDivergence("process", left.process_samples, right.process_samples),
|
|
218
|
+
firstCollectionDivergence("shim", left.shim_events, right.shim_events),
|
|
219
|
+
]);
|
|
220
|
+
const firstDivergence = observedFirstDivergence.status === "found"
|
|
221
|
+
? observedFirstDivergence
|
|
222
|
+
: !sameNormalization ||
|
|
223
|
+
left.residual_unknowns.length > 0 ||
|
|
224
|
+
right.residual_unknowns.length > 0
|
|
225
|
+
? {
|
|
226
|
+
status: "unknown",
|
|
227
|
+
reason: "Residual unknowns prevent proving that no divergence occurred.",
|
|
228
|
+
}
|
|
229
|
+
: observedFirstDivergence;
|
|
230
|
+
return {
|
|
231
|
+
status: comparisonStatus([
|
|
232
|
+
dimensions.terminal,
|
|
233
|
+
dimensions.interaction,
|
|
234
|
+
dimensions.exit,
|
|
235
|
+
dimensions.filesystem,
|
|
236
|
+
dimensions.protocol,
|
|
237
|
+
dimensions.process,
|
|
238
|
+
dimensions.shim,
|
|
239
|
+
]),
|
|
240
|
+
...dimensions,
|
|
241
|
+
first_divergence: firstDivergence,
|
|
98
242
|
limitations: [
|
|
99
243
|
...left.limitations,
|
|
100
244
|
...right.limitations,
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import canonicalize from "canonicalize";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const positiveBudget = z.number().int().positive();
|
|
6
|
+
const timedEventBase = { at_ms: z.number().int().nonnegative() };
|
|
7
|
+
const environmentName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
|
|
8
|
+
const reservedEnvironment = new Set([
|
|
9
|
+
"HOME",
|
|
10
|
+
"TERM",
|
|
11
|
+
"REA_PROCESS_RUN_ID",
|
|
12
|
+
"REA_REPLAY_HTTP_URL",
|
|
13
|
+
"REA_REPLAY_WEBSOCKET_URL",
|
|
14
|
+
"REA_SHIM_LEDGER_URL",
|
|
15
|
+
]);
|
|
16
|
+
export const normalizationSchema = z.object({
|
|
17
|
+
paths: z.boolean(),
|
|
18
|
+
pids: z.boolean(),
|
|
19
|
+
ports: z.boolean(),
|
|
20
|
+
time_bucket_ms: positiveBudget.max(60_000),
|
|
21
|
+
patterns: z.array(z.object({
|
|
22
|
+
pattern: z.string().max(500),
|
|
23
|
+
replacement: z.string().max(100),
|
|
24
|
+
})),
|
|
25
|
+
});
|
|
26
|
+
const checkpointNameSchema = z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u);
|
|
29
|
+
const commandNameSchema = z
|
|
30
|
+
.string()
|
|
31
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u);
|
|
32
|
+
const outputChunkSchema = z.object({
|
|
33
|
+
at_ms: z.number().int().nonnegative().max(300_000),
|
|
34
|
+
stream: z.enum(["stdout", "stderr"]),
|
|
35
|
+
data: z.string().max(1_000_000),
|
|
36
|
+
});
|
|
37
|
+
/** Actionable refusal shared by CLI, MCP, and Evidence import boundaries. */
|
|
38
|
+
export const LEGACY_PROCESS_CAPTURE_MESSAGE = "Process Capture v3 is unsupported. Re-run the scenario with this REA version using capture_process_scenario to produce Process Capture v4; captures cannot be upgraded because v4 requires new manifest and settlement evidence.";
|
|
39
|
+
/** Compute a canonical SHA-256 commitment independent of object key order. */
|
|
40
|
+
export const digestProcessCommitment = (value) => {
|
|
41
|
+
const serialized = canonicalize(value);
|
|
42
|
+
if (serialized === undefined)
|
|
43
|
+
throw new TypeError("Process capture commitment is not canonical JSON");
|
|
44
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
45
|
+
};
|
|
46
|
+
/** Build the secret-safe scenario projection committed by Process Capture v4. */
|
|
47
|
+
export const processScenarioCommitment = (scenario, executableSha256) => ({
|
|
48
|
+
...scenario,
|
|
49
|
+
environment: Object.fromEntries(Object.entries(scenario.environment).map(([name, value]) => [
|
|
50
|
+
name,
|
|
51
|
+
scenario.secret_aliases.includes(name) ? "<redacted-secret>" : value,
|
|
52
|
+
])),
|
|
53
|
+
unknown_registry_approved: scenario.unknown_registry_approved === true,
|
|
54
|
+
executable_sha256: executableSha256 ?? null,
|
|
55
|
+
});
|
|
56
|
+
/** Project observation settings shared by authority and reconstruction. */
|
|
57
|
+
export const processComparisonContract = (scenario) => ({
|
|
58
|
+
working_directory: scenario.working_directory,
|
|
59
|
+
inherit_environment: scenario.inherit_environment,
|
|
60
|
+
secret_aliases: scenario.secret_aliases,
|
|
61
|
+
network_access: scenario.network_access,
|
|
62
|
+
filesystem_roots: scenario.filesystem_roots,
|
|
63
|
+
terminal: scenario.terminal,
|
|
64
|
+
checkpoints: scenario.checkpoints,
|
|
65
|
+
events: scenario.events,
|
|
66
|
+
timeout_ms: scenario.timeout_ms,
|
|
67
|
+
idle_timeout_ms: scenario.idle_timeout_ms,
|
|
68
|
+
settle_ms: scenario.settle_ms,
|
|
69
|
+
limits: scenario.limits,
|
|
70
|
+
normalization: scenario.normalization,
|
|
71
|
+
command_shims: scenario.command_shims,
|
|
72
|
+
replay: scenario.replay,
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Boundary schema for one explicitly approved, bounded process experiment.
|
|
76
|
+
* Defaults are part of the evidence contract and must remain deterministic.
|
|
77
|
+
*/
|
|
78
|
+
export const processScenarioSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
approved: z
|
|
81
|
+
.literal(true)
|
|
82
|
+
.describe("Explicit per-call acknowledgement that this operation launches the target"),
|
|
83
|
+
unknown_registry_approved: z
|
|
84
|
+
.literal(true)
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Explicit approval to record capture residuals durably"),
|
|
87
|
+
executable: z.string().startsWith("/"),
|
|
88
|
+
arguments: z.array(z.string()).max(256).default([]),
|
|
89
|
+
working_directory: z.string().startsWith("/"),
|
|
90
|
+
environment: z.record(environmentName, z.string()).default({}),
|
|
91
|
+
inherit_environment: z.array(environmentName).max(64).default([]),
|
|
92
|
+
secret_aliases: z.array(environmentName).max(64).default([]),
|
|
93
|
+
network_access: z.literal("host").default("host"),
|
|
94
|
+
filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
|
|
95
|
+
terminal: z
|
|
96
|
+
.object({
|
|
97
|
+
columns: z.number().int().min(1).max(1_000).default(80),
|
|
98
|
+
rows: z.number().int().min(1).max(1_000).default(24),
|
|
99
|
+
scrollback: z.number().int().min(0).max(10_000).default(1_000),
|
|
100
|
+
})
|
|
101
|
+
.default({ columns: 80, rows: 24, scrollback: 1_000 }),
|
|
102
|
+
checkpoints: z
|
|
103
|
+
.array(z.object({
|
|
104
|
+
name: checkpointNameSchema,
|
|
105
|
+
trigger: z.discriminatedUnion("type", [
|
|
106
|
+
z.object({
|
|
107
|
+
type: z.literal("time"),
|
|
108
|
+
at_ms: z.number().int().nonnegative().max(300_000),
|
|
109
|
+
}),
|
|
110
|
+
z.object({
|
|
111
|
+
type: z.literal("terminal_literal"),
|
|
112
|
+
value: z.string().min(1).max(10_000),
|
|
113
|
+
occurrence: z.number().int().positive().max(1_000).default(1),
|
|
114
|
+
}),
|
|
115
|
+
z.object({ type: z.literal("root_exit") }),
|
|
116
|
+
z.object({ type: z.literal("settled") }),
|
|
117
|
+
]),
|
|
118
|
+
}))
|
|
119
|
+
.max(64)
|
|
120
|
+
.default([]),
|
|
121
|
+
command_shims: z
|
|
122
|
+
.array(z.object({
|
|
123
|
+
name: commandNameSchema,
|
|
124
|
+
routes: z
|
|
125
|
+
.array(z.object({
|
|
126
|
+
arguments: z.array(z.string()).max(256),
|
|
127
|
+
outputs: z.array(outputChunkSchema).max(1_000).default([]),
|
|
128
|
+
termination: z.discriminatedUnion("type", [
|
|
129
|
+
z.object({
|
|
130
|
+
type: z.literal("exit"),
|
|
131
|
+
code: z.number().int().min(0).max(255),
|
|
132
|
+
}),
|
|
133
|
+
z.object({
|
|
134
|
+
type: z.literal("signal"),
|
|
135
|
+
signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]),
|
|
136
|
+
}),
|
|
137
|
+
]),
|
|
138
|
+
max_calls: z.number().int().positive().max(100).default(1),
|
|
139
|
+
}))
|
|
140
|
+
.min(1)
|
|
141
|
+
.max(100),
|
|
142
|
+
}))
|
|
143
|
+
.max(32)
|
|
144
|
+
.default([]),
|
|
145
|
+
events: z
|
|
146
|
+
.array(z.discriminatedUnion("type", [
|
|
147
|
+
z.object({
|
|
148
|
+
...timedEventBase,
|
|
149
|
+
type: z.literal("input"),
|
|
150
|
+
data: z.string(),
|
|
151
|
+
sensitive: z.boolean().default(false),
|
|
152
|
+
}),
|
|
153
|
+
z.object({
|
|
154
|
+
...timedEventBase,
|
|
155
|
+
type: z.literal("resize"),
|
|
156
|
+
columns: z.number().int().min(1).max(1_000),
|
|
157
|
+
rows: z.number().int().min(1).max(1_000),
|
|
158
|
+
}),
|
|
159
|
+
z.object({
|
|
160
|
+
...timedEventBase,
|
|
161
|
+
type: z.literal("signal"),
|
|
162
|
+
signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]),
|
|
163
|
+
}),
|
|
164
|
+
]))
|
|
165
|
+
.max(1_000)
|
|
166
|
+
.default([]),
|
|
167
|
+
timeout_ms: positiveBudget.max(300_000).default(30_000),
|
|
168
|
+
idle_timeout_ms: positiveBudget.max(300_000).default(30_000),
|
|
169
|
+
settle_ms: z.number().int().nonnegative().max(10_000).default(100),
|
|
170
|
+
limits: z
|
|
171
|
+
.object({
|
|
172
|
+
output_bytes: positiveBudget.max(10_000_000).default(1_000_000),
|
|
173
|
+
frames: positiveBudget.max(100_000).default(10_000),
|
|
174
|
+
files: positiveBudget.max(100_000).default(10_000),
|
|
175
|
+
file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
|
|
176
|
+
processes: positiveBudget.max(10_000).default(1_000),
|
|
177
|
+
protocol_events: positiveBudget.max(100_000).default(10_000),
|
|
178
|
+
protocol_body_bytes: positiveBudget.max(10_000_000).default(1_000_000),
|
|
179
|
+
connections: positiveBudget.max(1_000).default(100),
|
|
180
|
+
filesystem_depth: z.number().int().min(1).max(64).default(16),
|
|
181
|
+
})
|
|
182
|
+
.default({
|
|
183
|
+
output_bytes: 1_000_000,
|
|
184
|
+
frames: 10_000,
|
|
185
|
+
files: 10_000,
|
|
186
|
+
file_bytes: 10_000_000,
|
|
187
|
+
processes: 1_000,
|
|
188
|
+
protocol_events: 10_000,
|
|
189
|
+
protocol_body_bytes: 1_000_000,
|
|
190
|
+
connections: 100,
|
|
191
|
+
filesystem_depth: 16,
|
|
192
|
+
}),
|
|
193
|
+
normalization: z
|
|
194
|
+
.object({
|
|
195
|
+
paths: z.boolean().default(true),
|
|
196
|
+
pids: z.boolean().default(true),
|
|
197
|
+
ports: z.boolean().default(true),
|
|
198
|
+
time_bucket_ms: positiveBudget.max(60_000).default(10),
|
|
199
|
+
patterns: z
|
|
200
|
+
.array(z.object({
|
|
201
|
+
pattern: z.string().max(500),
|
|
202
|
+
replacement: z.string().max(100),
|
|
203
|
+
}))
|
|
204
|
+
.max(32)
|
|
205
|
+
.default([]),
|
|
206
|
+
})
|
|
207
|
+
.default({
|
|
208
|
+
paths: true,
|
|
209
|
+
pids: true,
|
|
210
|
+
ports: true,
|
|
211
|
+
time_bucket_ms: 10,
|
|
212
|
+
patterns: [],
|
|
213
|
+
}),
|
|
214
|
+
replay: z
|
|
215
|
+
.object({
|
|
216
|
+
http: z
|
|
217
|
+
.array(z.object({
|
|
218
|
+
method: z.string().max(16),
|
|
219
|
+
path: z.string().startsWith("/"),
|
|
220
|
+
status: z.number().int().min(100).max(599),
|
|
221
|
+
body: z.string().max(1_000_000),
|
|
222
|
+
request_headers: z.record(z.string(), z.string()).default({}),
|
|
223
|
+
request_body: z.string().max(1_000_000).optional(),
|
|
224
|
+
response_headers: z.record(z.string(), z.string()).default({}),
|
|
225
|
+
delay_ms: z.number().int().nonnegative().max(30_000).default(0),
|
|
226
|
+
disconnect: z.boolean().default(false),
|
|
227
|
+
max_calls: z.number().int().min(1).max(100).default(1),
|
|
228
|
+
}))
|
|
229
|
+
.max(100)
|
|
230
|
+
.default([]),
|
|
231
|
+
websocket_messages: z
|
|
232
|
+
.array(z.string().max(1_000_000))
|
|
233
|
+
.max(100)
|
|
234
|
+
.default([]),
|
|
235
|
+
websocket_connections: z
|
|
236
|
+
.array(z.object({
|
|
237
|
+
messages: z
|
|
238
|
+
.array(z.object({
|
|
239
|
+
data: z.string().max(1_000_000),
|
|
240
|
+
delay_ms: z
|
|
241
|
+
.number()
|
|
242
|
+
.int()
|
|
243
|
+
.nonnegative()
|
|
244
|
+
.max(30_000)
|
|
245
|
+
.default(0),
|
|
246
|
+
}))
|
|
247
|
+
.max(100),
|
|
248
|
+
disconnect_after: z.boolean().default(false),
|
|
249
|
+
}))
|
|
250
|
+
.max(100)
|
|
251
|
+
.default([]),
|
|
252
|
+
})
|
|
253
|
+
.default({
|
|
254
|
+
http: [],
|
|
255
|
+
websocket_messages: [],
|
|
256
|
+
websocket_connections: [],
|
|
257
|
+
}),
|
|
258
|
+
})
|
|
259
|
+
.strict()
|
|
260
|
+
.superRefine((scenario, context) => {
|
|
261
|
+
for (let index = 1; index < scenario.events.length; index += 1) {
|
|
262
|
+
if (scenario.events[index].at_ms < scenario.events[index - 1].at_ms) {
|
|
263
|
+
context.addIssue({
|
|
264
|
+
code: "custom",
|
|
265
|
+
message: "events must be ordered by at_ms",
|
|
266
|
+
path: ["events", index],
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const secrets = new Set(scenario.secret_aliases);
|
|
271
|
+
for (const alias of secrets) {
|
|
272
|
+
if (!(alias in scenario.environment)) {
|
|
273
|
+
context.addIssue({
|
|
274
|
+
code: "custom",
|
|
275
|
+
message: "secret alias has no environment value",
|
|
276
|
+
path: ["secret_aliases"],
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const explicit = new Set(Object.keys(scenario.environment));
|
|
281
|
+
for (const name of [...explicit, ...scenario.inherit_environment]) {
|
|
282
|
+
if (reservedEnvironment.has(name)) {
|
|
283
|
+
context.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
message: `${name} is reserved by the process adapter`,
|
|
286
|
+
path: ["environment", name],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const name of scenario.inherit_environment) {
|
|
291
|
+
if (explicit.has(name)) {
|
|
292
|
+
context.addIssue({
|
|
293
|
+
code: "custom",
|
|
294
|
+
message: "an environment name cannot be explicit and inherited",
|
|
295
|
+
path: ["inherit_environment"],
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const event of scenario.events) {
|
|
300
|
+
if (event.at_ms > scenario.timeout_ms) {
|
|
301
|
+
context.addIssue({
|
|
302
|
+
code: "custom",
|
|
303
|
+
message: "event occurs after the scenario timeout",
|
|
304
|
+
path: ["events"],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const checkpointNames = scenario.checkpoints.map(({ name }) => name);
|
|
309
|
+
for (const [index, name] of checkpointNames.entries()) {
|
|
310
|
+
if (name === "before" || name === "after_settlement")
|
|
311
|
+
context.addIssue({
|
|
312
|
+
code: "custom",
|
|
313
|
+
message: "checkpoint name is reserved by the capture lifecycle",
|
|
314
|
+
path: ["checkpoints", index, "name"],
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (new Set(checkpointNames).size !== checkpointNames.length)
|
|
318
|
+
context.addIssue({
|
|
319
|
+
code: "custom",
|
|
320
|
+
message: "checkpoint names must be unique",
|
|
321
|
+
path: ["checkpoints"],
|
|
322
|
+
});
|
|
323
|
+
const shimNames = scenario.command_shims.map(({ name }) => name);
|
|
324
|
+
if (new Set(shimNames).size !== shimNames.length)
|
|
325
|
+
context.addIssue({
|
|
326
|
+
code: "custom",
|
|
327
|
+
message: "command shim names must be unique",
|
|
328
|
+
path: ["command_shims"],
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
/** Parse untrusted process scenario input and apply safe default budgets. */
|
|
332
|
+
export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
|
|
333
|
+
const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
|
|
334
|
+
resolve(candidate).startsWith(`${resolve(root)}${resolve(root).endsWith("/") ? "" : "/"}`);
|
|
335
|
+
/** Evaluate scenario authority before any process or filesystem side effect occurs. */
|
|
336
|
+
export const authorizeProcessScenario = (scenario, policy) => {
|
|
337
|
+
if (!policy.enabled)
|
|
338
|
+
return { allowed: false, reason: "process capture is disabled" };
|
|
339
|
+
if (scenario.network_access === "host" && !policy.allowExternalNetwork)
|
|
340
|
+
return {
|
|
341
|
+
allowed: false,
|
|
342
|
+
reason: "host network access is not approved by operator policy",
|
|
343
|
+
};
|
|
344
|
+
if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
|
|
345
|
+
return { allowed: false, reason: "executable is outside approved roots" };
|
|
346
|
+
}
|
|
347
|
+
if (!policy.workingRoots.some((root) => isWithin(scenario.working_directory, root))) {
|
|
348
|
+
return {
|
|
349
|
+
allowed: false,
|
|
350
|
+
reason: "working directory is outside approved roots",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const requestedNames = [
|
|
354
|
+
...Object.keys(scenario.environment),
|
|
355
|
+
...scenario.inherit_environment,
|
|
356
|
+
];
|
|
357
|
+
if (requestedNames.some((name) => !policy.allowedEnvironment.includes(name))) {
|
|
358
|
+
return {
|
|
359
|
+
allowed: false,
|
|
360
|
+
reason: "scenario requests an environment variable not allowed by policy",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (scenario.filesystem_roots.some((path) => !policy.workingRoots.some((root) => isWithin(path, root)))) {
|
|
364
|
+
return {
|
|
365
|
+
allowed: false,
|
|
366
|
+
reason: "filesystem root is outside approved roots",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return { allowed: true };
|
|
370
|
+
};
|
|
@@ -18,7 +18,7 @@ const MAX_CANONICAL_BYTES = 32 * 1024 * 1024;
|
|
|
18
18
|
const providers = {
|
|
19
19
|
behavioral: {
|
|
20
20
|
operation: "compare_process_captures",
|
|
21
|
-
predicate: "rea.process-comparison/
|
|
21
|
+
predicate: "rea.process-comparison/v3",
|
|
22
22
|
id: "rea-process",
|
|
23
23
|
name: "REA deterministic process harness",
|
|
24
24
|
},
|
|
@@ -137,7 +137,7 @@ const evaluateClaim = (claim, records, heads) => {
|
|
|
137
137
|
};
|
|
138
138
|
const validateSourceKinds = (claim, sources) => {
|
|
139
139
|
const expected = claim.kind === "behavioral"
|
|
140
|
-
? ["capture_process_scenario", "rea.process-capture/
|
|
140
|
+
? ["capture_process_scenario", "rea.process-capture/v4"]
|
|
141
141
|
: claim.kind === "structural-function"
|
|
142
142
|
? ["analyze_function", "rea.analysis/v2"]
|
|
143
143
|
: ["inventory_artifact", "rea.analysis/v2"];
|
|
@@ -160,7 +160,7 @@ const validateComparisonIdentity = (claim, evidence) => {
|
|
|
160
160
|
evidence.predicate_type !== expected.predicate ||
|
|
161
161
|
evidence.provider.id !== expected.id ||
|
|
162
162
|
evidence.provider.name !== expected.name ||
|
|
163
|
-
evidence.provider.version !== "1" ||
|
|
163
|
+
evidence.provider.version !== (claim.kind === "behavioral" ? "3" : "1") ||
|
|
164
164
|
evidence.confidence !== "derived" ||
|
|
165
165
|
evidence.authority !== "analyst-inference" ||
|
|
166
166
|
evidence.subject !== null)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Return whether a Node.js version satisfies REA's tested runtime families. */
|
|
2
|
+
export const supportsNodeVersion = (version) => {
|
|
3
|
+
const [majorText = "", minorText = ""] = version.split(".");
|
|
4
|
+
const major = Number.parseInt(majorText, 10);
|
|
5
|
+
const minor = Number.parseInt(minorText, 10);
|
|
6
|
+
return ((major === 22 && minor >= 19) ||
|
|
7
|
+
(major >= 24 && (major !== 24 || minor >= 11)));
|
|
8
|
+
};
|
|
@@ -138,7 +138,7 @@ const EXPECTED = {
|
|
|
138
138
|
"REA function comparison",
|
|
139
139
|
],
|
|
140
140
|
compare_process_captures: [
|
|
141
|
-
"rea.process-comparison/
|
|
141
|
+
"rea.process-comparison/v3",
|
|
142
142
|
"rea-process",
|
|
143
143
|
"REA deterministic process harness",
|
|
144
144
|
],
|
|
@@ -299,7 +299,8 @@ const assertComparisonIdentity = (evidence) => {
|
|
|
299
299
|
evidence.predicate_type !== expected[0] ||
|
|
300
300
|
evidence.provider.id !== expected[1] ||
|
|
301
301
|
evidence.provider.name !== expected[2] ||
|
|
302
|
-
evidence.provider.version !==
|
|
302
|
+
evidence.provider.version !==
|
|
303
|
+
(evidence.operation === "compare_process_captures" ? "3" : "1") ||
|
|
303
304
|
evidence.confidence !== "derived" ||
|
|
304
305
|
evidence.authority !== "analyst-inference" ||
|
|
305
306
|
evidence.subject !== null ||
|