@mettlelabs/mettle 0.0.0-beta.2-linux-arm64 → 0.0.0-beta.2

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.
Files changed (62) hide show
  1. package/bin/mettle.js +53 -0
  2. package/bin/postinstall.js +28 -0
  3. package/package.json +19 -13
  4. package/THIRD_PARTY_NOTICES.md +0 -11974
  5. package/bin/mettle +0 -4
  6. package/harness/audit-investigation.mjs +0 -203
  7. package/harness/chunk-1sygmp11.mjs +0 -18142
  8. package/harness/chunk-1ykxpmaz.mjs +0 -7887
  9. package/harness/chunk-2pm2sayw.mjs +0 -248
  10. package/harness/chunk-3m7jn5ac.mjs +0 -16242
  11. package/harness/chunk-3wt8a8fq.mjs +0 -1716
  12. package/harness/chunk-3wvjs93w.mjs +0 -538
  13. package/harness/chunk-49ytr8d5.mjs +0 -33761
  14. package/harness/chunk-4bepyxmq.mjs +0 -113
  15. package/harness/chunk-4vx34w5h.mjs +0 -2357
  16. package/harness/chunk-50sbrf09.mjs +0 -18972
  17. package/harness/chunk-56csr8sw.mjs +0 -322
  18. package/harness/chunk-5wgmv4np.mjs +0 -585
  19. package/harness/chunk-65eyq32y.mjs +0 -225
  20. package/harness/chunk-6fnh6qhk.mjs +0 -195
  21. package/harness/chunk-6kgqw9ny.mjs +0 -32303
  22. package/harness/chunk-7b242j0n.mjs +0 -14
  23. package/harness/chunk-9dg1btk1.mjs +0 -2818
  24. package/harness/chunk-ageqxj9v.mjs +0 -63008
  25. package/harness/chunk-b0qbt5jd.mjs +0 -14
  26. package/harness/chunk-b5g1r3fz.mjs +0 -4785
  27. package/harness/chunk-b71gpkxg.mjs +0 -1514
  28. package/harness/chunk-bdv1rx4a.mjs +0 -20364
  29. package/harness/chunk-c1631s2x.mjs +0 -12241
  30. package/harness/chunk-c7dx5mdd.mjs +0 -10807
  31. package/harness/chunk-d5xgxsnp.mjs +0 -74
  32. package/harness/chunk-erg1fbh0.mjs +0 -0
  33. package/harness/chunk-fh78v9vn.mjs +0 -19366
  34. package/harness/chunk-fjx29qvp.mjs +0 -50
  35. package/harness/chunk-gryka6kg.mjs +0 -39
  36. package/harness/chunk-h1td5kq9.mjs +0 -422
  37. package/harness/chunk-jb0r19et.mjs +0 -180
  38. package/harness/chunk-jp69h6sj.mjs +0 -95
  39. package/harness/chunk-mfn574ss.mjs +0 -179
  40. package/harness/chunk-nz5sv9as.mjs +0 -1642
  41. package/harness/chunk-p9d3cgvq.mjs +0 -452
  42. package/harness/chunk-pq6wvwxr.mjs +0 -12
  43. package/harness/chunk-qcm9arf8.mjs +0 -7736
  44. package/harness/chunk-qr8zfqjd.mjs +0 -24
  45. package/harness/chunk-rd80vvc5.mjs +0 -22047
  46. package/harness/chunk-rz2tz6jp.mjs +0 -26
  47. package/harness/chunk-sbkgc3kn.mjs +0 -8
  48. package/harness/chunk-v66qvpnb.mjs +0 -169
  49. package/harness/chunk-vjaxzab3.mjs +0 -136
  50. package/harness/chunk-vr1n5nza.mjs +0 -1045
  51. package/harness/chunk-vsf2zajv.mjs +0 -3867
  52. package/harness/chunk-w0qwc5z2.mjs +0 -41
  53. package/harness/chunk-wq253fj4.mjs +0 -19041
  54. package/harness/chunk-wyv7ceq4.mjs +0 -0
  55. package/harness/chunk-xnqknk2r.mjs +0 -198
  56. package/harness/chunk-z6n9thea.mjs +0 -45146
  57. package/harness/chunk-zty0d3bw.mjs +0 -0
  58. package/harness/chunk-zvkxtj82.mjs +0 -42
  59. package/harness/issue-api-token.mjs +0 -35
  60. package/harness/launcher.mjs +0 -459
  61. package/harness/sandbox.mjs +0 -133
  62. package/harness/server.mjs +0 -87314
@@ -1,2818 +0,0 @@
1
- import {
2
- IdSchema,
3
- SourceLocationSchema,
4
- HarnessError,
5
- canonicalJson,
6
- sha256,
7
- digest
8
- } from "./chunk-rd80vvc5.mjs";
9
- import {
10
- CHILD_TIMEOUT_MAX_MS,
11
- CommitSchema,
12
- RepositoryPathSchema,
13
- SemgrepProfileSchema,
14
- GVISOR_RUNTIME,
15
- SandboxRuntimeSchema,
16
- SandboxConfigSchema,
17
- sandboxRuntime,
18
- sandboxAllowsExecution,
19
- TaskNameSchema,
20
- PROBE_TOOL,
21
- SHELL_TOOL,
22
- HTTP_HEADER_LIMIT,
23
- HTTP_RESPONSE_LIMIT,
24
- WEBSITE_RESPONSE_LIMIT,
25
- withinHttpScope,
26
- PublicAddressSchema,
27
- PLAYWRIGHT_VERSION
28
- } from "./chunk-c7dx5mdd.mjs";
29
- import {
30
- exports_iso
31
- } from "./chunk-b71gpkxg.mjs";
32
- import {
33
- toJSONSchema,
34
- string,
35
- uuid,
36
- url,
37
- number,
38
- boolean,
39
- _null,
40
- unknown,
41
- array,
42
- object,
43
- strictObject,
44
- looseObject,
45
- union,
46
- discriminatedUnion,
47
- record,
48
- _enum,
49
- literal
50
- } from "./chunk-1sygmp11.mjs";
51
-
52
- // apps/harness/src/storage/joern.ts
53
- var JOERN_TIMEOUT_MS = 180000;
54
- var JOERN_TASK_TIMEOUT_MS = 210000;
55
- var JOERN_QUERY_ID = "mettle.source-trace-dataflow-v1";
56
- var JoernProfileSchema = strictObject({
57
- version: literal("4.0.647")
58
- });
59
- var JoernTraceSchema = strictObject({
60
- source: SourceLocationSchema,
61
- sink: SourceLocationSchema,
62
- locations: array(SourceLocationSchema).min(2).max(16)
63
- });
64
- function joernCandidate(finding, binding) {
65
- if (finding.surface !== "code" || finding.claim !== "vulnerability" || finding.status !== "candidate" || finding.verification.result !== "pending" || finding.subject.kind !== "source" || finding.subject.repository !== binding.repository || finding.subject.revision !== binding.revision || !finding.sourceTrace || !finding.evidence.toolCalls.some((call) => ["semgrep_scan", "secret_scan", "dependency_scan"].includes(call.tool)))
66
- return;
67
- const steps = finding.sourceTrace.pathToImpact;
68
- const inputs = steps.filter((step) => step.kind === "input");
69
- const sinks = steps.filter((step) => step.kind === "sink");
70
- const locations = steps.map((step) => step.location);
71
- if (inputs.length !== 1 || sinks.length !== 1 || locations.some((location) => location.revision !== binding.revision || !binding.paths.includes(location.path) || !/\.(?:[cm]?js|jsx|[cm]?ts|tsx)$/.test(location.path)))
72
- return;
73
- return JoernTraceSchema.parse({
74
- source: inputs[0].location,
75
- sink: sinks[0].location,
76
- locations
77
- });
78
- }
79
-
80
- // apps/harness/src/storage/scanners.ts
81
- var EcosystemSchema = _enum([
82
- "npm",
83
- "PyPI",
84
- "Go",
85
- "crates.io",
86
- "Maven",
87
- "Packagist",
88
- "RubyGems"
89
- ]);
90
- var MAX_DATABASE_BYTES = 268435456;
91
- var SEMGREP_VERSION_TIMEOUT_MS = 60000;
92
- var SEMGREP_SCAN_TIMEOUT_MS = 120000;
93
- var SEMGREP_TASK_TIMEOUT_MS = SEMGREP_VERSION_TIMEOUT_MS + SEMGREP_SCAN_TIMEOUT_MS + 30000;
94
- var SecretScanProfileSchema = strictObject({
95
- version: literal("8.30.1")
96
- });
97
- var DependencyScanProfileSchema = strictObject({
98
- version: literal("2.6.0"),
99
- paths: array(RepositoryPathSchema).min(1).max(64).refine((paths) => new Set(paths).size === paths.length),
100
- database: strictObject({
101
- revision: string().min(1).max(128),
102
- capturedAt: exports_iso.datetime(),
103
- ecosystems: array(strictObject({
104
- name: EcosystemSchema,
105
- sha256: string().regex(/^[a-f0-9]{64}$/)
106
- })).min(1).max(EcosystemSchema.options.length).refine((items) => new Set(items.map((item) => item.name)).size === items.length)
107
- })
108
- });
109
- var SOURCE_SCANNER_LABEL = "org.mettle.source-scanners";
110
- var ScannerImageProfilesSchema = strictObject({
111
- secretScan: SecretScanProfileSchema,
112
- semgrep: SemgrepProfileSchema,
113
- joern: JoernProfileSchema.optional(),
114
- dependencyDatabase: DependencyScanProfileSchema.shape.database.optional()
115
- });
116
- var SCANNER_FINDINGS_MAX = 40;
117
- var SOURCE_SCANNER_INPUT_LIMIT = 33554432;
118
- var SOURCE_SCANNER_TIMEOUT_MS = 120000;
119
- var SOURCE_SCANNER_VERSION_TIMEOUT_MS = 1e4;
120
- var SOURCE_SCANNER_TASK_TIMEOUT_MS = 150000;
121
- var ScannerStatusSchema = _enum([
122
- "completed",
123
- "failed",
124
- "timed_out",
125
- "output_limit",
126
- "unavailable",
127
- "version_mismatch",
128
- "database_mismatch"
129
- ]);
130
-
131
- // apps/harness/src/sandbox/worker.ts
132
- import { setTimeout as sleep } from "node:timers/promises";
133
-
134
- // apps/harness/src/tools/registry.ts
135
- function counted(count, noun, plural = `${noun}s`) {
136
- return `${count} ${count === 1 ? noun : plural}`;
137
- }
138
- var RAN = {
139
- completed: "Completed",
140
- passed: "Passed",
141
- failed: "Failed",
142
- timed_out: "Timed out",
143
- output_limit: "Output limit reached",
144
- unavailable: "Unavailable"
145
- };
146
- function ran(status, exitCode) {
147
- return exitCode === null ? RAN[status] : `${RAN[status]} (exit ${exitCode})`;
148
- }
149
- function scanned(scan, coverage) {
150
- if (scan.status !== "completed")
151
- return `Not completed (${scan.status.replaceAll("_", " ")})`;
152
- const count = scan.findingCount ?? scan.findings.length;
153
- return `${counted(count, "finding")} · ${coverage}`;
154
- }
155
-
156
- class ToolRegistry {
157
- tools = new Map;
158
- constructor(definitions) {
159
- if (!definitions.length)
160
- throw new HarnessError("invalid_tools", "At least one registered tool is required.");
161
- for (const definition of definitions) {
162
- if (!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(definition.name) || this.tools.has(definition.name) || !definition.description)
163
- throw new HarnessError("invalid_tools", "Tool metadata is invalid or duplicated.");
164
- this.tools.set(definition.name, Object.freeze({ ...definition }));
165
- }
166
- }
167
- describe() {
168
- return [...this.tools.values()].map(({ name, description, schema }) => ({
169
- name,
170
- description,
171
- schema
172
- }));
173
- }
174
- inventory() {
175
- return [...this.tools.values()].map((tool) => ({
176
- name: tool.name,
177
- version: "1",
178
- inputDigest: digest(toJSONSchema(tool.schema, { unrepresentable: "any" })),
179
- outputDigest: digest(toJSONSchema(tool.outputSchema, { unrepresentable: "any" }))
180
- })).sort((a, b) => a.name.localeCompare(b.name));
181
- }
182
- select(names) {
183
- return new ToolRegistry([...this.tools.values()].filter((tool) => names.includes(tool.name)));
184
- }
185
- unavailable(names, reason) {
186
- return new ToolRegistry([...this.tools.values()].map((tool) => names.includes(tool.name) ? { ...tool, unavailableReason: reason } : tool));
187
- }
188
- admit(name, input, canonical) {
189
- const tool = this.tools.get(name);
190
- if (!tool)
191
- throw new HarnessError("tool_denied", "The tool is not registered for this run.");
192
- const parsed = tool.schema.safeParse(input);
193
- if (!parsed.success || canonicalJson(parsed.data) !== canonical)
194
- throw new HarnessError("tool_denied", "Tool arguments must match the finalized proposal without transformation.");
195
- return tool;
196
- }
197
- }
198
-
199
- // apps/harness/src/sandbox/docker.ts
200
- import { request } from "node:http";
201
-
202
- // apps/harness/src/sandbox/limits.ts
203
- var TASK_TIMEOUT_MS = 30000;
204
- var TASK_INPUT_LIMIT = 1048576;
205
- var TASK_INPUT_LIMITS = {
206
- semgrep_scan: SOURCE_SCANNER_INPUT_LIMIT,
207
- secret_scan: SOURCE_SCANNER_INPUT_LIMIT,
208
- dependency_scan: SOURCE_SCANNER_INPUT_LIMIT,
209
- joern_confirm: SOURCE_SCANNER_INPUT_LIMIT
210
- };
211
- var TASK_INPUT_MAX = Math.max(TASK_INPUT_LIMIT, ...Object.values(TASK_INPUT_LIMITS));
212
- var TASK_RESULT_LIMIT = 32768;
213
- var PDF_BYTES_LIMIT = 524288;
214
- var PDF_RESULT_LIMIT = 1048576;
215
- var BROKER_RESPONSE_LIMIT = 4 * Math.ceil(WEBSITE_RESPONSE_LIMIT / 3) + 12 * HTTP_HEADER_LIMIT + 1024;
216
- var CHILD_OUTPUT_KEEP = 4096;
217
- var TASK_CONTAINER_LIFETIME_S = Math.ceil(Math.max(SEMGREP_TASK_TIMEOUT_MS, JOERN_TASK_TIMEOUT_MS) / 1000) + 15;
218
- var SANDBOX_UID = 65534;
219
- var REPO_SEARCH_FILES = 100;
220
- var REPO_SEARCH_MATCHES = 20;
221
- var REPO_EXCERPT = 256;
222
-
223
- // apps/harness/src/sandbox/exchange.ts
224
- class DockerExchange {
225
- exchange;
226
- write;
227
- fail;
228
- framed = Buffer.alloc(0);
229
- lines = "";
230
- total = 0;
231
- pending = false;
232
- final;
233
- decoder = new TextDecoder("utf-8", { fatal: true });
234
- constructor(exchange, write, fail) {
235
- this.exchange = exchange;
236
- this.write = write;
237
- this.fail = fail;
238
- }
239
- receive(chunk) {
240
- this.total += chunk.length;
241
- if (this.total > 1048576)
242
- throw new Error;
243
- this.framed = Buffer.concat([this.framed, chunk]);
244
- while (this.framed.length >= 8) {
245
- if (this.framed[0] !== 1 || this.framed.readUIntBE(1, 3) !== 0)
246
- throw new Error;
247
- const length = this.framed.readUInt32BE(4);
248
- if (length > 65536)
249
- throw new Error;
250
- if (this.framed.length < length + 8)
251
- break;
252
- this.lines += this.decoder.decode(this.framed.subarray(8, length + 8), {
253
- stream: true
254
- });
255
- this.framed = this.framed.subarray(length + 8);
256
- if (Buffer.byteLength(this.lines) > 524288)
257
- throw new Error;
258
- while (this.lines.includes(`
259
- `)) {
260
- const boundary = this.lines.indexOf(`
261
- `);
262
- const line = this.lines.slice(0, boundary);
263
- this.lines = this.lines.slice(boundary + 1);
264
- if (this.final || this.pending)
265
- throw new Error;
266
- const message = JSON.parse(line);
267
- if (message && Object.keys(message).length === 1 && "request" in message) {
268
- this.pending = true;
269
- this.exchange(message.request).then((response) => Buffer.from(`${JSON.stringify({ response })}
270
- `), () => Buffer.from(`${JSON.stringify({ error: "refused" })}
271
- `)).then((encoded) => {
272
- if (encoded.length > BROKER_RESPONSE_LIMIT)
273
- throw new Error;
274
- this.pending = false;
275
- this.write(encoded);
276
- }).catch(this.fail);
277
- } else {
278
- if (!message || Object.keys(message).sort().join(",") !== "inputDigest,value")
279
- throw new Error;
280
- const body = Buffer.from(line);
281
- if (body.length > TASK_RESULT_LIMIT)
282
- throw new Error;
283
- const header = Buffer.alloc(8);
284
- header[0] = 1;
285
- header.writeUInt32BE(body.length, 4);
286
- this.final = Buffer.concat([header, body]);
287
- }
288
- }
289
- }
290
- }
291
- result() {
292
- if (!this.final || this.pending || this.lines || this.framed.length || this.decoder.decode())
293
- throw new HarnessError("sandbox_protocol", "The brokered task did not end with one complete, bound result.");
294
- return this.final;
295
- }
296
- }
297
-
298
- // apps/harness/src/sandbox/docker.ts
299
- class DockerEngine {
300
- socket;
301
- constructor(socket) {
302
- this.socket = socket;
303
- }
304
- startExec(id, input, signal, timeoutMs, exchange, outputLimit = 65536) {
305
- if (!/^[a-f0-9]{64}$/.test(id) || input.length > TASK_INPUT_MAX || !Number.isSafeInteger(outputLimit) || outputLimit < 65536 || outputLimit > 1064960)
306
- throw new HarnessError("sandbox_input_limit", "Invalid isolated exec input.");
307
- return new Promise((resolve, reject) => {
308
- let complete;
309
- let rejectResult;
310
- const result = new Promise((resolveResult, failResult) => {
311
- complete = resolveResult;
312
- rejectResult = failResult;
313
- });
314
- result.catch(() => {
315
- return;
316
- });
317
- const payload = JSON.stringify({ Detach: false, Tty: false });
318
- const chunks = [];
319
- let size = 0;
320
- let transport;
321
- let finished = false;
322
- const client = request({
323
- socketPath: this.socket,
324
- method: "POST",
325
- path: `/v1.47/exec/${id}/start`,
326
- agent: false,
327
- headers: {
328
- Connection: "Upgrade",
329
- Upgrade: "tcp",
330
- "Content-Type": "application/json",
331
- "Content-Length": Buffer.byteLength(payload)
332
- }
333
- });
334
- const release = () => {
335
- clearTimeout(timer);
336
- signal.removeEventListener("abort", abort);
337
- };
338
- const fail = (code = "sandbox_transport") => {
339
- if (finished)
340
- return;
341
- finished = true;
342
- release();
343
- const error = new HarnessError(code, "Isolated exec transport failed; no result is invented.");
344
- reject(error);
345
- rejectResult(error);
346
- transport?.destroy();
347
- client.destroy();
348
- };
349
- const abort = () => fail();
350
- let timer = setTimeout(() => fail(), Math.min(2000, timeoutMs));
351
- signal.addEventListener("abort", abort, { once: true });
352
- client.once("error", () => fail());
353
- client.once("response", (response) => {
354
- response.destroy();
355
- fail("sandbox_protocol");
356
- });
357
- client.once("upgrade", (response, socket, head) => {
358
- transport = socket;
359
- if (finished || response.statusCode !== 101 || response.headers.upgrade?.toLowerCase() !== "tcp") {
360
- socket.destroy();
361
- fail("sandbox_protocol");
362
- return;
363
- }
364
- clearTimeout(timer);
365
- timer = setTimeout(() => fail(), timeoutMs);
366
- const interactive = exchange ? new DockerExchange(exchange, (bytes) => {
367
- if (!finished)
368
- socket.write(bytes);
369
- }, () => fail("web_uncertain")) : undefined;
370
- const receive = (chunk) => {
371
- if (interactive) {
372
- try {
373
- interactive.receive(chunk);
374
- } catch {
375
- fail("web_uncertain");
376
- }
377
- return;
378
- }
379
- size += chunk.length;
380
- if (size > outputLimit)
381
- fail("sandbox_output_limit");
382
- else
383
- chunks.push(chunk);
384
- };
385
- socket.on("data", receive);
386
- socket.once("error", () => fail());
387
- socket.once("close", () => {
388
- if (!finished)
389
- fail();
390
- });
391
- socket.once("end", () => {
392
- if (finished)
393
- return;
394
- let body;
395
- try {
396
- body = interactive ? interactive.result() : Buffer.concat(chunks);
397
- } catch {
398
- fail("web_uncertain");
399
- return;
400
- }
401
- finished = true;
402
- release();
403
- complete({ status: 200, body });
404
- socket.destroy();
405
- });
406
- receive(head);
407
- if (finished)
408
- return;
409
- resolve({ result });
410
- if (interactive)
411
- socket.write(Buffer.concat([input, Buffer.from(`
412
- `)]));
413
- else
414
- socket.end(input);
415
- });
416
- if (signal.aborted)
417
- fail();
418
- else
419
- client.end(payload);
420
- });
421
- }
422
- call(method, path, body, signal, timeoutMs = 2000, limit = 65536) {
423
- return new Promise((resolve, reject) => {
424
- const payload = body === undefined ? undefined : JSON.stringify(body);
425
- const client = request({
426
- socketPath: this.socket,
427
- method,
428
- path: `/v1.47${path}`,
429
- agent: false,
430
- signal,
431
- headers: payload ? {
432
- "Content-Type": "application/json",
433
- "Content-Length": Buffer.byteLength(payload)
434
- } : {}
435
- }, (response) => {
436
- const chunks = [];
437
- let size = 0;
438
- response.on("data", (chunk) => {
439
- size += chunk.length;
440
- if (size > limit) {
441
- response.destroy();
442
- client.destroy();
443
- reject(new HarnessError("sandbox_output_limit", "Sandbox transport output exceeded its limit."));
444
- } else
445
- chunks.push(chunk);
446
- });
447
- response.once("end", () => {
448
- clearTimeout(timer);
449
- resolve({
450
- status: response.statusCode ?? 0,
451
- body: Buffer.concat(chunks)
452
- });
453
- });
454
- response.once("error", fail);
455
- });
456
- const timer = setTimeout(() => client.destroy(), timeoutMs);
457
- const fail = () => {
458
- clearTimeout(timer);
459
- reject(new HarnessError("sandbox_transport", "The local sandbox engine request failed; its effect may be uncertain."));
460
- };
461
- client.once("error", fail);
462
- client.once("close", () => clearTimeout(timer));
463
- client.end(payload);
464
- });
465
- }
466
- }
467
- function dockerJson(response, status = 200) {
468
- if (response.status !== status)
469
- throw new HarnessError("sandbox_engine", "The sandbox engine rejected an operation.");
470
- try {
471
- return JSON.parse(response.body.toString("utf8"));
472
- } catch {
473
- throw new HarnessError("sandbox_protocol", "The sandbox engine returned an invalid response.");
474
- }
475
- }
476
- function dockerStatus(response, status) {
477
- if (response.status !== status)
478
- throw new HarnessError("sandbox_engine", "The sandbox engine rejected an operation.");
479
- }
480
- function dockerCreated(response) {
481
- if (response.status === 409)
482
- throw new HarnessError("sandbox_engine", "The sandbox engine reports a conflicting container name; it must be reconciled before reuse.");
483
- if (response.status !== 201)
484
- throw new HarnessError("sandbox_rejected", "The sandbox engine acknowledged and refused container creation; no container was created.");
485
- return dockerJson(response, 201);
486
- }
487
- function dockerFrames(response, streams = "stdout") {
488
- dockerStatus(response, 200);
489
- const chunks = [];
490
- let offset = 0;
491
- while (offset < response.body.length) {
492
- const header = response.body.subarray(offset, offset + 8);
493
- if (header.length !== 8 || header[0] !== 1 && (streams === "stdout" || header[0] !== 2) || header.readUIntBE(1, 3) !== 0)
494
- throw new HarnessError("sandbox_output", "The sandbox returned invalid output.");
495
- const end = offset + 8 + header.readUInt32BE(4);
496
- if (end > response.body.length)
497
- throw new HarnessError("sandbox_output", "The sandbox output was truncated.");
498
- chunks.push(response.body.subarray(offset + 8, end));
499
- offset = end;
500
- }
501
- return Buffer.concat(chunks);
502
- }
503
- function dockerLogs(response, streams = "stdout") {
504
- const bytes = dockerFrames(response, streams);
505
- try {
506
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
507
- } catch {
508
- throw new HarnessError("sandbox_output", "Sandbox output is not valid UTF-8.");
509
- }
510
- }
511
-
512
- // apps/harness/src/sandbox/policy.ts
513
- var PROBE_MARKER = `mettle-sandbox-probe-v2
514
- `;
515
- var PROBE_TIMEOUT_MS = 1e4;
516
- var SHELL_OUTPUT_MAX = 16384;
517
- var SHELL_KILL_AFTER_S = PROBE_TIMEOUT_MS / 1000 + 5;
518
- var SHELL_LOG_LIMIT = 262144;
519
- var ENGINE_LAUNCH_TIMEOUT_MS = 30000;
520
- var RESOURCE_PROFILES = {
521
- probe: {
522
- Memory: 268435456,
523
- MemorySwap: 268435456,
524
- NanoCpus: 500000000,
525
- PidsLimit: 32
526
- },
527
- tools: {
528
- Memory: 1073741824,
529
- MemorySwap: 1073741824,
530
- NanoCpus: 1e9,
531
- PidsLimit: 256,
532
- Tmpfs: { "/tmp": "rw,noexec,nosuid,nodev,size=256m" }
533
- },
534
- browser: {
535
- Memory: 2147483648,
536
- MemorySwap: 2147483648,
537
- NanoCpus: 2000000000,
538
- PidsLimit: 512,
539
- Tmpfs: { "/tmp": "rw,noexec,nosuid,nodev,size=512m" }
540
- },
541
- analysis: {
542
- Memory: 4294967296,
543
- MemorySwap: 4294967296,
544
- NanoCpus: 2000000000,
545
- PidsLimit: 512,
546
- Tmpfs: { "/tmp": "rw,noexec,nosuid,nodev,size=1024m" }
547
- }
548
- };
549
- var ShellCommandSchema = strictObject({
550
- command: string().min(1).max(2048).refine((command) => !command.includes("\x00"))
551
- });
552
- var RUNC_NETWORK_ASSERTION = [
553
- "for flags_file in /sys/class/net/*/flags; do",
554
- ' case "$flags_file" in */lo/flags) continue ;; esac',
555
- ' flags=$(cat "$flags_file")',
556
- ' test "$((flags & 1))" -eq 0',
557
- "done",
558
- `awk 'NR > 1 && $1 != "lo" { exit 1 }' /proc/net/route`,
559
- "if test -e /proc/net/ipv6_route; then",
560
- ` awk '$10 != "lo" { exit 1 }' /proc/net/ipv6_route`,
561
- "fi"
562
- ].join(`
563
- `);
564
- function probeScript(runtime) {
565
- return [
566
- "set -eu",
567
- 'test "$(id -u)" = 65534',
568
- 'test "$(id -g)" = 65534',
569
- `awk '$1 == "CapEff:" { seen=1; if ($2 !~ /^0+$/) exit 1 } END { if (!seen) exit 1 }' /proc/self/status`,
570
- `awk '$1 == "NoNewPrivs:" { seen=1; if ($2 != 1) exit 1 } END { if (!seen) exit 1 }' /proc/self/status`,
571
- ...runtime === "runc" ? [
572
- `awk '$1 == "Seccomp:" { seen=1; if ($2 != 2) exit 1 } END { if (!seen) exit 1 }' /proc/self/status`
573
- ] : [],
574
- `awk '$2 == "/" && $4 ~ /(^|,)ro(,|$)/ { seen=1 } END { if (!seen) exit 1 }' /proc/mounts`,
575
- runtime === "runc" ? RUNC_NETWORK_ASSERTION : `awk -F: 'NR > 2 { name=$1; gsub(/ /, "", name); if (name != "lo") exit 1 }' /proc/net/dev`,
576
- `/usr/local/bin/node -e 'const server = require("node:net").createServer(); server.on("error", () => process.exit(1)); server.listen(0, "127.0.0.1", () => server.close());'`,
577
- "test ! -e /var/run/docker.sock",
578
- "test ! -e /run/docker.sock",
579
- "test ! -e /host",
580
- "sleep 2",
581
- "printf 'mettle-sandbox-probe-v2\\n'"
582
- ].join(`
583
- `);
584
- }
585
- function sandboxConfig(environment = process.env) {
586
- const parsed = SandboxConfigSchema.safeParse({
587
- socket: environment.HARNESS_SANDBOX_SOCKET ?? "/var/run/docker.sock",
588
- image: environment.HARNESS_SANDBOX_IMAGE,
589
- ...environment.HARNESS_SANDBOX_RUNTIME !== undefined ? { runtime: environment.HARNESS_SANDBOX_RUNTIME } : {},
590
- ...environment.HARNESS_SANDBOX_ENGINE_ID !== undefined ? { engineId: environment.HARNESS_SANDBOX_ENGINE_ID } : {},
591
- ...environment.HARNESS_SANDBOX_LOCATION !== undefined ? { location: environment.HARNESS_SANDBOX_LOCATION } : {}
592
- });
593
- if (!parsed.success)
594
- throw new HarnessError("sandbox_config", "Use an approved repository@sha256 image or full local sha256 image ID, a local Unix socket, and HARNESS_SANDBOX_RUNTIME=mettle-runsc or runc.");
595
- return Object.freeze(parsed.data);
596
- }
597
- function launchSpec(image, script, labels, profile, runtime) {
598
- return {
599
- Image: image,
600
- User: "65534:65534",
601
- WorkingDir: "/",
602
- Entrypoint: ["/bin/sh"],
603
- Cmd: ["-c", script],
604
- Env: ["PATH=/usr/bin:/bin"],
605
- Labels: labels,
606
- NetworkDisabled: false,
607
- Tty: false,
608
- OpenStdin: false,
609
- Healthcheck: { Test: ["NONE"] },
610
- HostConfig: {
611
- Runtime: runtime,
612
- NetworkMode: "none",
613
- ReadonlyRootfs: true,
614
- Privileged: false,
615
- CapDrop: ["ALL"],
616
- SecurityOpt: [
617
- "no-new-privileges:true",
618
- ...runtime === "runc" ? ["seccomp=builtin"] : []
619
- ],
620
- ...RESOURCE_PROFILES[profile],
621
- RestartPolicy: { Name: "no", MaximumRetryCount: 0 },
622
- LogConfig: {
623
- Type: "json-file",
624
- Config: { "max-size": "64k", "max-file": "1" }
625
- }
626
- }
627
- };
628
- }
629
- var SHELL_BOUND = `timeout -s KILL ${SHELL_KILL_AFTER_S} /bin/sh -c "$1" sh "$2"; exit "$?"`;
630
- var SHELL_CAPTURE = [
631
- "exec 4>&1",
632
- `status=$( { { /bin/sh -c "$1" 2>&1 3>&- 4>&-; echo "$?" >&3; } | { head -c ${SHELL_OUTPUT_MAX + 1} >&4; cat >/dev/null; }; } 3>&1 )`,
633
- 'exit "$status"'
634
- ].join(`
635
- `);
636
- function shellSpec(image, command, labels, runtime) {
637
- const base = launchSpec(image, SHELL_BOUND, labels, "probe", runtime);
638
- return {
639
- ...base,
640
- Cmd: ["-c", SHELL_BOUND, "sh", SHELL_CAPTURE, command],
641
- HostConfig: {
642
- ...base.HostConfig,
643
- LogConfig: {
644
- Type: "json-file",
645
- Config: { "max-size": "2m", "max-file": "1" }
646
- }
647
- }
648
- };
649
- }
650
- function shellPolicyDigest(image, command, runtime) {
651
- return digest({
652
- spec: shellSpec(image, command, {}, runtime),
653
- timeoutMs: PROBE_TIMEOUT_MS
654
- });
655
- }
656
- function probePolicyDigest(image, runtime) {
657
- return digest({
658
- spec: launchSpec(image, probeScript(runtime), {}, "probe", runtime),
659
- timeoutMs: PROBE_TIMEOUT_MS
660
- });
661
- }
662
- function sandboxLabels(record) {
663
- return {
664
- "mettle.sandbox": "probe-v1",
665
- "mettle.run": record.runId,
666
- "mettle.call": record.callId,
667
- "mettle.epoch": String(record.epoch),
668
- "mettle.worker": record.workerId,
669
- "mettle.policy": record.policyDigest
670
- };
671
- }
672
-
673
- // apps/harness/src/storage/active-scanners.ts
674
- import { isIPv4 } from "node:net";
675
- var ACTIVE_SCAN_TIMEOUT_MS = 180000;
676
- var ACTIVE_SCAN_TASK_TIMEOUT_MS = 210000;
677
- var ACTIVE_SCAN_TOOLS = [
678
- "nuclei_scan",
679
- "sqli_confirm",
680
- "zap_scan"
681
- ];
682
- var common = {
683
- id: IdSchema,
684
- identity: IdSchema.optional(),
685
- url: url().max(2048),
686
- maxRequests: number().int().min(1).max(128),
687
- requestsPerSecond: number().int().min(1).max(5),
688
- maxResponseBytes: number().int().min(1024).max(HTTP_RESPONSE_LIMIT),
689
- maxTotalBytes: number().int().min(1024).max(4194304)
690
- };
691
- var ActiveScanProfileSchema = discriminatedUnion("tool", [
692
- strictObject({
693
- ...common,
694
- tool: literal("nuclei_scan"),
695
- version: literal("3.11.1"),
696
- template: strictObject({
697
- id: IdSchema,
698
- name: string().min(1).max(256),
699
- public: boolean(),
700
- paths: array(string().startsWith("/").max(512).refine((path) => !/[{}#\\\s]/.test(path))).min(1).max(8),
701
- status: array(number().int().min(200).max(599)).min(1).max(8),
702
- words: array(string().min(1).max(256)).min(1).max(8)
703
- })
704
- }),
705
- strictObject({
706
- ...common,
707
- tool: literal("sqli_confirm"),
708
- version: literal("1.10"),
709
- parameter: string().regex(/^[a-zA-Z0-9_]{1,64}$/),
710
- technique: _enum(["B", "E"]),
711
- request: strictObject({
712
- method: _enum(["POST", "PUT", "PATCH"]),
713
- contentType: _enum([
714
- "application/json",
715
- "application/x-www-form-urlencoded"
716
- ]),
717
- body: string().min(1).max(4096)
718
- }).optional()
719
- }),
720
- strictObject({
721
- ...common,
722
- tool: literal("zap_scan"),
723
- version: literal("2.17.0"),
724
- rules: array(number().int().positive().max(999999)).min(1).max(8).refine((rules) => new Set(rules).size === rules.length)
725
- })
726
- ]);
727
- var ActiveScannersSchema = strictObject({
728
- approvedBy: string().min(1).max(256),
729
- expiresAt: exports_iso.datetime(),
730
- disposableTarget: strictObject({
731
- instanceId: IdSchema,
732
- buildIdentityRef: string().min(1).max(2048)
733
- }),
734
- endpoints: array(strictObject({
735
- origin: url().max(2048),
736
- address: string().refine((address) => {
737
- if (!isIPv4(address))
738
- return false;
739
- const [first, second] = address.split(".").map(Number);
740
- return first !== 0 && first < 224 && !(first === 169 && second === 254) && address !== "168.63.129.16" && address !== "100.100.100.200";
741
- })
742
- })).min(1).max(4),
743
- profiles: array(ActiveScanProfileSchema).min(1).max(8)
744
- });
745
- function bodyParameters(body, contentType) {
746
- if (contentType === "application/x-www-form-urlencoded") {
747
- const entries = [...new URLSearchParams(body)];
748
- return entries.length <= 32 && entries.every(([key]) => /^[a-zA-Z0-9_]{1,64}$/.test(key)) ? entries : undefined;
749
- }
750
- try {
751
- const value = record(string().regex(/^[a-zA-Z0-9_]{1,64}$/), union([string(), number().finite(), boolean(), _null()])).parse(JSON.parse(body));
752
- return Object.keys(value).length <= 32 && JSON.stringify(value) === body ? Object.entries(value) : undefined;
753
- } catch {
754
- return;
755
- }
756
- }
757
- function scannerEndpoint(config, target, proposed) {
758
- const url2 = new URL(proposed);
759
- const scope = new URL(target);
760
- const endpoint = config.endpoints.find((entry) => entry.origin === url2.origin);
761
- if (!endpoint || url2.href !== proposed || !["http:", "https:"].includes(url2.protocol) || !withinHttpScope(new URL(scope.pathname, endpoint.origin), url2))
762
- throw new HarnessError("scanner_denied", "The scanner request is outside its approved origin, port or path.");
763
- return endpoint;
764
- }
765
- function validateActiveScanners(config, target, buildIdentityRef) {
766
- if (config.disposableTarget.buildIdentityRef !== buildIdentityRef || new Set(config.endpoints.map((entry) => entry.origin)).size !== config.endpoints.length || new Set(config.profiles.map((entry) => entry.id)).size !== config.profiles.length)
767
- throw new HarnessError("scanner_denied", "Scanner grants require unique endpoints/profiles and the approved disposable build identity.");
768
- for (const endpoint of config.endpoints) {
769
- const url2 = new URL(endpoint.origin);
770
- if (url2.origin !== endpoint.origin || url2.hostname !== new URL(target).hostname || !["http:", "https:"].includes(url2.protocol))
771
- throw new HarnessError("scanner_denied", "Scanner origins must explicitly name the target's HTTP(S) ports.");
772
- }
773
- for (const profile of config.profiles) {
774
- scannerEndpoint(config, target, profile.url);
775
- if (profile.tool === "nuclei_scan")
776
- for (const path of profile.template.paths)
777
- scannerEndpoint(config, target, new URL(path, profile.url).href);
778
- if (profile.tool === "sqli_confirm") {
779
- const query = new URL(profile.url).searchParams.getAll(profile.parameter);
780
- const body = profile.request ? bodyParameters(profile.request.body, profile.request.contentType) : undefined;
781
- const selected = body?.filter(([key]) => key === profile.parameter);
782
- if (profile.request ? !body || selected?.length !== 1 || typeof selected[0]?.[1] !== "string" || query.length !== 0 : query.length !== 1)
783
- throw new HarnessError("scanner_denied", "SQLMap requires one exact query or body parameter; JSON bodies must be canonical flat objects with a string test value.");
784
- }
785
- }
786
- }
787
- var SCANNER_REQUEST_STRIP_HEADERS = [
788
- "host",
789
- "connection",
790
- "content-length",
791
- "transfer-encoding",
792
- "upgrade",
793
- "trailer",
794
- "te",
795
- "keep-alive",
796
- "accept-encoding"
797
- ];
798
- function scannerRequestAllowed(profile, url2, method, request) {
799
- const approved = new URL(profile.url);
800
- if (url2.origin !== approved.origin)
801
- return false;
802
- if (profile.tool === "zap_scan")
803
- return true;
804
- if (profile.tool === "sqli_confirm" && profile.request) {
805
- if (url2.href !== profile.url || method !== profile.request.method || request?.body === undefined || request.headers["content-type"] !== profile.request.contentType)
806
- return false;
807
- const actual = bodyParameters(request.body, profile.request.contentType);
808
- const original = bodyParameters(profile.request.body, profile.request.contentType);
809
- const fixed = (entries) => entries.filter(([key]) => key !== profile.parameter);
810
- return Boolean(actual && original && actual.filter(([key]) => key === profile.parameter).length === 1 && digest(fixed(actual)) === digest(fixed(original)));
811
- }
812
- if (method !== "GET" && method !== "HEAD")
813
- return false;
814
- if (profile.tool === "nuclei_scan")
815
- return profile.template.paths.some((path) => new URL(path, approved).href === url2.href);
816
- if (url2.pathname !== approved.pathname || url2.searchParams.getAll(profile.parameter).length !== 1)
817
- return false;
818
- const fixed = (entry) => [...entry.searchParams].filter(([name]) => name !== profile.parameter);
819
- return JSON.stringify(fixed(url2)) === JSON.stringify(fixed(approved));
820
- }
821
- function sqlmapCandidate(finding, profile) {
822
- return finding.status === "candidate" && finding.verification.result === "pending" && finding.cwe === "CWE-89" && finding.subject.kind === "runtime" && ("endpoint" in finding && finding.endpoint ? finding.endpoint.url === profile.url && finding.endpoint.method === (profile.request?.method ?? "GET") && finding.endpoint.parameter === profile.parameter : !profile.request && finding.location === profile.url);
823
- }
824
-
825
- // apps/harness/src/storage/port-scan.ts
826
- var PORT_CONNECT_TIMEOUT_MS = 2000;
827
- var PORT_SCAN_TIMEOUT_MS = 120000;
828
- var PORT_SCAN_TASK_TIMEOUT_MS = 150000;
829
- var PortScanProfileSchema = strictObject({
830
- version: literal("2.6.1"),
831
- approvedBy: string().min(1).max(256),
832
- expiresAt: exports_iso.datetime(),
833
- host: string().min(1).max(253),
834
- address: PublicAddressSchema,
835
- ports: array(number().int().min(1).max(65535)).min(1).max(32).refine((ports) => new Set(ports).size === ports.length),
836
- connectionsPerSecond: number().int().min(1).max(5)
837
- });
838
- var PortRequestSchema = strictObject({
839
- address: PublicAddressSchema,
840
- port: number().int().min(1).max(65535)
841
- });
842
- var PortOutcomeSchema = _enum([
843
- "open",
844
- "closed",
845
- "timed_out",
846
- "unreachable",
847
- "cancelled"
848
- ]);
849
- function portDestination(profile, target, input) {
850
- const request = PortRequestSchema.parse(input);
851
- if (new URL(target).hostname !== profile.host || request.address !== profile.address || !profile.ports.includes(request.port))
852
- throw new HarnessError("scanner_denied", "TCP discovery requires the exact approved host, IPv4 address and port.");
853
- return request;
854
- }
855
-
856
- // apps/harness/src/sandbox/active-program.ts
857
- var ACTIVE_PROGRAM = String.raw`
858
- async function activeScan(tool, input, send) {
859
- const fs = require("node:fs/promises");
860
- const http = require("node:http");
861
- const tls = require("node:tls");
862
- const { spawn } = require("node:child_process");
863
- const root = await fs.mkdtemp("/tmp/mettle-active-");
864
- const profile = input.profile;
865
- const env = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, PYTHONDONTWRITEBYTECODE: "1", JAVA_TOOL_OPTIONS: "-Djava.awt.headless=true -Duser.home=" + root };
866
- const processes = new Set();
867
- const sockets = new Set();
868
- const tunnels = new WeakMap();
869
- let proxyFailure;
870
- const run = (command, args, timeout = ${ACTIVE_SCAN_TIMEOUT_MS}) => new Promise((resolve, reject) => {
871
- const child = spawn(command, args, { cwd: root, env, shell: false, stdio: ["ignore", "pipe", "pipe"], detached: true });
872
- processes.add(child);
873
- let bytes = 0;
874
- let stdout = "";
875
- let stderr = "";
876
- let error;
877
- const kill = () => { if (child.pid) { try { process.kill(-child.pid, "SIGKILL"); } catch {} } };
878
- const timer = setTimeout(() => { error = new Error("timed_out"); kill(); }, timeout);
879
- for (const [stream, field] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) stream.on("data", (chunk) => {
880
- bytes += chunk.length;
881
- if (field === "stdout") stdout += chunk.toString(); else stderr += chunk.toString();
882
- if (bytes > 1048576) { error = new Error("output_limit"); kill(); }
883
- });
884
- child.once("error", (cause) => { error = cause; });
885
- child.once("close", (code) => { clearTimeout(timer); processes.delete(child); if (error) reject(error); else resolve({ code, stdout, stderr }); });
886
- });
887
- const read = async (name) => {
888
- const file = root + "/" + name;
889
- const stat = await fs.lstat(file);
890
- if (!stat.isFile() || stat.size > 1048576) throw new Error("output_limit");
891
- return fs.readFile(file, "utf8");
892
- };
893
- const proxy = http.createServer({ maxHeaderSize: 16384 }, async (request, response) => {
894
- try {
895
- const origin = tunnels.get(request.socket);
896
- const url = origin ? new URL(request.url, origin).href : request.url;
897
- if (origin && new URL(url).origin !== origin) throw new Error();
898
- let size = 0;
899
- const chunks = [];
900
- for await (const chunk of request) { size += chunk.length; if (size > 8192) throw new Error(); chunks.push(chunk); }
901
- const headers = {};
902
- for (const [key, value] of Object.entries(request.headers)) {
903
- if (typeof value === "string" && !${JSON.stringify(SCANNER_REQUEST_STRIP_HEADERS)}.includes(key) && !key.startsWith("proxy-")) headers[key] = value;
904
- }
905
- const body = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
906
- const result = await send({ url, method: request.method, headers, ...(size ? { body } : {}) });
907
- response.writeHead(result.status, { ...result.headers, "content-type": result.contentType, connection: "close" });
908
- response.end(Buffer.from(result.body, "base64"));
909
- } catch (error) { proxyFailure = error; response.writeHead(502); response.end(); }
910
- });
911
- proxy.on("connection", (socket) => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); socket.setTimeout(15000, () => socket.destroy()); });
912
- proxy.on("upgrade", (_request, socket) => { proxyFailure = new Error("upgrade_denied"); socket.destroy(); });
913
- proxy.on("clientError", (error, socket) => { proxyFailure = error; socket.destroy(); });
914
- try {
915
- const certificate = await run("/usr/bin/openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", "-subj", "/CN=mettle-scanner-proxy", "-keyout", root + "/key.pem", "-out", root + "/cert.pem"], 10000);
916
- if (certificate.code !== 0) throw new Error("proxy_unavailable");
917
- const secureContext = tls.createSecureContext({ key: await read("key.pem"), cert: await read("cert.pem") });
918
- proxy.on("connect", (request, socket, head) => {
919
- try {
920
- const url = new URL("https://" + request.url);
921
- if (head.length || (request.url !== url.host && request.url !== url.hostname + ":443") || url.username || url.password || url.pathname !== "/") throw new Error();
922
- socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
923
- const secure = new tls.TLSSocket(socket, { isServer: true, secureContext });
924
- tunnels.set(secure, url.origin);
925
- secure.on("error", (error) => { proxyFailure = error; secure.destroy(); });
926
- proxy.emit("connection", secure);
927
- } catch (error) { proxyFailure = error; socket.destroy(); }
928
- });
929
- await new Promise((resolve, reject) => { proxy.once("error", reject); proxy.listen(0, "127.0.0.1", resolve); });
930
- const proxyUrl = "http://127.0.0.1:" + proxy.address().port;
931
- let findings = [];
932
- if (tool === "nuclei_scan") {
933
- const version = await run("/usr/local/bin/nuclei", ["-version"], 10000);
934
- if (version.code !== 0 || !(version.stdout + version.stderr).includes("v" + profile.version)) throw new Error("version_mismatch");
935
- const template = { id: profile.template.id, info: { name: profile.template.name, author: "mettle", severity: "info" }, http: [{ method: "GET", path: profile.template.paths.map((path) => new URL(path, profile.url).href), "matchers-condition": "and", matchers: [{ type: "status", status: profile.template.status }, { type: "word", part: "body", words: profile.template.words, condition: "and" }] }] };
936
- await fs.writeFile(root + "/template.yaml", JSON.stringify(template));
937
- await fs.writeFile(root + "/config.yaml", "{}\n");
938
- const scanned = await run("/usr/local/bin/nuclei", ["-u", profile.url, "-t", root + "/template.yaml", "-config", root + "/config.yaml", "-o", root + "/nuclei.jsonl", "-jsonl", "-silent", "-omit-raw", "-omit-template", "-no-stdin", "-no-interactsh", "-disable-update-check", "-disable-redirects", "-no-httpx", "-proxy", proxyUrl, "-proxy-internal", "-rate-limit", String(profile.requestsPerSecond), "-c", "1", "-bs", "1", "-retries", "0", "-timeout", "5"]);
939
- if (scanned.code !== 0 || /\[(?:ERR|FTL)\]/.test(scanned.stderr)) throw new Error("scan_failed");
940
- findings = (await read("nuclei.jsonl")).split("\n").filter(Boolean).map((line) => {
941
- const item = JSON.parse(line);
942
- if (item["template-id"] !== profile.template.id || typeof item["matched-at"] !== "string") throw new Error();
943
- return { ruleId: item["template-id"], url: item["matched-at"], title: profile.template.name };
944
- });
945
- } else if (tool === "sqli_confirm") {
946
- const version = await run("/usr/bin/python3", ["/opt/mettle/sqlmap/sqlmap.py", "--version"], 10000);
947
- if (version.code !== 0 || version.stdout.split("\n")[0].trim() !== profile.version + "#stable") throw new Error("version_mismatch");
948
- await fs.writeFile(root + "/targets.txt", profile.url + "\n");
949
- const scanned = await run("/usr/bin/python3", ["/opt/mettle/sqlmap/sqlmap.py", "-m", root + "/targets.txt", "-p", profile.parameter, ...(profile.request ? ["--method=" + profile.request.method, "--data=" + profile.request.body, "--headers=Content-Type: " + profile.request.contentType] : []), "--technique=" + profile.technique, "--batch", "--skip-waf", "--level=1", "--risk=1", "--threads=1", "--timeout=5", "--retries=0", "--delay=" + String(1 / profile.requestsPerSecond), "--proxy=" + proxyUrl, "--ignore-redirects", "--disable-coloring", "--flush-session", "--output-dir=" + root + "/sqlmap", "--results-file=" + root + "/sqlmap.csv", "-v", "0"]);
950
- const errors = (scanned.stdout + scanned.stderr).split("\n").filter((line) => /\[(?:CRITICAL|ERROR)\]/.test(line));
951
- if (scanned.code !== 0 || errors.some((line) => !/\[(?:CRITICAL|ERROR)\] all tested parameters do not appear to be injectable\./.test(line))) throw new Error("scan_failed");
952
- const parsed = await run("/usr/bin/python3", ["-c", "import csv,json,sys; print(json.dumps(list(csv.DictReader(open(sys.argv[1],newline='')))))", root + "/sqlmap.csv"], 10000);
953
- if (parsed.code !== 0) throw new Error("scan_failed");
954
- findings = JSON.parse(parsed.stdout).flatMap((item) => {
955
- if (item['Target URL'] !== profile.url) throw new Error();
956
- if (!item['Technique(s)'] || item['Note(s)']) return [];
957
- const json = profile.request?.contentType === "application/json";
958
- if (item.Place !== (profile.request ? json ? "(custom) POST" : "POST" : "GET") || item.Parameter !== (json ? "JSON " : "") + profile.parameter) throw new Error();
959
- return [{ ruleId: "sqlmap-" + profile.technique, url: profile.url, title: "SQL injection observation", parameter: profile.parameter }];
960
- });
961
- } else if (tool === "zap_scan") {
962
- const version = await run("/usr/bin/python3", ["-c", "import sys; from zipfile import ZipFile; archive = ZipFile('/zap/zap-' + sys.argv[1] + '.jar'); assert archive.getinfo('META-INF/MANIFEST.MF').file_size <= 16384; print(archive.read('META-INF/MANIFEST.MF').decode('utf-8'))", profile.version], 10000);
963
- if (version.code !== 0 || !version.stdout.split(/\r?\n/).includes("Implementation-Version: " + profile.version)) throw new Error("version_mismatch");
964
- const plan = { env: { contexts: [{ name: "approved", urls: [new URL(profile.url).origin + new URL(profile.url).pathname] }], parameters: { failOnError: true, failOnWarning: true, progressToStdout: false } }, jobs: [
965
- { type: "passiveScan-config", parameters: { disableAllRules: true } },
966
- { type: "activeScan-config", inputVectors: { scripts: false } },
967
- { type: "requestor", parameters: {}, requests: [{ url: profile.url, method: "GET" }] },
968
- { type: "activeScan", parameters: { context: "approved", maxRuleDurationInMins: 0, maxScanDurationInMins: 0, threadPerHost: 1, delayInMs: Math.ceil(1000 / profile.requestsPerSecond) }, policyDefinition: { defaultStrength: "Low", defaultThreshold: "Off", rules: profile.rules.map((id) => ({ id, strength: "Low", threshold: "Medium" })) } },
969
- { type: "report", parameters: { template: "traditional-json", reportDir: root, reportFile: "zap.json" } }
970
- ] };
971
- await fs.writeFile(root + "/plan.yaml", JSON.stringify(plan));
972
- const scanned = await run("/zap/zap.sh", ["-Xmx768m", "-silent", "-cmd", "-dir", root + "/zap", "-autorun", root + "/plan.yaml", "-config", "network.connection.httpProxy.enabled=true", "-config", "network.connection.httpProxy.host=127.0.0.1", "-config", "network.connection.httpProxy.port=" + proxy.address().port, "-config", "network.connection.timeoutInSecs=5", "-config", "autoupdate.checkOnStart=false", "-config", "autoupdate.downloadNewRelease=false", "-config", "autoupdate.installAddonUpdates=false"]);
973
- if (scanned.code !== 0) throw new Error("scan_failed");
974
- const report = JSON.parse(await read("zap.json"));
975
- if (report['@version'] !== profile.version || !Array.isArray(report.site)) throw new Error();
976
- findings = report.site.flatMap((site) => site.alerts.flatMap((alert) => {
977
- if (!profile.rules.includes(Number(alert.pluginid))) throw new Error();
978
- return alert.instances.map((item) => ({ ruleId: String(alert.pluginid), url: item.uri, title: String(alert.name).slice(0, 256) }));
979
- }));
980
- } else throw new Error();
981
- if (proxyFailure) throw proxyFailure;
982
- if (findings.some((finding) => typeof finding.url !== "string" || finding.url.length > 1024)) throw new Error();
983
- return { status: "completed", version: profile.version, findingCount: findings.length, findings: findings.slice(0, 10), complete: findings.length <= 10 };
984
- } catch {
985
- return { status: "failed", version: profile.version, findingCount: 0, findings: [], complete: false };
986
- } finally {
987
- for (const child of processes) { try { process.kill(-child.pid, "SIGKILL"); } catch {} }
988
- for (const socket of sockets) socket.destroy();
989
- await new Promise((resolve) => proxy.close(resolve));
990
- await fs.rm(root, { recursive: true, force: true });
991
- }
992
- }
993
- `;
994
-
995
- // apps/harness/src/sandbox/browser-page-program.ts
996
- var daemon = String.raw`
997
- const crypto = require("node:crypto"), net = require("node:net"), readline = require("node:readline");
998
- const retained = {};
999
- let busy = false;
1000
- const server = net.createServer(async (socket) => {
1001
- if (busy) { socket.destroy(); return; }
1002
- busy = true;
1003
- const lines = readline.createInterface({ input: socket, crlfDelay: Infinity });
1004
- const iterator = lines[Symbol.asyncIterator]();
1005
- let disconnected = false;
1006
- socket.on("close", () => { disconnected = true; });
1007
- socket.on("error", () => {});
1008
- const next = async () => {
1009
- const line = await iterator.next();
1010
- if (line.done || Buffer.byteLength(line.value) > ${BROKER_RESPONSE_LIMIT}) throw new Error("browser_page_protocol");
1011
- return JSON.parse(line.value);
1012
- };
1013
- let queue = Promise.resolve();
1014
- const send = (request) => {
1015
- const result = queue.then(async () => {
1016
- if (disconnected) throw new Error("browser_page_disconnected");
1017
- socket.write(JSON.stringify({ request }) + "\n");
1018
- const response = await next();
1019
- if (!Object.hasOwn(response, "response")) throw new Error("browser_page_refused");
1020
- return response.response;
1021
- });
1022
- queue = result;
1023
- return result;
1024
- };
1025
- try {
1026
- const { input } = await next();
1027
- if ((input.page.operation === "open") === Boolean(retained.page)) throw new Error("browser_page_lost");
1028
- const value = await websiteBrowse(input, send, retained);
1029
- await queue;
1030
- socket.end(JSON.stringify({ value }) + "\n");
1031
- if (input.page.operation === "close") server.close();
1032
- } catch {
1033
- if (retained.browser) await retained.browser.close().catch(() => {});
1034
- socket.end(JSON.stringify({ error: "browser_page_failed" }) + "\n");
1035
- server.close();
1036
- } finally { lines.close(); busy = false; }
1037
- });
1038
- server.listen("/tmp/mettle-browser-page.sock", () => { process.send({ ready: true }); process.disconnect(); });
1039
- server.on("error", () => process.exit(1));
1040
- `;
1041
- var BROWSER_PAGE_PROGRAM = String.raw`
1042
- async function browseRetainedPage(input, send) {
1043
- if (input.page.operation === "open") {
1044
- const definitions = [websiteScope, restoreBrowserPrivateState, captureBrowserPrivateState, websiteBrowse].map((fn) => fn.toString()).join("\n");
1045
- const child = require("node:child_process").spawn(process.execPath, ["-e", definitions + "\n" + ${JSON.stringify(daemon)}], { detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"] });
1046
- await new Promise((resolve, reject) => {
1047
- const timer = setTimeout(() => { child.kill(); reject(new Error("browser_page_start")); }, 10000);
1048
- child.once("error", (error) => { clearTimeout(timer); reject(error); });
1049
- child.once("exit", () => { clearTimeout(timer); reject(new Error("browser_page_start")); });
1050
- child.once("message", (message) => { clearTimeout(timer); message.ready === true ? resolve() : reject(new Error("browser_page_start")); });
1051
- });
1052
- child.unref();
1053
- }
1054
- const socket = require("node:net").connect("/tmp/mettle-browser-page.sock");
1055
- await new Promise((resolve, reject) => { socket.once("connect", resolve); socket.once("error", reject); });
1056
- const lines = readline.createInterface({ input: socket, crlfDelay: Infinity });
1057
- const encoded = JSON.stringify({ input });
1058
- if (Buffer.byteLength(encoded) > ${TASK_INPUT_LIMIT}) throw new Error("browser_page_input");
1059
- socket.write(encoded + "\n");
1060
- try {
1061
- for await (const line of lines) {
1062
- if (Buffer.byteLength(line) > ${BROKER_RESPONSE_LIMIT}) throw new Error("browser_page_output");
1063
- const message = JSON.parse(line);
1064
- if (Object.keys(message).length !== 1) throw new Error("browser_page_protocol");
1065
- if (Object.hasOwn(message, "request")) socket.write(JSON.stringify({ response: await send(message.request) }) + "\n");
1066
- else if (Object.hasOwn(message, "value")) return message.value;
1067
- else throw new Error("browser_page_failed");
1068
- }
1069
- throw new Error("browser_page_lost");
1070
- } finally { lines.close(); socket.destroy(); }
1071
- }
1072
- `;
1073
-
1074
- // apps/harness/src/sandbox/port-program.ts
1075
- var PORT_PROGRAM = String.raw`
1076
- async function portScan(input, send) {
1077
- const fs = require("node:fs/promises");
1078
- const net = require("node:net");
1079
- const { spawn } = require("node:child_process");
1080
- const root = await fs.mkdtemp("/tmp/mettle-ports-");
1081
- const profile = input.profile;
1082
- const sockets = new Set();
1083
- const children = new Set();
1084
- const jobs = new Set();
1085
- let failure;
1086
- const run = (args, timeout) => new Promise((resolve, reject) => {
1087
- const child = spawn("/usr/local/bin/naabu", args, { cwd: root, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root }, shell: false, stdio: ["ignore", "pipe", "pipe"] });
1088
- children.add(child);
1089
- let stdout = "", stderr = "", size = 0, error;
1090
- const timer = setTimeout(() => { error = new Error("timed_out"); child.kill("SIGKILL"); }, timeout);
1091
- for (const [stream, field] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) stream.on("data", (chunk) => {
1092
- size += chunk.length;
1093
- if (size > 65536) { error = new Error("output_limit"); child.kill("SIGKILL"); return; }
1094
- if (field === "stdout") stdout += chunk.toString(); else stderr += chunk.toString();
1095
- });
1096
- child.once("error", (cause) => { error = cause; });
1097
- child.once("close", (code) => { clearTimeout(timer); children.delete(child); if (error) reject(error); else resolve({ code, stdout, stderr }); });
1098
- });
1099
- const proxy = net.createServer((socket) => {
1100
- let bytes = Buffer.alloc(0), phase = "greeting";
1101
- sockets.add(socket);
1102
- socket.on("close", () => sockets.delete(socket));
1103
- socket.on("error", (error) => { if (phase !== "done" || error.code !== "ECONNRESET") failure ??= error; });
1104
- socket.setTimeout(10000, () => { failure ??= new Error("proxy_timeout"); socket.destroy(); });
1105
- socket.on("data", (chunk) => {
1106
- try {
1107
- if (phase === "connecting" || phase === "done") throw new Error("payload_denied");
1108
- bytes = Buffer.concat([bytes, chunk]);
1109
- if (bytes.length > 512) throw new Error("proxy_input_limit");
1110
- if (phase === "greeting") {
1111
- if (bytes.length < 2) return;
1112
- if (bytes[0] !== 5 || bytes[1] !== 1) throw new Error("auth_denied");
1113
- if (bytes.length < 3) return;
1114
- if (bytes[2] !== 0) throw new Error("auth_denied");
1115
- bytes = bytes.subarray(3);
1116
- phase = "request";
1117
- socket.write(Buffer.from([5, 0]));
1118
- }
1119
- if (phase !== "request" || bytes.length < 4) return;
1120
- if (bytes[0] !== 5 || bytes[1] !== 1 || bytes[2] !== 0 || bytes[3] !== 1) throw new Error("connect_only_ipv4");
1121
- if (bytes.length < 10) return;
1122
- if (bytes.length !== 10) throw new Error("payload_denied");
1123
- const address = [...bytes.subarray(4, 8)].join(".");
1124
- const port = bytes.readUInt16BE(8);
1125
- if (address !== profile.address || !profile.ports.includes(port)) throw new Error("scope_denied");
1126
- phase = "connecting";
1127
- const job = send({ address, port }).then((response) => {
1128
- phase = "done";
1129
- const code = response.outcome === "open" ? 0 : response.outcome === "closed" ? 5 : 4;
1130
- socket.end(Buffer.from([5, code, 0, 1, 0, 0, 0, 0, 0, 0]));
1131
- }).catch((error) => { failure ??= error; socket.destroy(); });
1132
- jobs.add(job);
1133
- void job.finally(() => jobs.delete(job));
1134
- } catch (error) { failure ??= error; socket.destroy(); }
1135
- });
1136
- });
1137
- try {
1138
- const version = await run(["-version"], 10000);
1139
- if (version.code !== 0 || !(version.stdout + version.stderr).split(/\s+/).includes(profile.version)) throw new Error("version_mismatch");
1140
- await fs.writeFile(root + "/config.yaml", "{}\n", { flag: "wx", mode: 0o600 });
1141
- await new Promise((resolve, reject) => { proxy.once("error", reject); proxy.listen(0, "127.0.0.1", resolve); });
1142
- const result = await run(["-host", profile.address, "-p", profile.ports.join(","), "-s", "c", "-iv", "4", "-Pn", "-proxy", "127.0.0.1:" + proxy.address().port, "-config", root + "/config.yaml", "-no-stdin", "-disable-update-check", "-c", "1", "-rate", String(profile.connectionsPerSecond), "-retries", "1", "-timeout", "5000", "-warm-up-time", "0", "-silent", "-json"], ${PORT_SCAN_TIMEOUT_MS});
1143
- await Promise.all(jobs);
1144
- if (failure) throw failure;
1145
- if (result.code !== 0 || /\[(?:ERR|FTL)\]/.test(result.stderr)) throw new Error("scan_failed");
1146
- const openPorts = result.stdout.split("\n").filter(Boolean).map((line) => {
1147
- const entry = JSON.parse(line);
1148
- if (entry.ip !== profile.address || entry.protocol !== "tcp" || !profile.ports.includes(entry.port)) throw new Error("scan_output");
1149
- return entry.port;
1150
- });
1151
- return { status: "completed", version: profile.version, openPorts: [...new Set(openPorts)].sort((a, b) => a - b) };
1152
- } finally {
1153
- for (const child of children) child.kill("SIGKILL");
1154
- for (const socket of sockets) socket.destroy();
1155
- await new Promise((resolve) => proxy.close(resolve));
1156
- await fs.rm(root, { recursive: true, force: true });
1157
- }
1158
- }
1159
- `;
1160
-
1161
- // apps/harness/src/sandbox/browser-state-program.ts
1162
- var BROWSER_STATE_PROGRAM = `
1163
- async function restoreBrowserPrivateState(page, context, state, url) {
1164
- const origin = new URL(url).origin;
1165
- const saved = state.origins.find((entry) => entry.origin === origin);
1166
- if (!saved || (!saved.sessionStorage?.length && !saved.opfs?.length)) return;
1167
- const bootstrap = new URL(url);
1168
- bootstrap.searchParams.set("__mettle_restore", crypto.randomBytes(16).toString("hex"));
1169
- const route = async (request) => request.fulfill({ status: 200, contentType: "text/html", body: "<!doctype html><title>Restore private state</title>" });
1170
- await page.route(bootstrap.href, route);
1171
- try {
1172
- await page.goto(bootstrap.href, { waitUntil: "domcontentloaded" });
1173
- await page.evaluate(async (saved) => {
1174
- sessionStorage.clear();
1175
- for (const { name, value } of saved.sessionStorage || []) sessionStorage.setItem(name, value);
1176
- if (!saved.opfs?.length) return;
1177
- const root = await navigator.storage.getDirectory();
1178
- for (const entry of saved.opfs) {
1179
- const parts = entry.path.split("/"), name = parts.pop();
1180
- let parent = root;
1181
- for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true });
1182
- if (entry.kind === "directory") await parent.getDirectoryHandle(name, { create: true });
1183
- else {
1184
- const handle = await parent.getFileHandle(name, { create: true });
1185
- const writer = await handle.createWritable();
1186
- await writer.write(Uint8Array.from(atob(entry.base64), (char) => char.charCodeAt(0)));
1187
- await writer.close();
1188
- }
1189
- }
1190
- }, saved);
1191
- } finally { await page.unroute(bootstrap.href, route); }
1192
- }
1193
-
1194
- async function captureBrowserPrivateState(page, context, target) {
1195
- if (new URL(page.url()).origin !== new URL(target).origin) throw new Error("browser_state_origin");
1196
- const extras = await page.evaluate(async () => {
1197
- const session = Object.entries(sessionStorage).map(([name, value]) => ({ name, value }));
1198
- if (session.length > 64 || session.some((entry) => entry.name.length > 256 || entry.value.length > 16384)) throw new Error("browser_state_limit");
1199
- const opfs = [];
1200
- let bytes = 0;
1201
- const visit = async (directory, prefix, depth) => {
1202
- if (depth > 8) throw new Error("browser_state_limit");
1203
- for await (const [name, handle] of directory.entries()) {
1204
- if (opfs.length >= 64) throw new Error("browser_state_limit");
1205
- const path = prefix + name;
1206
- if (handle.kind === "directory") {
1207
- opfs.push({ kind: "directory", path });
1208
- await visit(handle, path + "/", depth + 1);
1209
- } else {
1210
- const file = await handle.getFile();
1211
- bytes += file.size;
1212
- if (file.size > 65536 || bytes > 131072) throw new Error("browser_state_limit");
1213
- const buffer = new Uint8Array(await file.arrayBuffer());
1214
- let binary = "";
1215
- for (const byte of buffer) binary += String.fromCharCode(byte);
1216
- opfs.push({ kind: "file", path, base64: btoa(binary) });
1217
- }
1218
- }
1219
- };
1220
- if (navigator.storage?.getDirectory) await visit(await navigator.storage.getDirectory(), "", 1);
1221
- return { origin: location.origin, sessionStorage: session, opfs };
1222
- });
1223
- const state = await context.storageState({ indexedDB: true });
1224
- let entry = state.origins.find((entry) => entry.origin === extras.origin);
1225
- if (!entry) { entry = { origin: extras.origin, localStorage: [] }; state.origins.push(entry); }
1226
- Object.assign(entry, extras);
1227
- return state;
1228
- }
1229
- `;
1230
-
1231
- // apps/harness/src/sandbox/javascript-program.ts
1232
- var JAVASCRIPT_PROGRAM = String.raw`
1233
- const node = (value) => value && typeof value === "object" && ("type" in value) && typeof value.type === "string" ? value : undefined;
1234
- function literal(value, depth = 0) {
1235
- const entry = node(value);
1236
- if (!entry || depth > 8)
1237
- return;
1238
- if (entry.type === "StringLiteral")
1239
- return typeof entry.value === "string" ? entry.value : undefined;
1240
- if (entry.type === "TemplateLiteral" && Array.isArray(entry.expressions) && !entry.expressions.length) {
1241
- const quasis = entry.quasis;
1242
- return quasis[0]?.value.cooked ?? undefined;
1243
- }
1244
- if (entry.type === "BinaryExpression" && entry.operator === "+") {
1245
- const left = literal(entry.left, depth + 1), right = literal(entry.right, depth + 1);
1246
- return left !== undefined && right !== undefined && left.length + right.length <= 2048 ? left + right : undefined;
1247
- }
1248
- return;
1249
- }
1250
- const property = (value) => {
1251
- const entry = node(value);
1252
- return entry?.type === "Identifier" ? String(entry.name) : literal(entry);
1253
- };
1254
- function javascriptDiscovery(source, base, contentType) {
1255
- const scripts = /text\/html/i.test(contentType) ? [...source.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi)].slice(0, 32).map((entry) => entry[1]) : /(?:javascript|ecmascript)/i.test(contentType) ? [source] : [];
1256
- if (!scripts.length)
1257
- return { leads: [], omitted: 0 };
1258
- const { parse } = require("/opt/mettle-parser/lib/index.js");
1259
- const leads = [];
1260
- let omitted = 0;
1261
- for (const script of scripts) {
1262
- if (Buffer.byteLength(script) > 262144) {
1263
- omitted++;
1264
- continue;
1265
- }
1266
- let ast;
1267
- try {
1268
- ast = parse(script, {
1269
- sourceType: "unambiguous",
1270
- errorRecovery: false,
1271
- allowReturnOutsideFunction: true
1272
- });
1273
- } catch {
1274
- omitted++;
1275
- continue;
1276
- }
1277
- const pending = [ast];
1278
- let visited = 0;
1279
- while (pending.length && visited++ < 40000) {
1280
- const current = node(pending.pop());
1281
- if (!current)
1282
- continue;
1283
- for (const [key, value] of Object.entries(current)) {
1284
- if (["loc", "start", "end", "comments", "tokens", "extra"].includes(key))
1285
- continue;
1286
- if (Array.isArray(value))
1287
- pending.push(...value);
1288
- else if (node(value))
1289
- pending.push(value);
1290
- }
1291
- if (current.type !== "CallExpression")
1292
- continue;
1293
- const callee = node(current.callee);
1294
- const name = callee?.type === "Identifier" ? String(callee.name) : callee?.type === "MemberExpression" ? property(callee.property) : undefined;
1295
- const receiver = callee?.type === "MemberExpression" ? property(callee.object) : undefined;
1296
- const args = Array.isArray(current.arguments) ? current.arguments : [];
1297
- let method, address;
1298
- if (name === "fetch" && (!receiver || ["window", "globalThis"].includes(receiver))) {
1299
- address = literal(args[0]);
1300
- method = "GET";
1301
- if (args[1] !== undefined) {
1302
- const options = node(args[1]);
1303
- if (options?.type !== "ObjectExpression" || !Array.isArray(options.properties)) {
1304
- omitted++;
1305
- continue;
1306
- }
1307
- for (const value of options.properties) {
1308
- const option = node(value);
1309
- if (option?.type !== "ObjectProperty" || option.computed) {
1310
- method = undefined;
1311
- break;
1312
- }
1313
- if (property(option.key) === "method")
1314
- method = literal(option.value)?.toUpperCase();
1315
- }
1316
- }
1317
- } else if (name === "open" && receiver) {
1318
- method = literal(args[0])?.toUpperCase();
1319
- address = literal(args[1]);
1320
- } else if (receiver === "axios" && name && ["get", "head", "options", "post", "put", "patch", "delete"].includes(name)) {
1321
- method = name.toUpperCase();
1322
- address = literal(args[0]);
1323
- } else
1324
- continue;
1325
- if (!address || address.length > 2048 || !method || !["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
1326
- omitted++;
1327
- continue;
1328
- }
1329
- try {
1330
- const url = new URL(address, base);
1331
- url.hash = "";
1332
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.href.length > 2048 || leads.length >= 200) {
1333
- omitted++;
1334
- continue;
1335
- }
1336
- leads.push({
1337
- url: url.href,
1338
- method,
1339
- kind: "javascript",
1340
- inputs: [],
1341
- contentType: null,
1342
- operation: null,
1343
- authentication: "unknown",
1344
- stateChanging: !["GET", "HEAD", "OPTIONS"].includes(method),
1345
- limitations: [
1346
- "Static syntax-tree request lead; execution, body values, authentication and dynamic bindings remain unproven."
1347
- ]
1348
- });
1349
- } catch {
1350
- omitted++;
1351
- }
1352
- }
1353
- if (pending.length)
1354
- omitted++;
1355
- }
1356
- return { leads, omitted };
1357
- }
1358
- `;
1359
-
1360
- // apps/harness/src/sandbox/website-program.ts
1361
- var WEBSITE_PROGRAM = String.raw`
1362
- ${JAVASCRIPT_PROGRAM}
1363
- ${BROWSER_STATE_PROGRAM}
1364
- function websiteScope(input, raw, method = "GET") {
1365
- try {
1366
- const url = new URL(raw);
1367
- return (input.scopes || [{ url: input.target, methods: ["GET", "HEAD"] }]).some((entry) => {
1368
- const scope = new URL(entry.url);
1369
- return entry.methods.includes(method) && url.origin === scope.origin && !url.username && !url.password && !url.hash &&
1370
- !/%(?:2f|5c|00|25)/i.test(url.pathname) &&
1371
- (url.pathname === scope.pathname || url.pathname.startsWith(scope.pathname.endsWith("/") ? scope.pathname : scope.pathname + "/"));
1372
- });
1373
- } catch { return false; }
1374
- }
1375
- function websiteAttribute(attributes, key) {
1376
- const match = new RegExp('(?:^|\\s)' + key + '\\s*=\\s*(?:"([^"<>]*)"|\'([^\'<>]*)\'|([^\\s<>]+))', 'i').exec(attributes);
1377
- return match ? (match[1] ?? match[2] ?? match[3]).replace(/&amp;/g, "&") : undefined;
1378
- }
1379
- function websiteLeads(body, base, contentType) {
1380
- const leads = [];
1381
- let omitted = 0;
1382
- const add = (raw, method, kind, fields = [], media = null, limitations = []) => {
1383
- try {
1384
- const url = new URL(raw, base); url.hash = "";
1385
- if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.href.length > 2048 || leads.length >= 200) { omitted++; return; }
1386
- leads.push({ url: url.href, method, kind, inputs: fields, contentType: media, operation: null, authentication: "unknown", stateChanging: !["GET", "HEAD", "OPTIONS"].includes(method), limitations });
1387
- } catch { omitted++; }
1388
- };
1389
- if (/text\/html/i.test(contentType)) {
1390
- for (const match of body.matchAll(/<(a|script)\b([^>]{0,8192})>/gi)) {
1391
- const tag = match[1].toLowerCase();
1392
- const value = websiteAttribute(match[2], tag === "script" ? "src" : "href");
1393
- if (value !== undefined) add(value, "GET", tag === "script" ? "script" : "page");
1394
- }
1395
- for (const match of body.matchAll(/<form\b([^>]{0,8192})>([\s\S]{0,32768}?)(?:<\/form\s*>|$)/gi)) {
1396
- const method = (websiteAttribute(match[1], "method") || "GET").toUpperCase();
1397
- if (!["GET", "POST"].includes(method)) { omitted++; continue; }
1398
- const fields = [], names = new Set();
1399
- let missing = 0;
1400
- for (const control of match[2].matchAll(/<(?:input|select|textarea|button)\b([^>]{0,8192})>/gi)) {
1401
- const key = websiteAttribute(control[1], "name");
1402
- if (!key || /(?:^|\s)disabled(?:\s|=|$)/i.test(control[1]) || names.has(key)) continue;
1403
- names.add(key);
1404
- if (fields.length >= 64 || key.length > 256) { missing++; continue; }
1405
- fields.push({ location: method === "GET" ? "query" : "body", name: key, required: /(?:^|\s)required(?:\s|=|$)/i.test(control[1]), type: "string", identifier: false });
1406
- }
1407
- const media = method === "POST" ? (websiteAttribute(match[1], "enctype") || "application/x-www-form-urlencoded").toLowerCase() : null;
1408
- add(websiteAttribute(match[1], "action") || base, method, "form", fields, media, ["Form controls are leads; values, login state and workflow prerequisites are not synthesized.", ...(missing ? [missing + " form input names omitted by bounds."] : [])]);
1409
- omitted += missing;
1410
- }
1411
- }
1412
- const priority = { page: 0, javascript: 1, script: 2, form: 3 };
1413
- return { leads: leads.sort((a, b) => priority[a.kind] - priority[b.kind]), omitted };
1414
- }
1415
- function websiteRequestKey(request) { return request.method + " " + request.url; }
1416
- function websiteLeadKey(lead) { return websiteRequestKey(lead) + " " + (lead.contentType || "") + " " + JSON.stringify((lead.inputs || []).map((input) => input.location + ":" + input.name).sort()); }
1417
- async function websiteObserve(tool, input, send) {
1418
- const initial = { url: input.url || input.target, method: input.method || "GET", ...(input.body !== undefined ? { body: input.body } : {}), ...(input.bodyBase64 !== undefined ? { bodyBase64: input.bodyBase64 } : {}), ...(input.headers ? { headers: input.headers } : {}) };
1419
- const queue = [{ request: initial, depth: 0 }];
1420
- const seen = new Set(), pages = [], endpoints = new Map(), leads = new Map();
1421
- let omitted = 0, redirects = 0;
1422
- while (queue.length && pages.length < input.maxPages && seen.size < 100) {
1423
- const next = queue.shift(), key = websiteRequestKey(next.request);
1424
- if (seen.has(key)) continue;
1425
- seen.add(key);
1426
- const response = await send(next.request);
1427
- const textual = /^(text\/|application\/(.*json|.*xml|javascript|x-javascript))/i.test(response.contentType);
1428
- const body = textual ? Buffer.from(response.body, "base64").toString("utf8") : "";
1429
- if ([301,302,303,307,308].includes(response.status)) {
1430
- const location = response.headers && response.headers.location;
1431
- if (typeof location === "string" && redirects++ < 5 && ["GET", "HEAD"].includes(next.request.method)) {
1432
- const redirect = new URL(location, next.request.url); redirect.hash = "";
1433
- const lead = { url: redirect.href, method: next.request.method, kind: "redirect", inScope: websiteScope(input, redirect.href, next.request.method) };
1434
- if (leads.size < 40) leads.set(websiteLeadKey(lead), lead); else omitted++;
1435
- if (lead.inScope) queue.unshift({ request: { url: redirect.href, method: next.request.method }, depth: next.depth });
1436
- else omitted++;
1437
- } else omitted++;
1438
- }
1439
- const url = new URL(next.request.url);
1440
- const parameters = [...new Set(url.searchParams.keys())];
1441
- omitted += Math.max(0, parameters.length - 16);
1442
- if (endpoints.size < 40 || endpoints.has(key)) endpoints.set(key, { id: crypto.createHash("sha256").update(key).digest("hex"), url: url.href, method: next.request.method, parameters: parameters.slice(0,16), observed: true }); else omitted++;
1443
- pages.push({ url: url.href, status: response.status, contentType: response.contentType, excerpt: textual ? (response.excerpt ?? body).slice(0, tool === "web_request" ? 2048 : 512) : "[Binary response; body not retained as text]", truncated: Boolean(response.bodyTruncated) || textual && body.length > (tool === "web_request" ? 2048 : 512), ...(response.bodyTruncated ? { bodyTruncated: true } : {}), ...(response.evidenceRef ? { evidenceRef: response.evidenceRef } : {}) });
1444
- if (response.bodyTruncated) omitted++;
1445
- const discovered = websiteLeads(body, url.href, response.contentType);
1446
- const scripts = javascriptDiscovery(body, url.href, response.contentType);
1447
- discovered.leads.push(...scripts.leads);
1448
- omitted += discovered.omitted + scripts.omitted;
1449
- for (const link of discovered.leads) {
1450
- const inScope = websiteScope(input, link.url, link.method), linkKey = websiteRequestKey(link), leadKey = websiteLeadKey(link);
1451
- if (leads.size < 40 || leads.has(leadKey)) leads.set(leadKey, { ...link, inScope }); else omitted++;
1452
- if (!inScope) { omitted++; continue; }
1453
- if (endpoints.size < 40 && !endpoints.has(linkKey)) endpoints.set(linkKey, { id: crypto.createHash("sha256").update(linkKey).digest("hex"), url: link.url, method: link.method, parameters: [], observed: false });
1454
- if (tool !== "web_discover" || !["page", "script", "javascript"].includes(link.kind) || !["GET", "HEAD"].includes(link.method)) continue;
1455
- if (/\.(?:css|png|jpe?g|gif|svg|ico|avif|webp|woff2?|ttf|mp4|pdf)(?:\?|$)/i.test(link.url)) continue;
1456
- if (next.depth < input.maxDepth && queue.length < 40 && !seen.has(linkKey)) queue.push({ request: { url: link.url, method: link.method, ...(link.kind === "script" ? { resourceType: "script" } : {}) }, depth: next.depth + 1 });
1457
- else if (!seen.has(linkKey)) omitted++;
1458
- }
1459
- }
1460
- const value = { pages, endpoints: [...endpoints.values()], leads: [...leads.values()], omitted: omitted + queue.length,
1461
- coverage: "Bounded HTTP observations, page/form inputs and literal JavaScript/XHR hints. Hints remain leads until concrete responses are retained. External origins do not grant access; dynamic routes, omitted inputs and unexercised business workflows remain explicit gaps.", complete: false, verification: "not-independent" };
1462
- while (Buffer.byteLength(JSON.stringify(value)) > 16000) {
1463
- if (value.leads.length) value.leads.pop(); else if (value.endpoints.length) value.endpoints.pop(); else value.pages.pop();
1464
- value.omitted++;
1465
- }
1466
- return value;
1467
- }
1468
-
1469
- async function websiteBrowse(input, send, retained) {
1470
- const version = require("/opt/mettle-browser/node_modules/playwright/package.json").version;
1471
- if (version !== input.version) throw new Error("browser_version");
1472
- const { chromium, errors } = require("/opt/mettle-browser/node_modules/playwright");
1473
- const browser = retained?.browser || await chromium.launch({ headless: true, chromiumSandbox: false, timeout: 10000, args: ["--disable-dev-shm-usage"] });
1474
- const blocked = [];
1475
- const websocketObserver = retained || {};
1476
- let context, page, routeHandler, consoleHandler, dialogHandler, pageHandler, keep = false;
1477
- let failure, requests = 0, pending = Promise.resolve(), accepting = true, blockedOmitted = 0;
1478
- websocketObserver.blockedWebSocket = (url) => {
1479
- if (blocked.length < 20) blocked.push({ url: url.slice(0, 2048), reason: "websocket" }); else blockedOmitted++;
1480
- };
1481
- try {
1482
- const restored = input.persistSession ? await send({ sessionState: "load" }) : undefined;
1483
- if (retained?.context && (retained.startUrl !== input.url || retained.credentialRevision !== restored?.credentialRevision || retained.stateDigest !== restored?.stateDigest)) throw new Error("browser_page_binding");
1484
- context = retained?.context || await browser.newContext({ serviceWorkers: "block", acceptDownloads: false, ...(restored ? { storageState: { cookies: restored.state.cookies, origins: restored.state.origins.map(({ sessionStorage, opfs, ...origin }) => origin) } } : {}) });
1485
- const executionEvents = [];
1486
- let executionEventsOmitted = 0;
1487
- const executionEvent = (channel, kind, text, url) => {
1488
- if (executionEvents.length >= 32) { executionEventsOmitted++; return; }
1489
- executionEvents.push({ channel, kind: kind.slice(0,32), text: text.slice(0,256), url: url.slice(0,256), truncated: text.length > 256 || url.length > 256 });
1490
- };
1491
- consoleHandler = (message) => executionEvent("console", message.type(), message.text(), message.page()?.url() || "");
1492
- context.on("console", consoleHandler);
1493
- dialogHandler = (dialog) => {
1494
- executionEvent("dialog", dialog.type(), dialog.message(), dialog.page()?.url() || "");
1495
- void dialog.dismiss().catch((error) => { failure = error; });
1496
- };
1497
- context.on("dialog", dialogHandler);
1498
- pageHandler = (event) => executionEvent("error", "pageerror", event.error().message, event.page()?.url() || "");
1499
- context.on("weberror", pageHandler);
1500
- context.setDefaultTimeout(5000);
1501
- if (!retained?.context) {
1502
- await context.routeWebSocket("**/*", (socket) => {
1503
- websocketObserver.blockedWebSocket?.(socket.url());
1504
- socket.close();
1505
- });
1506
- if (retained) await context.route("**/*", (route) => route.abort());
1507
- }
1508
- routeHandler = async (route) => {
1509
- const request = route.request();
1510
- const url = request.url();
1511
- const hints = request.headers();
1512
- if (hints["next-router-prefetch"] === "1" || /(?:^|[\s,;])prefetch(?:$|[\s,;])/.test(hints["sec-purpose"] || hints.purpose || "")) {
1513
- if (blocked.length < 20) blocked.push({ url: url.slice(0, 2048), reason: "prefetch" }); else blockedOmitted++;
1514
- await route.abort(); return;
1515
- }
1516
- if (!accepting || !websiteScope(input, url, request.method()) || ["image", "font", "media"].includes(request.resourceType()) || requests >= 40) {
1517
- if (blocked.length < 20) blocked.push({ url: url.slice(0, 2048), reason: !accepting ? "workflow_complete" : !websiteScope(input, url, request.method()) ? "scope" : requests >= 40 ? "request_limit" : "asset" });
1518
- else blockedOmitted++;
1519
- await route.abort(); return;
1520
- }
1521
- requests++;
1522
- const work = pending.then(async () => {
1523
- try {
1524
- if (failure) { await route.abort(); return; }
1525
- const headers = Object.fromEntries(Object.entries(await request.allHeaders()).filter(([name]) => input.requestHeaders.includes(name)));
1526
- const resourceType = request.resourceType();
1527
- const body = request.postDataBuffer();
1528
- const response = await send({ url, method: request.method(), headers, ...(["script", "stylesheet"].includes(resourceType) ? { resourceType } : {}), ...(body !== null ? { bodyBase64: body.toString("base64") } : {}) });
1529
- const responseHeaders = Object.fromEntries(Object.entries(response.headers).filter(([, value]) => value !== undefined).map(([name, value]) => [name, Array.isArray(value) ? value.join(name === "set-cookie" ? "\n" : ", ") : value]));
1530
- await route.fulfill({ status: response.status, headers: responseHeaders, body: Buffer.from(response.body, "base64") });
1531
- } catch (error) { failure = error; await route.abort().catch(() => {}); }
1532
- });
1533
- pending = work;
1534
- await work;
1535
- };
1536
- await context.route("**/*", routeHandler);
1537
- page = retained?.page || await context.newPage();
1538
- if (restored && !retained?.context) await restoreBrowserPrivateState(page, context, restored.state, input.url);
1539
- let workflow = { outcome: "completed" }, actions = 0, phase = "navigation";
1540
- try {
1541
- if (!retained?.page) await page.goto(input.url, { waitUntil: "domcontentloaded", timeout: 60000 });
1542
- phase = "action";
1543
- for (const action of input.actions) {
1544
- if (action.type === "click") await page.locator(action.selector).click();
1545
- else if (action.type === "fill") await page.locator(action.selector).fill(action.value);
1546
- else if (action.type === "waitFor") await page.locator(action.selector).filter({ hasText: action.text }).waitFor({ state: "visible" });
1547
- else if (action.type === "select") await page.locator(action.selector).selectOption({ label: action.label });
1548
- else if (action.type === "check") await page.locator(action.selector).setChecked(action.checked);
1549
- else if (action.type === "press") await page.locator(action.selector).press(action.key);
1550
- else throw new Error("browser_action");
1551
- actions++;
1552
- }
1553
- } catch (error) {
1554
- if (!(error instanceof errors.TimeoutError)) throw error;
1555
- workflow = { outcome: "timed_out", phase, ...(phase === "action" ? { actionIndex: actions } : {}) };
1556
- }
1557
- await pending;
1558
- if (failure) throw failure;
1559
- const observe = { selector: "body", textOffset: 0, controlsOffset: 0, ...input.observe };
1560
- const snapshot = await page.locator(observe.selector).evaluateAll((regions, offsets) => {
1561
- const root = regions[0], items = [];
1562
- const controlSelector = 'input:not([type="hidden"]), button, select, textarea, a[href], [role="button"]';
1563
- const elements = root ? [...(root.matches(controlSelector) ? [root] : []), ...root.querySelectorAll(controlSelector)] : [];
1564
- let total = 0;
1565
- for (const element of elements) {
1566
- if (!element.getClientRects().length || getComputedStyle(element).visibility === "hidden") continue;
1567
- const index = total++;
1568
- if (index < offsets.controlsOffset || items.length >= 20) continue;
1569
- let selector = element.id ? "#" + CSS.escape(element.id) : "";
1570
- if (!selector || document.querySelectorAll(selector).length !== 1) {
1571
- const parts = [];
1572
- for (let node = element; node; node = node.parentElement) {
1573
- let index = 1;
1574
- for (let sibling = node.previousElementSibling; sibling; sibling = sibling.previousElementSibling) if (sibling.localName === node.localName) index++;
1575
- parts.unshift(CSS.escape(node.localName) + ":nth-of-type(" + index + ")");
1576
- }
1577
- selector = parts.join(" > ");
1578
- }
1579
- if (selector.length > 256) continue;
1580
- const label = element.getAttribute("aria-label") || Array.from(element.labels || []).map((label) => label.innerText).join(" ") || element.getAttribute("placeholder") || (element.localName === "input" ? "" : element.innerText) || "";
1581
- items.push({ index, selector, tag: element.localName, type: (element.getAttribute("type") || "").slice(0, 32), label: label.trim().slice(0, 128), disabled: element.matches(":disabled") || element.getAttribute("aria-disabled") === "true", ...(element.localName === "a" && element.href.length <= 2048 ? { href: element.href } : {}) });
1582
- }
1583
- const text = Array.from(root?.innerText || "");
1584
- return { controls: items, totalControls: total, matchedRegions: regions.length, text: text.slice(offsets.textOffset, offsets.textOffset + 4096).join(""), totalTextChars: text.length };
1585
- }, observe);
1586
- const url = page.url();
1587
- const value = { url: url.slice(0, 2048), title: (await page.title()).slice(0, 256), text: snapshot.text, truncated: snapshot.totalTextChars > observe.textOffset + Array.from(snapshot.text).length || observe.textOffset > 0 || snapshot.matchedRegions !== 1 || url.length > 2048, actions, workflow, controls: snapshot.controls, controlsOmitted: snapshot.totalControls - snapshot.controls.length, blocked, blockedOmitted,
1588
- observation: { ...observe, matchedRegions: snapshot.matchedRegions, totalTextChars: snapshot.totalTextChars, totalControls: snapshot.totalControls, nextTextOffset: null, nextControlsOffset: null },
1589
- complete: false, coverage: "One isolated browser workflow and a snapshot of the first matching DOM region. Missing or additional regions and omitted text/controls are not inspected. Offsets refer to this DOM snapshot. Continue a retained page to observe it without replaying actions; a call without page reloads the URL. A timed-out action may have had effects; inspect recorded traffic before deciding whether to retry. Remaining actions were not attempted. " + (input.persistSession ? "The approved identity retains bounded cookies, localStorage, IndexedDB, sessionStorage and OPFS across fresh contexts." : "Storage lasts for this browser page only."), verification: "not-independent" };
1590
- accepting = false;
1591
- await pending;
1592
- if (failure) throw failure;
1593
- const saved = restored ? await send({ sessionState: "save", expectedRevision: restored.revision, state: await captureBrowserPrivateState(page, context, input.target) }) : undefined;
1594
- if (retained) {
1595
- Object.assign(retained, { browser, context, page, startUrl: input.url, credentialRevision: restored?.credentialRevision, stateDigest: saved?.stateDigest });
1596
- value.page = { id: input.page.id, state: input.page.operation === "close" ? "closed" : "retained" };
1597
- keep = input.page.operation !== "close";
1598
- } else await context.close();
1599
- value.blockedOmitted = blockedOmitted;
1600
- value.executionEvents = executionEvents;
1601
- value.executionEventsOmitted = executionEventsOmitted;
1602
- while (Buffer.byteLength(JSON.stringify(value)) > 15936) {
1603
- if (value.executionEvents.length) { value.executionEvents.pop(); value.executionEventsOmitted++; }
1604
- else if (value.blocked.length) { value.blocked.pop(); value.blockedOmitted++; }
1605
- else if (Array.from(value.text).length > 512) { value.text = Array.from(value.text).slice(0, Math.floor(Array.from(value.text).length / 2)).join(""); value.truncated = true; }
1606
- else if (value.controls.length) { value.controls.pop(); value.controlsOmitted++; }
1607
- else { value.text = Array.from(value.text).slice(0, Math.floor(Array.from(value.text).length / 2)).join(""); value.truncated = true; }
1608
- }
1609
- const textEnd = observe.textOffset + Array.from(value.text).length;
1610
- value.observation.nextTextOffset = textEnd < snapshot.totalTextChars ? textEnd : null;
1611
- const controlsEnd = value.controls.length ? value.controls.at(-1).index + 1 : snapshot.totalControls;
1612
- value.observation.nextControlsOffset = controlsEnd < snapshot.totalControls ? controlsEnd : null;
1613
- if (retained) value.coverage += " The live page retains DOM and JavaScript state until close, worker loss or expiry. HTTP is denied between commands; no idle-time execution assertion is made.";
1614
- return value;
1615
- } finally {
1616
- accepting = false;
1617
- delete websocketObserver.blockedWebSocket;
1618
- if (retained && context) {
1619
- if (routeHandler) await context.unroute("**/*", routeHandler);
1620
- if (consoleHandler) context.off("console", consoleHandler);
1621
- if (dialogHandler) context.off("dialog", dialogHandler);
1622
- if (pageHandler) context.off("weberror", pageHandler);
1623
- }
1624
- if (!keep) await browser.close();
1625
- }
1626
- }
1627
- `;
1628
-
1629
- // apps/harness/src/sandbox/live-program.ts
1630
- var LIVE_PROGRAM = String.raw`
1631
- const readline = require("node:readline");
1632
- const crypto = require("node:crypto");
1633
- const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
1634
- const iterator = lines[Symbol.asyncIterator]();
1635
- ${ACTIVE_PROGRAM}
1636
- ${WEBSITE_PROGRAM}
1637
- ${PORT_PROGRAM}
1638
- ${BROWSER_PAGE_PROGRAM}
1639
- (async () => {
1640
- if (process.getuid() !== ${SANDBOX_UID}) throw new Error();
1641
- const first = await iterator.next();
1642
- if (first.done || Buffer.byteLength(first.value) > ${TASK_INPUT_LIMIT}) throw new Error();
1643
- const { tool, input } = JSON.parse(first.value);
1644
- let queue = Promise.resolve();
1645
- const send = (request) => {
1646
- const result = queue.then(async () => {
1647
- process.stdout.write(JSON.stringify({ request }) + "\n");
1648
- const next = await iterator.next();
1649
- if (next.done || Buffer.byteLength(next.value) > ${BROKER_RESPONSE_LIMIT}) throw new Error();
1650
- const message = JSON.parse(next.value);
1651
- if (!message || !("response" in message)) throw new Error();
1652
- return message.response;
1653
- });
1654
- queue = result;
1655
- return result;
1656
- };
1657
- let value;
1658
- if (tool === "port_scan") {
1659
- value = await portScan(input, send);
1660
- } else if (tool === "web_discover" || tool === "web_request") {
1661
- value = await websiteObserve(tool, input, send);
1662
- } else if (tool === "web_browse") {
1663
- value = input.page ? await browseRetainedPage(input, send) : await websiteBrowse(input, send);
1664
- } else if (tool === "http_request") {
1665
- const response = await send({ ...input.request, requestId: input.requestId });
1666
- const body = Buffer.from(response.body, "base64").toString("utf8");
1667
- value = { requestId: input.requestId, status: response.status, contentType: response.contentType, body: body.slice(0, 8192), truncated: body.length > 8192 };
1668
- } else if (tool === "session_health") {
1669
- value = { status: "healthy" };
1670
- } else if (tool === "browser_workflow") {
1671
- const version = require("/opt/mettle-browser/node_modules/playwright/package.json").version;
1672
- if (version !== input.version) throw new Error();
1673
- const { chromium } = require("/opt/mettle-browser/node_modules/playwright");
1674
- const browser = await chromium.launch({ headless: true, chromiumSandbox: false, timeout: 10000, args: ["--disable-dev-shm-usage"] });
1675
- try {
1676
- const context = await browser.newContext({ serviceWorkers: "block", acceptDownloads: false });
1677
- context.setDefaultTimeout(3000);
1678
- await context.routeWebSocket("**/*", (socket) => socket.close());
1679
- let failure;
1680
- await context.route("**/*", async (route) => {
1681
- try {
1682
- const request = route.request();
1683
- const body = request.postDataBuffer();
1684
- const response = await send({ url: request.url(), method: request.method(), ...(body !== null ? { bodyBase64: body.toString("base64") } : {}) });
1685
- await route.fulfill({ status: response.status, contentType: response.contentType, body: Buffer.from(response.body, "base64") });
1686
- } catch (error) { failure = error; await route.abort(); }
1687
- });
1688
- const page = await context.newPage();
1689
- await page.goto(input.url, { waitUntil: "domcontentloaded", timeout: 10000 });
1690
- for (const action of input.actions) {
1691
- if (action.type === "click") await page.locator(action.selector).click();
1692
- else if (action.type === "fill") await page.locator(action.selector).fill(action.value);
1693
- else throw new Error();
1694
- }
1695
- await queue;
1696
- if (failure) throw failure;
1697
- const content = await page.locator("body").innerText();
1698
- value = { workflowId: input.workflowId, url: page.url(), title: await page.title(), text: content.slice(0, 8192), truncated: content.length > 8192, actions: input.actions.length };
1699
- await context.close();
1700
- await queue;
1701
- if (failure) throw failure;
1702
- } finally { await browser.close(); }
1703
- } else if (${JSON.stringify(ACTIVE_SCAN_TOOLS)}.includes(tool)) {
1704
- value = await activeScan(tool, input, send);
1705
- } else throw new Error();
1706
- const result = JSON.stringify({ inputDigest: crypto.createHash("sha256").update(first.value).digest("hex"), value });
1707
- if (Buffer.byteLength(result) > ${TASK_RESULT_LIMIT}) throw new Error();
1708
- process.stdout.write(result + "\n");
1709
- lines.close();
1710
- process.stdin.destroy();
1711
- })().catch(() => { lines.close(); process.stdin.destroy(); process.exitCode = 1; });
1712
- `;
1713
-
1714
- // apps/harness/src/sandbox/environment-program.ts
1715
- var ENVIRONMENT_PROGRAM = String.raw`
1716
- function environmentCheck(input) {
1717
- const child = require("node:child_process");
1718
- const checks = [];
1719
- const missing = [];
1720
- const run = (binary, args) => child.spawnSync(binary, args, { env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: "/tmp", TMPDIR: "/tmp", LC_ALL: "C", PLAYWRIGHT_BROWSERS_PATH: "/opt/mettle-browser/browsers", PYTHONDONTWRITEBYTECODE: "1", JAVA_TOOL_OPTIONS: "-Djava.awt.headless=true -Duser.home=/tmp" }, stdio: ["ignore", "pipe", "pipe"], timeout: 20000, killSignal: "SIGKILL", maxBuffer: 8192, encoding: "utf8", shell: false });
1721
- if (input.browserVersion) {
1722
- const script = "(async () => { const p = require('/opt/mettle-browser/node_modules/playwright'); if (require('/opt/mettle-browser/node_modules/playwright/package.json').version !== process.argv[1]) process.exit(2); const browser = await p.chromium.launch({headless:true,chromiumSandbox:false,args:['--disable-dev-shm-usage']}); await browser.close(); process.stdout.write('ready'); })().catch(() => process.exit(1));";
1723
- const result = run("/usr/local/bin/node", ["-e", script, input.browserVersion]);
1724
- (result.status === 0 && result.stdout === "ready" ? checks : missing).push("playwright/" + input.browserVersion);
1725
- }
1726
- for (const profile of input.scanners) {
1727
- let result, matched = false;
1728
- if (profile.tool === "port_scan") {
1729
- result = run("/usr/local/bin/naabu", ["-version"]);
1730
- matched = (result.stdout + result.stderr).split(/\s+/).includes(profile.version);
1731
- } else if (profile.tool === "nuclei_scan") {
1732
- result = run("/usr/local/bin/nuclei", ["-version"]);
1733
- matched = (result.stdout + result.stderr).split(/\s+/).includes("v" + profile.version);
1734
- } else if (profile.tool === "sqli_confirm") {
1735
- result = run("/usr/bin/python3", ["/opt/mettle/sqlmap/sqlmap.py", "--version"]);
1736
- matched = String(result.stdout).split("\n")[0].trim() === profile.version + "#stable";
1737
- } else if (profile.tool === "zap_scan") {
1738
- result = run("/zap/zap.sh", ["-Xmx768m", "-silent", "-cmd", "-dir", "/tmp/zap-preflight", "-version"]);
1739
- matched = String(result.stdout).split(/\s+/).includes(profile.version);
1740
- } else throw new Error("Unsupported admitted prerequisite");
1741
- (result.status === 0 && !result.error && matched ? checks : missing).push(profile.tool + "/" + profile.version);
1742
- }
1743
- return { status: missing.length ? "unavailable" : "ready", checks, missing };
1744
- }
1745
- `;
1746
-
1747
- // apps/harness/src/sandbox/experiment-program.ts
1748
- var EXPERIMENT_PROGRAM = String.raw`
1749
- function experimentRun(args, input) {
1750
- const path = require("node:path"), child = require("node:child_process");
1751
- const { policy } = input;
1752
- if (!policy.runtimes.includes(args.runtime) || !["node", "bash"].includes(args.runtime) || !Number.isInteger(policy.timeoutMs) || policy.timeoutMs < 100 || policy.timeoutMs > 10000 || !Number.isInteger(policy.maxScriptBytes) || policy.maxScriptBytes > 16384) throw new Error("experiment_policy");
1753
- const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
1754
- const run = (program) => {
1755
- if (typeof program !== "string" || !program.length || program.includes("\0") || Buffer.byteLength(program) > policy.maxScriptBytes) throw new Error("experiment_program");
1756
- const root = fs.mkdtempSync("/tmp/mettle-experiment-"), workspace = path.join(root, "workspace");
1757
- try {
1758
- materializeWorkspace(workspace, input.files);
1759
- const script = path.join(root, args.runtime === "node" ? "experiment.cjs" : "experiment.bash");
1760
- fs.writeFileSync(script, program, { flag: "wx", mode: 0o600 });
1761
- const result = child.spawnSync(args.runtime === "node" ? "/usr/local/bin/node" : "/bin/bash", args.runtime === "node" ? ["--", script] : ["--noprofile", "--norc", "-p", "--", script], {
1762
- cwd: workspace, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, LC_ALL: "C" },
1763
- stdio: ["ignore", "pipe", "pipe"], timeout: policy.timeoutMs, killSignal: "SIGKILL", maxBuffer: ${TASK_INPUT_LIMIT}, encoding: "utf8", shell: false
1764
- });
1765
- const stdout = result.stdout || "", stderr = result.stderr || "", exceeded = result.error?.code === "ENOBUFS";
1766
- return { scriptSha256: hash(program), status: spawnStatus(result, "completed", exceeded), exitCode: result.status, stdout: stdout.slice(0, ${CHILD_OUTPUT_KEEP}), stderr: stderr.slice(0, ${CHILD_OUTPUT_KEEP}), truncated: exceeded || stdout.length > ${CHILD_OUTPUT_KEEP} || stderr.length > ${CHILD_OUTPUT_KEEP} };
1767
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
1768
- };
1769
- const control = run(args.negativeControl), probe = run(args.program);
1770
- return { subjectDigest: hash(JSON.stringify(input.subject)), filesDigest: hash(JSON.stringify(input.files)), runtime: args.runtime, control, probe, assertion: "inconclusive", verification: "not-independent" };
1771
- }
1772
- `;
1773
-
1774
- // apps/harness/src/sandbox/joern-program.ts
1775
- var JOERN_QUERY = `
1776
- import io.joern.dataflowengineoss.language.*
1777
- import io.shiftleft.codepropertygraph.generated.nodes.CfgNode
1778
- import io.shiftleft.semanticcpg.language.*
1779
- import java.nio.file.{Files, Paths}
1780
-
1781
- @main def exec(cpgFile: String, traceFile: String, outFile: String): Unit = {
1782
- val request = ujson.read(Files.readString(Paths.get(traceFile)))
1783
- val expected = request("files").arr.map(_.str).toSet
1784
- val workspace = request("workspace").str + "/"
1785
- def relative(name: String): String = name.stripPrefix(workspace).stripPrefix("./")
1786
- val version = io.joern.joerncli.console.JoernConsole.version
1787
- if (version != request("version").str) throw new IllegalArgumentException("version_mismatch")
1788
- importCpg(cpgFile).getOrElse(throw new IllegalArgumentException("invalid_cpg"))
1789
- def matches(node: CfgNode, location: ujson.Value): Boolean =
1790
- relative(node.method.filename) == location("path").str && node.lineNumber.exists(line =>
1791
- line >= location("startLine").num.toInt && line <= location("endLine").num.toInt)
1792
- val sources = cpg.all.collect { case node: CfgNode if matches(node, request("source")) => node }.take(129).toList
1793
- val sinks = cpg.call.filter(node => matches(node, request("sink"))).argument.filter(_.argumentIndex > 0).take(129).toList
1794
- val parsed = cpg.file.name.map(relative).filter(expected.contains).toSet
1795
- var omitted = sources.size > 128 || sinks.size > 128
1796
- val flows = sinks.take(128).iterator.reachableByFlows(sources.take(128)).take(17).toList
1797
- omitted ||= flows.size > 16
1798
- val paths = flows.take(16).flatMap { flow =>
1799
- val located = flow.elements.collect {
1800
- case node: CfgNode if node.lineNumber.exists(_ > 0) && expected.contains(relative(node.method.filename)) =>
1801
- (relative(node.method.filename), node.lineNumber.get)
1802
- }.toList
1803
- val points = located.distinct
1804
- if (located.size != flow.elements.size || points.size > 32) omitted = true
1805
- if (points.size < 2 || points.size > 32) {
1806
- omitted = true
1807
- None
1808
- } else Some(ujson.Arr.from(points.map { case (path, line) => ujson.Obj("path" -> path, "line" -> line) }))
1809
- }
1810
- val result = ujson.Obj(
1811
- "version" -> version,
1812
- "sourceNodes" -> sources.size,
1813
- "sinkNodes" -> sinks.size,
1814
- "parsedFiles" -> ujson.Arr.from(parsed.toList.sorted),
1815
- "paths" -> ujson.Arr.from(paths),
1816
- "omitted" -> omitted,
1817
- "complete" -> (sources.nonEmpty && sinks.nonEmpty && parsed == expected && !omitted)
1818
- )
1819
- Files.writeString(Paths.get(outFile), result.toString)
1820
- }
1821
- `;
1822
- var JOERN_PROGRAM = `
1823
- function scanJoern(input, workspace, root) {
1824
- const path = require("node:path");
1825
- const child = require("node:child_process");
1826
- const query = ${JSON.stringify(JOERN_QUERY)};
1827
- const queryDigest = crypto.createHash("sha256").update(query).digest("hex");
1828
- const binding = { version: input.version, ruleId: ${JSON.stringify(JOERN_QUERY_ID)}, queryDigest };
1829
- const incomplete = (status) => ({ ...binding, status, complete: false, sourceNodes: 0, sinkNodes: 0, parsedFiles: [], paths: [], omitted: false });
1830
- const deadline = Date.now() + ${JOERN_TIMEOUT_MS};
1831
- const run = (binary, args) => child.spawnSync(binary, args, {
1832
- cwd: root, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, LC_ALL: "C.UTF-8", JAVA_TOOL_OPTIONS: "-Xmx2304m -XX:ActiveProcessorCount=2 -DZstdNativePath=/opt/mettle/joern/native/libzstd-jni.so -Djava.io.tmpdir=" + root + " -Duser.home=" + root, JOERN_HOME: "/opt/mettle/joern" },
1833
- stdio: ["ignore", "pipe", "pipe"], timeout: Math.max(1, deadline - Date.now()), killSignal: "SIGKILL", maxBuffer: 1048576, encoding: "utf8", shell: false,
1834
- });
1835
- const cpg = path.join(root, "cpg.bin");
1836
- const trace = path.join(root, "trace.json");
1837
- const script = path.join(root, "query.sc");
1838
- const report = path.join(root, "report.json");
1839
- fs.writeFileSync(trace, JSON.stringify({ ...input.trace, version: input.version, workspace, files: input.files.map((file) => file.path) }), { flag: "wx", mode: 0o600 });
1840
- fs.writeFileSync(script, query, { flag: "wx", mode: 0o600 });
1841
- const parsed = run("/opt/mettle/joern/frontends/jssrc2cpg/bin/jssrc2cpg", ["-J-Xmx2304m", "-J-XX:ActiveProcessorCount=2", "-Dlog4j.configurationFile=/opt/mettle/joern/conf/log4j2.xml", workspace, "--output", cpg, "--no-tsTypes"]);
1842
- if (parsed.error || parsed.status !== 0) return incomplete(spawnStatus(parsed));
1843
- const analyzed = run("/opt/mettle/joern/bin/repl-bridge", ["-J-Xmx2304m", "-J-XX:ActiveProcessorCount=2", "-Dlog4j.configurationFile=/opt/mettle/joern/conf/log4j2.xml", "--script", script, "--param", "cpgFile=" + cpg, "--param", "traceFile=" + trace, "--param", "outFile=" + report]);
1844
- if (analyzed.error || analyzed.status !== 0) return incomplete(String(analyzed.stdout).includes("version_mismatch") ? "version_mismatch" : spawnStatus(analyzed));
1845
- try {
1846
- if (fs.statSync(report).size > 24576) return incomplete("output_limit");
1847
- return { ...binding, ...JSON.parse(fs.readFileSync(report, "utf8")), status: "completed" };
1848
- } catch { return incomplete("failed"); }
1849
- }
1850
- `;
1851
-
1852
- // apps/harness/src/storage/package-probe.ts
1853
- var PACKAGE_IMPORT_TIMEOUT_MS = 3000;
1854
- var PACKAGE_CHANGE_LIMIT = 128;
1855
- function packageJavaScript(path) {
1856
- return /\.[cm]?js$/.test(path);
1857
- }
1858
- var PackageImportObservationSchema = strictObject({
1859
- status: _enum([
1860
- "loaded",
1861
- "load_failed",
1862
- "timed_out",
1863
- "output_limit",
1864
- "unavailable",
1865
- "failed"
1866
- ]),
1867
- changes: array(strictObject({
1868
- target: string().min(1).max(64),
1869
- key: string().max(256),
1870
- kind: _enum(["added", "removed", "changed"]),
1871
- enumerable: boolean().nullable()
1872
- })).max(PACKAGE_CHANGE_LIMIT),
1873
- error: string().max(1024).nullable(),
1874
- truncated: boolean(),
1875
- stdout: string().max(4096),
1876
- stderr: string().max(4096)
1877
- });
1878
- var PackageImportResultSchema = PackageImportObservationSchema.extend({
1879
- repository: string().min(1).max(256),
1880
- revision: CommitSchema,
1881
- path: RepositoryPathSchema,
1882
- verification: literal("not-independent"),
1883
- complete: boolean()
1884
- }).refine((value) => value.complete === (value.status === "loaded" && !value.truncated), { message: "Only an untruncated loaded import has complete observations." });
1885
-
1886
- // apps/harness/src/sandbox/package-program.ts
1887
- var PACKAGE_IMPORT_PROGRAM = `
1888
- const fs = require("node:fs");
1889
- const { pathToFileURL } = require("node:url");
1890
- const { isMainThread, Worker, MessageChannel, receiveMessageOnPort, parentPort, workerData } = require("node:worker_threads");
1891
- if (isMainThread) {
1892
- const { port1, port2 } = new MessageChannel();
1893
- const worker = new Worker(__filename, { workerData: { path: process.argv[2], port: port2 }, transferList: [port2] });
1894
- let error = "The import worker did not return a complete observation.";
1895
- worker.on("error", cause => { error = String(cause).slice(0, 1024); });
1896
- worker.on("exit", code => {
1897
- const before = receiveMessageOnPort(port1)?.message;
1898
- const after = receiveMessageOnPort(port1)?.message;
1899
- port1.close();
1900
- const changes = [];
1901
- let truncated = false;
1902
- if (code !== 0 || !before || !after) {
1903
- fs.writeFileSync(3, JSON.stringify({ status: "failed", changes, error, truncated }));
1904
- process.exit(0);
1905
- }
1906
- for (let i = 0; i < before.length; i++) {
1907
- const [name, entries] = before[i];
1908
- const previous = new Map(entries.map(entry => [entry[0], entry]));
1909
- const current = new Map(after.targets[i][1].map(entry => [entry[0], entry]));
1910
- for (const key of new Set([...current.keys(), ...previous.keys()])) {
1911
- const old = previous.get(key), next = current.get(key);
1912
- if (old && next && old[2].every((value, index) => Object.is(value, next[2][index]))) continue;
1913
- if (changes.length === ${PACKAGE_CHANGE_LIMIT}) { truncated = true; continue; }
1914
- changes.push({ target: name, key: (next || old)[1], kind: !old ? "added" : !next ? "removed" : "changed", enumerable: next?.[3] ?? null });
1915
- }
1916
- }
1917
- fs.writeFileSync(3, JSON.stringify({ status: after.status, changes, error: after.error, truncated }));
1918
- process.exit(0);
1919
- });
1920
- } else {
1921
- const { path, port } = workerData;
1922
- delete workerData.port;
1923
- parentPort.close();
1924
- const post = port.postMessage.bind(port);
1925
- const ownKeys = Reflect.ownKeys;
1926
- const descriptor = Object.getOwnPropertyDescriptor;
1927
- const same = Object.is;
1928
- const string = String;
1929
- const slice = Function.call.bind(String.prototype.slice);
1930
- const append = Function.call.bind(Array.prototype.push);
1931
- const get = Function.call.bind(Map.prototype.get);
1932
- const set = Function.call.bind(Map.prototype.set);
1933
- const exit = process.exit.bind(process);
1934
- const identities = new Map();
1935
- let nextIdentity = 1;
1936
- function identity(value) {
1937
- if (same(value, -0)) return 0;
1938
- let id = get(identities, value);
1939
- if (id === undefined) { id = nextIdentity++; set(identities, value, id); }
1940
- return id;
1941
- }
1942
- const targets = [
1943
- ["globalThis", globalThis], ["Object.prototype", Object.prototype],
1944
- ["Array.prototype", Array.prototype], ["Function.prototype", Function.prototype],
1945
- ["String.prototype", String.prototype], ["Number.prototype", Number.prototype],
1946
- ["Boolean.prototype", Boolean.prototype], ["Promise.prototype", Promise.prototype],
1947
- ["Map.prototype", Map.prototype], ["Set.prototype", Set.prototype],
1948
- ["RegExp.prototype", RegExp.prototype], ["Date.prototype", Date.prototype]
1949
- ];
1950
- function snapshot() {
1951
- const result = [];
1952
- for (let i = 0; i < targets.length; i++) {
1953
- const [name, target] = targets[i];
1954
- const keys = ownKeys(target), entries = [];
1955
- for (let j = 0; j < keys.length; j++) {
1956
- const key = keys[j], value = descriptor(target, key);
1957
- append(entries, [identity(key), slice(string(key), 0, 256), [identity(value.value), identity(value.get), identity(value.set), value.writable, value.enumerable, value.configurable], value.enumerable]);
1958
- }
1959
- append(result, [name, entries]);
1960
- }
1961
- return result;
1962
- }
1963
- for (const key of ownKeys(globalThis)) descriptor(globalThis, key);
1964
- post(snapshot());
1965
- (async () => {
1966
- let status = "loaded", error = null;
1967
- try { await import(pathToFileURL(path).href); }
1968
- catch (cause) { status = "load_failed"; error = slice(string(cause), 0, 1024); }
1969
- post({ status, error, targets: snapshot() });
1970
- exit(0);
1971
- })().catch(() => exit(1));
1972
- }
1973
- `;
1974
- var PACKAGE_PROBE_PROGRAM = String.raw`
1975
- function packageImportProbe(input, args, workspace, root) {
1976
- const child = require("node:child_process");
1977
- const path = require("node:path");
1978
- if (args.path !== input.path || !input.files.some(file => file.path === args.path) || !/\.[cm]?js$/.test(args.path)) throw new Error();
1979
- const script = path.join(root, "import-probe.cjs");
1980
- fs.writeFileSync(script, ${JSON.stringify(PACKAGE_IMPORT_PROGRAM)}, { flag: "wx", mode: 0o600 });
1981
- const result = child.spawnSync("/usr/local/bin/node", ["--max-old-space-size=128", script, path.join(workspace, args.path)], {
1982
- cwd: workspace, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root },
1983
- timeout: ${PACKAGE_IMPORT_TIMEOUT_MS}, killSignal: "SIGKILL", maxBuffer: ${TASK_INPUT_LIMIT},
1984
- stdio: ["ignore", "pipe", "pipe", "pipe"], encoding: "utf8", shell: false
1985
- });
1986
- let value = { status: spawnStatus(result), changes: [], error: null, truncated: false };
1987
- if (value.status === "completed") {
1988
- try { value = JSON.parse(result.output[3]); }
1989
- catch { value.status = "failed"; value.error = "The import probe did not return a complete observation."; }
1990
- }
1991
- const stdout = result.stdout || "", stderr = result.stderr || "";
1992
- const observation = { ...value, stdout: stdout.slice(0, ${CHILD_OUTPUT_KEEP}), stderr: stderr.slice(0, ${CHILD_OUTPUT_KEEP}),
1993
- truncated: value.truncated || stdout.length > ${CHILD_OUTPUT_KEEP} || stderr.length > ${CHILD_OUTPUT_KEEP} || result.error?.code === "ENOBUFS" };
1994
- return Buffer.byteLength(JSON.stringify(observation)) > ${TASK_RESULT_LIMIT - 1024}
1995
- ? { status: "output_limit", changes: [], error: "The import observation exceeded the task evidence limit.", truncated: true, stdout: "", stderr: "" }
1996
- : observation;
1997
- }
1998
- `;
1999
-
2000
- // apps/harness/src/sandbox/report-program.ts
2001
- var PDF_BROWSER_PROGRAM = `
2002
- (async () => {
2003
- const fs = require("node:fs");
2004
- const crypto = require("node:crypto");
2005
- const input = JSON.parse(fs.readFileSync(0, "utf8"));
2006
- if (require("/opt/mettle-browser/node_modules/playwright/package.json").version !== input.version) throw new Error();
2007
- const { chromium } = require("/opt/mettle-browser/node_modules/playwright");
2008
- const browser = await chromium.launch({ headless: true, chromiumSandbox: false, timeout: 10000, args: ["--disable-dev-shm-usage"] });
2009
- try {
2010
- const context = await browser.newContext({ javaScriptEnabled: false, serviceWorkers: "block", acceptDownloads: false });
2011
- let blockedRequests = 0;
2012
- await context.route("**/*", async route => { blockedRequests++; await route.abort(); });
2013
- await context.routeWebSocket("**/*", socket => socket.close());
2014
- const page = await context.newPage();
2015
- await page.setContent(input.html, { waitUntil: "load", timeout: 10000 });
2016
- const body = await page.pdf({ format: "A4", printBackground: true, preferCSSPageSize: true, tagged: true, displayHeaderFooter: true, headerTemplate: "<span></span>", footerTemplate: '<div style="font-size:9px;width:100%;text-align:center">Mettle - <span class="pageNumber"></span> / <span class="totalPages"></span></div>', timeout: 10000 });
2017
- if (body.length > ${PDF_BYTES_LIMIT} || body.subarray(0, 5).toString() !== "%PDF-") throw new Error();
2018
- process.stdout.write(JSON.stringify({ content: body.toString("base64"), sha256: crypto.createHash("sha256").update(body).digest("hex"), htmlDigest: crypto.createHash("sha256").update(input.html).digest("hex"), blockedRequests }));
2019
- await context.close();
2020
- } finally { await browser.close(); }
2021
- })().catch(() => { process.exitCode = 1; });
2022
- `;
2023
- var REPORT_PROGRAM = `
2024
- function reportPdf(input) {
2025
- if (typeof input.html !== "string" || Buffer.byteLength(input.html) > ${PDF_BYTES_LIMIT} || input.version !== ${JSON.stringify(PLAYWRIGHT_VERSION)}) throw new Error();
2026
- const result = require("node:child_process").spawnSync("/usr/local/bin/node", ["-e", ${JSON.stringify(PDF_BROWSER_PROGRAM)}], {
2027
- input: JSON.stringify(input), encoding: "utf8", shell: false, timeout: 25000, killSignal: "SIGKILL", maxBuffer: ${PDF_RESULT_LIMIT},
2028
- env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: "/tmp", TMPDIR: "/tmp", PLAYWRIGHT_BROWSERS_PATH: "/opt/mettle-browser/browsers" }, stdio: ["pipe", "pipe", "pipe"]
2029
- });
2030
- if (result.status !== 0 || result.error) throw new Error();
2031
- return JSON.parse(result.stdout);
2032
- }
2033
- `;
2034
-
2035
- // apps/harness/src/sandbox/scanner-program.ts
2036
- var SPAWN_STATUS_PROGRAM = `
2037
- function spawnStatus(result, success = "completed", exceeded = false) {
2038
- return result.error?.code === "ETIMEDOUT" ? "timed_out" : result.error?.code === "ENOBUFS" || exceeded ? "output_limit" : result.error ? "unavailable" : result.status === 0 ? success : "failed";
2039
- }
2040
- `;
2041
- var SOURCE_SCANNER_PROGRAM = String.raw`
2042
- function scanSource(tool, input, workspace, root) {
2043
- const path = require("node:path");
2044
- const child = require("node:child_process");
2045
- const secret = tool === "secret_scan";
2046
- const binary = secret ? "/usr/local/bin/gitleaks" : "/usr/local/bin/osv-scanner";
2047
- const environment = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, LC_ALL: "C" };
2048
- if (!secret) environment.OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY = "/opt/mettle/osv";
2049
- const invoke = (args, timeout, maxBuffer) => child.spawnSync(binary, args, {
2050
- cwd: workspace, env: environment, stdio: ["ignore", "pipe", "pipe"],
2051
- timeout, killSignal: "SIGKILL", maxBuffer, encoding: "utf8", shell: false
2052
- });
2053
-
2054
- const incomplete = (status) => ({ status, complete: false, findingCount: null, findings: [], scannedFiles: [] });
2055
- const version = invoke(secret ? ["version"] : ["--version"], ${SOURCE_SCANNER_VERSION_TIMEOUT_MS}, 4096);
2056
- if (version.error || version.status !== 0) return incomplete(spawnStatus(version));
2057
- if (!String(version.stdout).split(/\s+/).some((part) => part.replace(/^v/, "") === input.version)) return incomplete("version_mismatch");
2058
- const approved = new Set(input.files.map((file) => file.path));
2059
- const relative = (name) => {
2060
- if (typeof name !== "string") throw new Error();
2061
- const normalized = path.isAbsolute(name) ? path.relative(workspace, name) : name.replace(/^\.\//, "");
2062
- if (!approved.has(normalized)) throw new Error();
2063
- return normalized;
2064
- };
2065
- const bounded = (value, max = 256) => {
2066
- if (typeof value !== "string" || !value.length || value.length > max) throw new Error();
2067
- return value;
2068
- };
2069
- let args;
2070
- if (secret) {
2071
- fs.writeFileSync(path.join(root, "gitleaks.toml"), "[extend]\nuseDefault = true\n", { flag: "wx", mode: 0o600 });
2072
- fs.writeFileSync(path.join(root, "ignore"), "", { flag: "wx", mode: 0o600 });
2073
- args = ["dir", "--config", path.join(root, "gitleaks.toml"), "--gitleaks-ignore-path", path.join(root, "ignore"), "--ignore-gitleaks-allow", "--redact=100", "--no-banner", "--no-color", "--log-level", "error", "--exit-code", "10", "--max-archive-depth", "0", "--max-decode-depth", "0", "--report-format", "json", "--report-path", path.join(root, "gitleaks.json"), workspace];
2074
- } else {
2075
- try {
2076
- for (const entry of input.database.ecosystems) {
2077
- if (!${JSON.stringify(EcosystemSchema.options)}.includes(entry.name)) throw new Error();
2078
- const file = "/opt/mettle/osv/osv-scalibr/" + entry.name + "/all.zip";
2079
- const info = fs.statSync(file);
2080
- if (!info.isFile() || info.size <= 0 || info.size > ${MAX_DATABASE_BYTES}) throw new Error();
2081
- if (crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== entry.sha256) throw new Error();
2082
- }
2083
- } catch { return incomplete("database_mismatch"); }
2084
- fs.writeFileSync(path.join(root, "osv.toml"), "", { flag: "wx", mode: 0o600 });
2085
- args = ["scan", "source", "--offline", "--no-call-analysis=all", "--no-resolve", "--no-ignore", "--all-packages", "--config", path.join(root, "osv.toml"), "--format", "json", "--recursive", workspace];
2086
- }
2087
- const result = invoke(args, ${SOURCE_SCANNER_TIMEOUT_MS}, 1048576);
2088
- if (result.error || ![0, secret ? 10 : 1].includes(result.status)) return incomplete(spawnStatus(result));
2089
- try {
2090
- if (secret && fs.statSync(path.join(root, "gitleaks.json")).size > 1048576) return incomplete("output_limit");
2091
- const raw = JSON.parse(secret ? fs.readFileSync(path.join(root, "gitleaks.json"), "utf8") : result.stdout);
2092
- const findings = [];
2093
- const scanned = new Set();
2094
- let count = 0;
2095
- const add = (finding) => { count++; if (findings.length < ${SCANNER_FINDINGS_MAX}) findings.push(finding); };
2096
- if (secret) {
2097
- if (!Array.isArray(raw)) throw new Error();
2098
- for (const finding of raw) {
2099
- if (!Number.isInteger(finding.StartLine) || finding.StartLine < 1 || !Number.isInteger(finding.EndLine) || finding.EndLine < finding.StartLine) throw new Error();
2100
- add({ rule: bounded(finding.RuleID, 128), path: relative(finding.File), line: finding.StartLine, endLine: finding.EndLine });
2101
- }
2102
- if ((result.status === 10) !== (count > 0)) throw new Error();
2103
- } else {
2104
- if (!raw || !Array.isArray(raw.results)) throw new Error();
2105
- const ecosystems = new Set(input.database.ecosystems.map((entry) => entry.name));
2106
- for (const source of raw.results) {
2107
- const file = relative(source.source?.path);
2108
- if (!Array.isArray(source.packages)) throw new Error();
2109
- scanned.add(file);
2110
- for (const entry of source.packages) {
2111
- const ecosystem = bounded(entry.package?.ecosystem, 64);
2112
- if (!ecosystems.has(ecosystem) || !Array.isArray(entry.vulnerabilities ?? [])) throw new Error();
2113
- for (const advisory of entry.vulnerabilities ?? []) add({
2114
- path: file, ecosystem, package: bounded(entry.package.name), version: bounded(entry.package.version, 128), id: bounded(advisory.id, 128)
2115
- });
2116
- }
2117
- }
2118
- if ((result.status === 1) !== (count > 0) || !scanned.size) throw new Error();
2119
- }
2120
- const value = { status: "completed", complete: count <= ${SCANNER_FINDINGS_MAX} && (secret || scanned.size === approved.size), findingCount: count, findings, scannedFiles: secret ? "approved" : [...scanned] };
2121
- return Buffer.byteLength(JSON.stringify(value)) > ${TASK_RESULT_LIMIT - 1024} ? incomplete("output_limit") : value;
2122
- } catch { return incomplete("failed"); }
2123
- }
2124
- `;
2125
-
2126
- // apps/harness/src/sandbox/workspace-program.ts
2127
- var WORKSPACE_PROGRAM = String.raw`
2128
- function materializeWorkspace(workspace, files) {
2129
- const path = require("node:path");
2130
- fs.mkdirSync(workspace, { recursive: true });
2131
- for (const file of files) {
2132
- if (!file.path || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("workspace_path");
2133
- const destination = path.join(workspace, file.path);
2134
- fs.mkdirSync(path.dirname(destination), { recursive: true });
2135
- fs.writeFileSync(destination, file.content, { flag: "wx", mode: 0o600 });
2136
- }
2137
- }
2138
- `;
2139
-
2140
- // apps/harness/src/sandbox/task-program.ts
2141
- var TASK_PROGRAM = String.raw`
2142
- const fs = require("node:fs");
2143
- const crypto = require("node:crypto");
2144
- ${SPAWN_STATUS_PROGRAM}
2145
- ${WORKSPACE_PROGRAM}
2146
- ${EXPERIMENT_PROGRAM}
2147
- ${SOURCE_SCANNER_PROGRAM}
2148
- ${JOERN_PROGRAM}
2149
- ${ENVIRONMENT_PROGRAM}
2150
- ${REPORT_PROGRAM}
2151
- ${PACKAGE_PROBE_PROGRAM}
2152
- try {
2153
- const bytes = fs.readFileSync(0);
2154
- if (bytes.length > ${TASK_INPUT_MAX} || process.getuid() !== ${SANDBOX_UID}) throw new Error();
2155
- const request = JSON.parse(bytes.toString("utf8"));
2156
- const { tool, args, input } = request;
2157
- if (bytes.length > (${JSON.stringify(TASK_INPUT_LIMITS)}[tool] ?? ${TASK_INPUT_LIMIT})) throw new Error();
2158
- let value;
2159
- if (tool === "environment_check") {
2160
- value = environmentCheck(input);
2161
- } else if (tool === "experiment_run") {
2162
- value = experimentRun(args, input);
2163
- } else if (tool === "report_pdf") {
2164
- value = reportPdf(input);
2165
- } else if (tool === "web_capture_read") {
2166
- if (args.id !== input.capture.id) throw new Error();
2167
- value = { ...input.capture, provenance: "operator-supplied-unverified" };
2168
- } else if (tool === "web_capture_compare") {
2169
- const { left, right } = input;
2170
- if (left.id !== args.left || right.id !== args.right || left.id === right.id || left.url !== right.url || left.method !== right.method) throw new Error();
2171
- value = {
2172
- left: left.id, right: right.id, url: left.url, method: left.method,
2173
- leftIdentity: left.identity, rightIdentity: right.identity,
2174
- leftStatus: left.status, rightStatus: right.status,
2175
- sameBody: left.body === right.body, provenance: "operator-supplied-unverified"
2176
- };
2177
- } else if (tool === "change_inspect") {
2178
- value = input;
2179
- } else if (["test_run", "package_import_probe", "semgrep_scan", "bash_run", "secret_scan", "dependency_scan", "joern_confirm"].includes(tool)) {
2180
- const path = require("node:path");
2181
- const child = require("node:child_process");
2182
- const root = fs.mkdtempSync("/tmp/mettle-task-");
2183
- const workspace = path.join(root, "workspace");
2184
- materializeWorkspace(workspace, input.files);
2185
- if (tool === "package_import_probe") {
2186
- value = packageImportProbe(input, args, workspace, root);
2187
- } else if (tool === "joern_confirm") {
2188
- value = scanJoern(input, workspace, root);
2189
- } else if (tool === "secret_scan" || tool === "dependency_scan") {
2190
- value = scanSource(tool, input, workspace, root);
2191
- } else if (tool === "bash_run") {
2192
- const profile = input.profile;
2193
- if (args.profileId !== profile.id || typeof profile.script !== "string" || !profile.script.length || profile.script.length > 8192 || profile.script.includes("\0") || !Number.isInteger(profile.timeoutMs) || profile.timeoutMs < 100 || profile.timeoutMs > ${CHILD_TIMEOUT_MAX_MS}) throw new Error();
2194
- const script = path.join(root, "approved.bash");
2195
- fs.writeFileSync(script, profile.script, { flag: "wx", mode: 0o600 });
2196
- const result = child.spawnSync("/bin/bash", ["--noprofile", "--norc", "-p", "--", script], {
2197
- cwd: workspace, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, LC_ALL: "C" },
2198
- stdio: ["ignore", "pipe", "pipe"], timeout: profile.timeoutMs, killSignal: "SIGKILL",
2199
- maxBuffer: ${TASK_INPUT_LIMIT}, encoding: "utf8", shell: false
2200
- });
2201
- const stdout = result.stdout || "";
2202
- const stderr = result.stderr || "";
2203
- const exceeded = result.error?.code === "ENOBUFS";
2204
- value = {
2205
- subject: input.subject, profileId: profile.id,
2206
- scriptSha256: crypto.createHash("sha256").update(profile.script).digest("hex"), verification: "not-independent",
2207
- status: spawnStatus(result, "completed", exceeded),
2208
- exitCode: result.status, signal: result.signal ?? null,
2209
- stdout: stdout.slice(0, ${CHILD_OUTPUT_KEEP}), stderr: stderr.slice(0, ${CHILD_OUTPUT_KEEP}),
2210
- truncated: stdout.length > ${CHILD_OUTPUT_KEEP} || stderr.length > ${CHILD_OUTPUT_KEEP} || exceeded
2211
- };
2212
- } else if (tool === "semgrep_scan") {
2213
- const environment = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root, SEMGREP_SETTINGS_FILE: "/dev/null", SEMGREP_SEND_METRICS: "off", SEMGREP_ENABLE_VERSION_CHECK: "0" };
2214
- const version = child.spawnSync("/usr/local/bin/semgrep", ["--version"], { cwd: workspace, env: environment, timeout: ${SEMGREP_VERSION_TIMEOUT_MS}, killSignal: "SIGKILL", maxBuffer: 4096, encoding: "utf8", shell: false });
2215
- if (version.error?.code === "ETIMEDOUT") value = { status: "timed_out", output: "" };
2216
- else if (version.error || version.status !== 0) value = { status: "unavailable", output: "" };
2217
- else if (version.stdout.trim() !== input.version) value = { status: "version_mismatch", output: "" };
2218
- else {
2219
- const rules = path.join(root, "rules.yaml");
2220
- fs.writeFileSync(rules, input.rules, { flag: "wx", mode: 0o600 });
2221
- const scanned = child.spawnSync("/usr/local/bin/semgrep", ["scan", "--config", rules, "--json", "--quiet", "--metrics=off", "--disable-version-check", "--oss-only", "--jobs=1", "."], { cwd: workspace, env: environment, timeout: ${SEMGREP_SCAN_TIMEOUT_MS}, killSignal: "SIGKILL", maxBuffer: ${TASK_INPUT_LIMIT}, encoding: "utf8", shell: false });
2222
- let status = spawnStatus(scanned);
2223
- let output = "";
2224
- if (status === "completed") {
2225
- try {
2226
- const report = JSON.parse(scanned.stdout || "");
2227
- const results = Array.isArray(report.results) ? report.results : [];
2228
- output = JSON.stringify({
2229
- version: report.version,
2230
- errors: (Array.isArray(report.errors) ? report.errors : []).slice(0, 1000).map(() => ({})),
2231
- paths: { scanned: (report.paths && Array.isArray(report.paths.scanned) ? report.paths.scanned : []).slice(0, 10000) },
2232
- resultCount: results.length,
2233
- results: results.slice(0, ${SCANNER_FINDINGS_MAX}).map((entry) => ({ check_id: entry.check_id, path: entry.path, start: { line: entry.start.line }, end: { line: entry.end.line }, extra: { severity: entry.extra.severity, message: String(entry.extra.message).slice(0, 1024) } }))
2234
- });
2235
- } catch { status = "failed"; }
2236
- }
2237
- value = { status, output };
2238
- }
2239
- } else {
2240
- if (args.profileId !== input.profile.id || !input.files.some((file) => file.path === input.profile.entrypoint) || input.profile.timeoutMs < 100 || input.profile.timeoutMs > ${CHILD_TIMEOUT_MAX_MS}) throw new Error();
2241
- const result = child.spawnSync("/usr/local/bin/node", ["--test", "--", input.profile.entrypoint], {
2242
- cwd: workspace, env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: root, TMPDIR: root },
2243
- timeout: input.profile.timeoutMs, killSignal: "SIGKILL", maxBuffer: ${TASK_INPUT_LIMIT},
2244
- encoding: "utf8", shell: false
2245
- });
2246
- const stdout = result.stdout || "";
2247
- const stderr = result.stderr || "";
2248
- value = {
2249
- repository: input.repository, revision: input.revision, profileId: input.profile.id,
2250
- patchProposalId: input.patchProposalId, verification: "not-independent",
2251
- status: spawnStatus(result, "passed"),
2252
- exitCode: result.status, stdout: stdout.slice(0, ${CHILD_OUTPUT_KEEP}), stderr: stderr.slice(0, ${CHILD_OUTPUT_KEEP}),
2253
- truncated: stdout.length > ${CHILD_OUTPUT_KEEP} || stderr.length > ${CHILD_OUTPUT_KEEP} || result.error?.code === "ENOBUFS"
2254
- };
2255
- }
2256
- } else if (tool === "repo_read" || tool === "repo_search") {
2257
- const binding = { repository: input.repository, revision: input.revision };
2258
- if (tool === "repo_read") {
2259
- const file = input.files.find((entry) => entry.path === args.path);
2260
- if (!file) throw new Error();
2261
- value = { ...binding, path: file.path, content: file.content, sha256: crypto.createHash("sha256").update(file.content).digest("hex") };
2262
- } else {
2263
- const matches = [];
2264
- let scanned = 0;
2265
- let limited = false;
2266
- for (const file of input.files) {
2267
- for (const [index, line] of file.content.split("\n").entries()) {
2268
- if (!line.includes(args.text)) continue;
2269
- matches.push({ path: file.path, line: index + 1, excerpt: line.slice(0, ${REPO_EXCERPT}) });
2270
- if (matches.length === ${REPO_SEARCH_MATCHES}) { limited = true; break; }
2271
- }
2272
- if (limited) break;
2273
- scanned++;
2274
- }
2275
- value = { ...binding, scanned, total: input.total, complete: !limited && scanned === input.total, matches };
2276
- }
2277
- } else throw new Error();
2278
- const result = JSON.stringify({ inputDigest: crypto.createHash("sha256").update(bytes).digest("hex"), value });
2279
- if (Buffer.byteLength(result) > (tool === "report_pdf" ? ${PDF_RESULT_LIMIT} : ${TASK_RESULT_LIMIT})) throw new Error();
2280
- fs.writeFileSync(1, result);
2281
- } catch { process.exitCode = 1; }
2282
- `;
2283
-
2284
- // apps/harness/src/sandbox/tasks.ts
2285
- var sandboxReceipt = strictObject({
2286
- runtime: SandboxRuntimeSchema,
2287
- image: string(),
2288
- policyDigest: string(),
2289
- inputDigest: string(),
2290
- cleanupVerified: literal(true)
2291
- });
2292
- var SandboxReceiptSchema = union([
2293
- sandboxReceipt,
2294
- sandboxReceipt.extend({
2295
- cleanupVerified: literal(false),
2296
- retainedPageId: uuid()
2297
- })
2298
- ]);
2299
- function isolatedTask(tool, args, input, session) {
2300
- TaskNameSchema.parse(tool);
2301
- const bytes = Buffer.from(canonicalJson({ tool, args, input }), "utf8");
2302
- const limit = TASK_INPUT_LIMITS[tool] ?? TASK_INPUT_LIMIT;
2303
- if (bytes.length > limit)
2304
- throw new HarnessError("sandbox_input_limit", `The isolated ${tool} task exceeds its ${limit}-byte input limit.`);
2305
- return {
2306
- tool,
2307
- args,
2308
- input: bytes,
2309
- inputDigest: sha256(bytes.toString("utf8")),
2310
- session
2311
- };
2312
- }
2313
- function taskProfile(tool) {
2314
- if (tool === "joern_confirm")
2315
- return "analysis";
2316
- return [
2317
- "browser_workflow",
2318
- "web_browse",
2319
- "zap_scan",
2320
- "environment_check",
2321
- "report_pdf"
2322
- ].includes(tool) ? "browser" : "tools";
2323
- }
2324
- function taskTimeoutMs(tool) {
2325
- if (tool === "joern_confirm")
2326
- return JOERN_TASK_TIMEOUT_MS;
2327
- if (tool === "port_scan")
2328
- return PORT_SCAN_TASK_TIMEOUT_MS;
2329
- if (tool === "environment_check")
2330
- return 90000;
2331
- if (["web_discover", "web_browse"].includes(tool))
2332
- return ACTIVE_SCAN_TASK_TIMEOUT_MS;
2333
- if (ACTIVE_SCAN_TOOLS.some((name) => name === tool))
2334
- return ACTIVE_SCAN_TASK_TIMEOUT_MS;
2335
- if (["secret_scan", "dependency_scan"].includes(tool))
2336
- return SOURCE_SCANNER_TASK_TIMEOUT_MS;
2337
- return tool === "semgrep_scan" ? SEMGREP_TASK_TIMEOUT_MS : TASK_TIMEOUT_MS;
2338
- }
2339
- function taskResultLimit(tool) {
2340
- return tool === "report_pdf" ? PDF_RESULT_LIMIT : TASK_RESULT_LIMIT;
2341
- }
2342
- function taskSpec(image, labels, profile, runtime, retained = false) {
2343
- return {
2344
- ...launchSpec(image, "", labels, profile, runtime),
2345
- Entrypoint: ["/bin/sleep"],
2346
- Cmd: [String(retained ? 600 : TASK_CONTAINER_LIFETIME_S)]
2347
- };
2348
- }
2349
- function taskExecSpec(live, runtime) {
2350
- const base = launchSpec("", "", {}, "probe", runtime);
2351
- const program = live ? LIVE_PROGRAM : TASK_PROGRAM;
2352
- return {
2353
- User: base.User,
2354
- Privileged: false,
2355
- Tty: false,
2356
- AttachStdin: true,
2357
- AttachStdout: true,
2358
- AttachStderr: true,
2359
- WorkingDir: base.WorkingDir,
2360
- Env: base.Env,
2361
- Cmd: [
2362
- "/usr/local/bin/node",
2363
- "-e",
2364
- runtime === "runc" ? `if (!/^Seccomp:\\s+2$/m.test(require("node:fs").readFileSync("/proc/self/status", "utf8"))) process.exit(1);
2365
- ${program}` : program
2366
- ]
2367
- };
2368
- }
2369
- function taskRuntimeDigest(image, runtime) {
2370
- return digest({
2371
- specs: {
2372
- tools: taskSpec(image, {}, "tools", runtime),
2373
- browser: taskSpec(image, {}, "browser", runtime),
2374
- analysis: taskSpec(image, {}, "analysis", runtime)
2375
- },
2376
- exec: taskExecSpec(false, runtime),
2377
- liveExec: taskExecSpec(true, runtime),
2378
- timeoutMs: Object.fromEntries(TaskNameSchema.options.map((tool) => [tool, taskTimeoutMs(tool)]))
2379
- });
2380
- }
2381
- function taskPolicyDigest(image, task, runtime) {
2382
- const profile = taskProfile(task.tool);
2383
- return digest({
2384
- profile,
2385
- spec: taskSpec(image, {}, profile, runtime, Boolean(task.session?.page)),
2386
- exec: taskExecSpec(isLiveTask(task.tool), runtime),
2387
- tool: task.tool,
2388
- inputDigest: task.inputDigest,
2389
- timeoutMs: taskTimeoutMs(task.tool)
2390
- });
2391
- }
2392
- function isLiveTask(tool) {
2393
- return [
2394
- "port_scan",
2395
- "http_request",
2396
- "web_discover",
2397
- "web_request",
2398
- "web_browse",
2399
- "session_health",
2400
- "browser_workflow",
2401
- ...ACTIVE_SCAN_TOOLS
2402
- ].includes(tool);
2403
- }
2404
-
2405
- // apps/harness/src/sandbox/worker.ts
2406
- async function checkSandbox(config, engine) {
2407
- const info = dockerJson(await engine.call("GET", "/info"));
2408
- const selected = sandboxRuntime(SandboxConfigSchema.parse(config));
2409
- const runtime = info?.Runtimes?.[selected];
2410
- if (info?.OSType !== "linux" || typeof info.ID !== "string" || !info.ID || config.engineId !== undefined && info.ID !== config.engineId || !runtime || typeof runtime.path !== "string" || (selected === GVISOR_RUNTIME ? !/(^|\/)runsc$/.test(runtime.path) || !Array.isArray(runtime.runtimeArgs) || canonicalJson(runtime.runtimeArgs) !== canonicalJson(["--platform=systrap"]) : !/(^|\/)runc$/.test(runtime.path) || runtime.runtimeArgs !== undefined && (!Array.isArray(runtime.runtimeArgs) || runtime.runtimeArgs.length !== 0)) || info.MemoryLimit !== true || info.CpuCfsQuota !== true || info.PidsLimit !== true)
2411
- throw new HarnessError("sandbox_unavailable", `A Linux engine with ${selected === GVISOR_RUNTIME ? "mettle-runsc systrap" : "runc without custom runtime arguments"} and resource controllers is required; runtime fallback is forbidden.`);
2412
- if (selected === "runc" && (!Array.isArray(info.SecurityOptions) || !info.SecurityOptions.some((option) => typeof option === "string" && option.split(",").includes("name=seccomp"))))
2413
- throw new HarnessError("sandbox_unavailable", "Local Docker requires kernel seccomp support and the explicit builtin profile.");
2414
- const image = dockerJson(await engine.call("GET", `/images/${encodeURIComponent(config.image)}/json`));
2415
- if ((config.image.startsWith("sha256:") ? image?.Id !== config.image : !Array.isArray(image?.RepoDigests) || !image.RepoDigests.includes(config.image)) || Object.keys(image.Config?.Volumes ?? {}).length !== 0)
2416
- throw new HarnessError("sandbox_image", "Preload the approved digest-pinned probe image without declared volumes.");
2417
- return info.ID;
2418
- }
2419
- async function sandboxFromEnvironment(chat, environment = process.env) {
2420
- if (!environment.HARNESS_SANDBOX_IMAGE) {
2421
- if (environment.HARNESS_SANDBOX_RUNTIME !== undefined || environment.HARNESS_SANDBOX_ENGINE_ID !== undefined || environment.HARNESS_SANDBOX_LOCATION !== undefined)
2422
- throw new HarnessError("sandbox_config", "Explicit sandbox settings require HARNESS_SANDBOX_IMAGE.");
2423
- return;
2424
- }
2425
- if (!chat)
2426
- throw new HarnessError("sandbox_config", "HARNESS_SANDBOX_IMAGE requires HARNESS_CHAT=1.");
2427
- const config = sandboxConfig(environment);
2428
- const engineId = await checkSandbox(config, new DockerEngine(config.socket));
2429
- return Object.freeze({
2430
- ...config,
2431
- engineId,
2432
- location: config.location ?? "local"
2433
- });
2434
- }
2435
- var CONTAINMENT_FINDINGS = new Set([
2436
- "sandbox_policy",
2437
- "sandbox_identity",
2438
- "sandbox_probe_failed",
2439
- "sandbox_output",
2440
- "sandbox_missing"
2441
- ]);
2442
- function containmentFinding(error) {
2443
- return error instanceof HarnessError && CONTAINMENT_FINDINGS.has(error.code);
2444
- }
2445
- function cancelled(error) {
2446
- return !(error instanceof HarnessError) || error.code === "sandbox_transport" || error.code === "stop_requested";
2447
- }
2448
- function jobSpec(record2, job, runtime) {
2449
- const labels = sandboxLabels(record2);
2450
- if (job?.kind === "task")
2451
- return taskSpec(record2.image, labels, taskProfile(job.task.tool), runtime, Boolean(job.task.session?.page));
2452
- if (job?.kind === "shell")
2453
- return shellSpec(record2.image, job.command, labels, runtime);
2454
- return launchSpec(record2.image, probeScript(runtime), labels, "probe", runtime);
2455
- }
2456
-
2457
- class SandboxWorker {
2458
- engine;
2459
- journal;
2460
- createBroker;
2461
- config;
2462
- runtime;
2463
- constructor(config, engine, journal, createBroker) {
2464
- this.engine = engine;
2465
- this.journal = journal;
2466
- this.createBroker = createBroker;
2467
- this.config = Object.freeze(SandboxConfigSchema.parse(config));
2468
- this.runtime = sandboxRuntime(this.config);
2469
- }
2470
- tool() {
2471
- return {
2472
- name: PROBE_TOOL,
2473
- description: "Run the fixed offline sandbox installation probe; no target, shell arguments or file inputs are accepted.",
2474
- schema: strictObject({}),
2475
- outputSchema: strictObject({
2476
- probe: literal("passed"),
2477
- runtime: literal(this.runtime),
2478
- image: string(),
2479
- cleanupVerified: literal(true)
2480
- }),
2481
- label: () => "Check sandbox",
2482
- summarizeArgs: () => "",
2483
- summarizeResult: () => ({ summary: "Probe passed" }),
2484
- execute: (_input, signal, callId) => this.execute(callId, signal, undefined)
2485
- };
2486
- }
2487
- shellTools() {
2488
- return sandboxAllowsExecution(this.config, SHELL_TOOL) ? [this.shellTool()] : [];
2489
- }
2490
- shellTool() {
2491
- if (!sandboxAllowsExecution(this.config, SHELL_TOOL))
2492
- throw new HarnessError("sandbox_denied", "Shell and repository code execution require gVisor.");
2493
- return {
2494
- name: SHELL_TOOL,
2495
- description: `Run one POSIX shell command inside a disposable gVisor container: no network, no host files, no writable path, unprivileged user, ${PROBE_TIMEOUT_MS / 1000}-second limit, output cut at ${SHELL_OUTPUT_MAX} bytes. Returns exit code and captured output.`,
2496
- schema: ShellCommandSchema,
2497
- outputSchema: strictObject({
2498
- command: string(),
2499
- exitCode: number().int().nullable(),
2500
- timedOut: boolean(),
2501
- output: string(),
2502
- truncated: boolean(),
2503
- runtime: literal(this.runtime),
2504
- image: string(),
2505
- cleanupVerified: literal(true)
2506
- }),
2507
- label: () => "Run shell command",
2508
- summarizeArgs: (args) => {
2509
- const parsed = ShellCommandSchema.safeParse(args);
2510
- return parsed.success ? `$ ${parsed.data.command.split(`
2511
- `)[0]}` : "shell command";
2512
- },
2513
- summarizeResult: (output) => {
2514
- const value = output;
2515
- const suffix = value.truncated ? " (truncated)" : "";
2516
- if (value.timedOut)
2517
- return { summary: `Timed out${suffix}` };
2518
- const lines = value.output ? value.output.split(`
2519
- `).length : 0;
2520
- return {
2521
- summary: `Exit ${value.exitCode} · ${counted(lines, "line")}${suffix}`
2522
- };
2523
- },
2524
- execute: async (input, signal, callId) => {
2525
- const { command } = ShellCommandSchema.parse(input);
2526
- return this.execute(callId, signal, { kind: "shell", command });
2527
- }
2528
- };
2529
- }
2530
- runTask = (tool, args, input, signal, callId, session) => {
2531
- if (!sandboxAllowsExecution(this.config, tool))
2532
- return Promise.reject(new HarnessError("sandbox_denied", "Shell and repository code execution require gVisor."));
2533
- return this.execute(callId, signal, {
2534
- kind: "task",
2535
- task: isolatedTask(tool, args, input, session)
2536
- });
2537
- };
2538
- async inspect(record2) {
2539
- const response = await this.engine.call("GET", `/containers/${record2.containerId ?? record2.name}/json`);
2540
- if (response.status === 404)
2541
- return;
2542
- const container = dockerJson(response);
2543
- const expected = sandboxLabels(record2);
2544
- if (!/^[a-f0-9]{64}$/.test(container?.Id ?? "") || record2.containerId && container.Id !== record2.containerId || container.Name !== `/${record2.name}` || container.Config?.Image !== record2.image || Object.entries(expected).some(([key, value]) => container.Config?.Labels?.[key] !== value))
2545
- throw new HarnessError("sandbox_identity", "The container does not match its durable reservation; it will not be executed or removed.");
2546
- return container;
2547
- }
2548
- assertPolicy(container, record2, job) {
2549
- const spec = jobSpec(record2, job, this.runtime);
2550
- for (const key of [
2551
- "User",
2552
- "WorkingDir",
2553
- "Entrypoint",
2554
- "Cmd",
2555
- "Tty",
2556
- "OpenStdin",
2557
- "NetworkDisabled",
2558
- "Healthcheck"
2559
- ]) {
2560
- if (canonicalJson(container.Config?.[key] ?? (key === "NetworkDisabled" ? false : null)) !== canonicalJson(spec[key]))
2561
- throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox policy.");
2562
- }
2563
- for (const key of [
2564
- "Runtime",
2565
- "NetworkMode",
2566
- "ReadonlyRootfs",
2567
- "Privileged",
2568
- "CapDrop",
2569
- "SecurityOpt",
2570
- "Memory",
2571
- "MemorySwap",
2572
- "NanoCpus",
2573
- "PidsLimit",
2574
- "RestartPolicy"
2575
- ]) {
2576
- if (canonicalJson(container.HostConfig?.[key] ?? null) !== canonicalJson(spec.HostConfig[key]))
2577
- throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox policy.");
2578
- }
2579
- const env = container.Config?.Env ?? [];
2580
- if (!spec.Env.every((entry) => env.includes(entry)))
2581
- throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox environment.");
2582
- const log = container.HostConfig?.LogConfig;
2583
- if (log?.Type !== spec.HostConfig.LogConfig.Type || Object.entries(spec.HostConfig.LogConfig.Config).some(([key, value]) => log?.Config?.[key] !== value))
2584
- throw new HarnessError("sandbox_policy", "The engine did not retain the required sandbox log policy.");
2585
- const containment = object({
2586
- Mounts: array(looseObject({ Type: string(), Destination: string() })),
2587
- HostConfig: looseObject({
2588
- Binds: array(string()).nullable(),
2589
- Devices: array(unknown()).nullable(),
2590
- DeviceRequests: array(unknown()).nullable(),
2591
- CapAdd: array(string()).nullable(),
2592
- VolumesFrom: array(string()).nullable(),
2593
- PortBindings: record(string(), unknown()).nullable()
2594
- })
2595
- }).safeParse(container);
2596
- if (!containment.success)
2597
- throw new HarnessError("sandbox_policy", "The engine inspect response omitted a required containment field; an absent mount, device or port surface cannot be certified empty.");
2598
- const tmpfs = spec.HostConfig.Tmpfs ?? {};
2599
- const mounts = container.Mounts ?? [];
2600
- if (canonicalJson(container.HostConfig?.Tmpfs ?? {}) !== canonicalJson(tmpfs) || mounts.some((mount) => mount?.Type !== "tmpfs" || !Object.hasOwn(tmpfs, mount.Destination)) || container.HostConfig?.Binds?.length || container.HostConfig?.Devices?.length || container.HostConfig?.CapAdd?.length || container.HostConfig?.VolumesFrom?.length || container.HostConfig?.DeviceRequests?.length || container.HostConfig?.PublishAllPorts || Object.keys(container.HostConfig?.PortBindings ?? {}).length || ["PidMode", "IpcMode", "UTSMode", "UsernsMode"].some((key) => container.HostConfig?.[key] === "host" || String(container.HostConfig?.[key] ?? "").startsWith("container:")))
2601
- throw new HarnessError("sandbox_policy", "Unexpected sandbox mounts, devices or capabilities are forbidden.");
2602
- }
2603
- async cleanup(record2, workerId) {
2604
- if (record2.workerId !== workerId)
2605
- throw new HarnessError("sandbox_worker_mismatch", "Cleanup must run against the original Docker engine.");
2606
- const container = await this.inspect(record2);
2607
- if (!container) {
2608
- if (!record2.containerId)
2609
- throw new HarnessError("sandbox_uncertain", "Container creation was not acknowledged; absence cannot prove that a delayed create will not arrive.");
2610
- } else {
2611
- dockerStatus(await this.engine.call("DELETE", `/containers/${container.Id}?force=true`), 204);
2612
- const absent = await this.engine.call("GET", `/containers/${container.Id}/json`);
2613
- if (absent.status !== 404)
2614
- throw new HarnessError("sandbox_cleanup_failed", "Container removal has not been verified.");
2615
- }
2616
- await this.journal.removed(record2);
2617
- }
2618
- async engineId() {
2619
- const info = dockerJson(await this.engine.call("GET", "/info"));
2620
- if (typeof info?.ID !== "string" || !info.ID)
2621
- throw new HarnessError("sandbox_unavailable", "The original Docker engine identity is required.");
2622
- return info.ID;
2623
- }
2624
- async reap(workerId) {
2625
- const id = workerId ?? await this.engineId();
2626
- const pending = await this.journal.pending();
2627
- for (const record2 of pending)
2628
- await this.cleanup(record2, id);
2629
- return pending.length;
2630
- }
2631
- async execute(callId, signal, job) {
2632
- const task = job?.kind === "task" ? job.task : undefined;
2633
- const shell = job?.kind === "shell";
2634
- if (signal.aborted)
2635
- throw new HarnessError("stop_requested", "Sandbox execution was stopped before admission.");
2636
- const workerId = await checkSandbox(this.config, this.engine);
2637
- if (signal.aborted)
2638
- throw new HarnessError("stop_requested", "Sandbox execution was stopped before allocation.");
2639
- const page = task?.tool === "web_browse" ? task.session?.page : undefined;
2640
- if (page && (!this.journal.acquirePage || !this.journal.releasePage))
2641
- throw new HarnessError("browser_page_denied", "Retained pages require durable lifecycle support.");
2642
- const reused = page && page.operation !== "open";
2643
- const record2 = reused ? await this.journal.acquirePage(callId, task.args, page) : await this.journal.reserve(callId, workerId, this.config.image, task ? taskPolicyDigest(this.config.image, task, this.runtime) : job?.kind === "shell" ? shellPolicyDigest(this.config.image, job.command, this.runtime) : probePolicyDigest(this.config.image, this.runtime), task ? { tool: task.tool, args: task.args } : job?.kind === "shell" ? { tool: SHELL_TOOL, args: { command: job.command } } : undefined, page);
2644
- const deadline = AbortSignal.timeout(task ? taskTimeoutMs(task.tool) : PROBE_TIMEOUT_MS);
2645
- const bounded = AbortSignal.any([signal, deadline]);
2646
- let primary;
2647
- let value;
2648
- let exitCode = null;
2649
- let output = Buffer.alloc(0);
2650
- try {
2651
- if (!reused)
2652
- await this.journal.provision(record2, async () => {
2653
- const created = dockerCreated(await this.engine.call("POST", `/containers/create?name=${record2.name}`, jobSpec(record2, job, this.runtime), bounded, ENGINE_LAUNCH_TIMEOUT_MS));
2654
- if (!/^[a-f0-9]{64}$/.test(created?.Id ?? ""))
2655
- throw new HarnessError("sandbox_protocol", "Container creation did not return a valid identity.");
2656
- return created.Id;
2657
- });
2658
- const container = await this.inspect(record2);
2659
- if (!container)
2660
- throw new HarnessError("sandbox_missing", "The created sandbox disappeared before execution.");
2661
- if (record2.workerId !== workerId || record2.image !== this.config.image)
2662
- throw new HarnessError("sandbox_identity", "The retained page belongs to a different engine or image.");
2663
- this.assertPolicy(container, record2, job);
2664
- if (!reused)
2665
- await this.journal.start(record2, async () => {
2666
- if (bounded.aborted)
2667
- throw new HarnessError("stop_requested", "Sandbox dispatch was cancelled.");
2668
- dockerStatus(await this.engine.call("POST", `/containers/${record2.containerId}/start`, undefined, bounded, ENGINE_LAUNCH_TIMEOUT_MS), 204);
2669
- });
2670
- if (task)
2671
- value = await this.runIsolated(record2, task, bounded, callId);
2672
- else {
2673
- const exit = await this.engine.call("POST", `/containers/${record2.containerId}/wait?condition=not-running`, undefined, bounded, PROBE_TIMEOUT_MS).then((response) => dockerJson(response), (error) => {
2674
- if (shell && deadline.aborted && !signal.aborted && cancelled(error))
2675
- return;
2676
- throw error;
2677
- });
2678
- if (exit !== undefined) {
2679
- if (!Number.isInteger(exit?.StatusCode) || exit.Error)
2680
- throw new HarnessError("sandbox_protocol", "The sandbox engine did not report a valid exit status.");
2681
- if (!shell && exit.StatusCode !== 0)
2682
- throw new HarnessError("sandbox_probe_failed", "The fixed sandbox probe did not exit successfully.");
2683
- exitCode = exit.StatusCode;
2684
- }
2685
- const logs = await this.engine.call("GET", `/containers/${record2.containerId}/logs?stdout=true${shell ? "&stderr=true" : ""}`, undefined, shell ? signal : bounded, undefined, shell ? SHELL_LOG_LIMIT : undefined);
2686
- if (shell)
2687
- output = dockerFrames(logs, "both");
2688
- else if (dockerLogs(logs) !== PROBE_MARKER)
2689
- throw new HarnessError("sandbox_probe_failed", "The fixed sandbox probe returned unexpected output.");
2690
- }
2691
- if (signal.aborted || !shell && deadline.aborted)
2692
- throw new HarnessError("stop_requested", "Sandbox execution was cancelled.");
2693
- } catch (error) {
2694
- primary = containmentFinding(error) ? error : signal.aborted ? new HarnessError("stop_requested", "Sandbox execution was cancelled; no result is invented.") : deadline.aborted && cancelled(error) ? new HarnessError("sandbox_timeout", "The sandbox exceeded its wall deadline.") : error;
2695
- }
2696
- const retained = Boolean(page && page.operation !== "close" && primary === undefined);
2697
- try {
2698
- if (retained) {
2699
- try {
2700
- await this.journal.releasePage(record2, callId);
2701
- } catch (error) {
2702
- await this.cleanup(record2, workerId);
2703
- throw error;
2704
- }
2705
- } else if (primary instanceof HarnessError && primary.code === "sandbox_rejected" && !record2.containerId)
2706
- await this.journal.removed(record2);
2707
- else
2708
- await this.cleanup(record2, workerId);
2709
- } catch (cleanupError) {
2710
- if (!containmentFinding(primary))
2711
- primary = cleanupError;
2712
- }
2713
- if (primary !== undefined)
2714
- throw primary;
2715
- if (task)
2716
- return {
2717
- ...value,
2718
- sandbox: {
2719
- runtime: this.runtime,
2720
- image: record2.image,
2721
- policyDigest: record2.policyDigest,
2722
- inputDigest: task.inputDigest,
2723
- cleanupVerified: !retained,
2724
- ...retained ? { retainedPageId: page.id } : {}
2725
- }
2726
- };
2727
- if (job?.kind === "shell") {
2728
- const truncated = output.length > SHELL_OUTPUT_MAX;
2729
- return {
2730
- command: job.command,
2731
- exitCode,
2732
- timedOut: exitCode === null,
2733
- output: new TextDecoder("utf-8").decode(output.subarray(0, SHELL_OUTPUT_MAX), { stream: truncated }),
2734
- truncated,
2735
- runtime: this.runtime,
2736
- image: record2.image,
2737
- cleanupVerified: true
2738
- };
2739
- }
2740
- return {
2741
- probe: "passed",
2742
- runtime: this.runtime,
2743
- image: record2.image,
2744
- cleanupVerified: true
2745
- };
2746
- }
2747
- async runIsolated(record2, task, signal, callId) {
2748
- const live = isLiveTask(task.tool);
2749
- if (live && !this.createBroker)
2750
- throw new HarnessError("web_denied", "Live tasks require the operator-approved broker; no network fallback is allowed.");
2751
- const spec = taskExecSpec(live, this.runtime);
2752
- const created = dockerJson(await this.engine.call("POST", `/containers/${record2.containerId}/exec`, spec, signal), 201);
2753
- if (!/^[a-f0-9]{64}$/.test(created?.Id ?? ""))
2754
- throw new HarnessError("sandbox_protocol", "The engine did not return an isolated exec identity.");
2755
- const inspect = () => this.engine.call("GET", `/exec/${created.Id}/json`, undefined, signal, undefined, Math.max(65536, Buffer.byteLength(canonicalJson(spec)) * 2 + 8192)).then((response) => dockerJson(response));
2756
- const initial = await inspect();
2757
- if (initial.ID !== created.Id || initial.ContainerID !== record2.containerId || initial.Running !== false || initial.OpenStdin !== true || initial.OpenStdout !== true || initial.OpenStderr !== true || initial.ProcessConfig?.privileged !== false || initial.ProcessConfig?.tty !== false || initial.ProcessConfig?.user !== spec.User || initial.ProcessConfig?.entrypoint !== spec.Cmd[0] || canonicalJson(initial.ProcessConfig?.arguments) !== canonicalJson(spec.Cmd.slice(1)))
2758
- throw new HarnessError("sandbox_policy", "The engine did not retain the fixed isolated task command.");
2759
- await this.journal.assertTaskRunning(record2);
2760
- const broker = live ? await this.createBroker(task, signal, callId) : undefined;
2761
- let refusal;
2762
- const exchange = broker ? async (request) => {
2763
- try {
2764
- await this.journal.assertTaskRunning(record2);
2765
- const response = await broker.request(request);
2766
- await this.journal.assertTaskRunning(record2);
2767
- return response;
2768
- } catch (error) {
2769
- refusal ??= error;
2770
- throw error;
2771
- }
2772
- } : undefined;
2773
- await this.journal.assertTaskRunning(record2);
2774
- let failed = true;
2775
- try {
2776
- const session = await this.engine.startExec(created.Id, task.input, signal, taskTimeoutMs(task.tool), exchange, Math.max(65536, taskResultLimit(task.tool) + 16384));
2777
- const result = await this.collectIsolated(record2, task, created.Id, session, inspect, signal, broker);
2778
- failed = false;
2779
- return result;
2780
- } catch (error) {
2781
- if (broker?.inFlight())
2782
- throw new HarnessError("web_uncertain", "A live request was in flight when the task failed; inspect the journal before any retry.");
2783
- if (refusal !== undefined)
2784
- throw refusal;
2785
- throw error;
2786
- } finally {
2787
- await broker?.close?.(failed);
2788
- }
2789
- }
2790
- async collectIsolated(record2, task, execId, session, inspect, signal, broker) {
2791
- const created = { Id: execId };
2792
- const output = dockerLogs(await session.result);
2793
- if (Buffer.byteLength(output) > taskResultLimit(task.tool))
2794
- throw new HarnessError("sandbox_output_limit", "The isolated result exceeds its size limit.");
2795
- while (true) {
2796
- const status = await inspect();
2797
- if (status.ID !== created.Id || status.ContainerID !== record2.containerId || typeof status.Running !== "boolean")
2798
- throw new HarnessError("sandbox_identity", "The exec changed container identity.");
2799
- if (!status.Running) {
2800
- if (status.ExitCode !== 0)
2801
- throw new HarnessError("sandbox_task_failed", "The fixed isolated observation failed; no result is invented.");
2802
- break;
2803
- }
2804
- await sleep(50, undefined, { signal });
2805
- }
2806
- let result;
2807
- try {
2808
- result = JSON.parse(output);
2809
- } catch {
2810
- throw new HarnessError("sandbox_output", "The isolated result is not JSON.");
2811
- }
2812
- if (!result || result.inputDigest !== task.inputDigest || !result.value || typeof result.value !== "object" || Array.isArray(result.value))
2813
- throw new HarnessError("sandbox_output", "The isolated output does not match its exact input.");
2814
- return broker ? broker.finish(result.value) : result.value;
2815
- }
2816
- }
2817
-
2818
- export { counted, ran, scanned, ToolRegistry, JOERN_QUERY_ID, JoernProfileSchema, joernCandidate, EcosystemSchema, MAX_DATABASE_BYTES, SecretScanProfileSchema, DependencyScanProfileSchema, SOURCE_SCANNER_LABEL, ScannerImageProfilesSchema, SCANNER_FINDINGS_MAX, ScannerStatusSchema, PDF_BYTES_LIMIT, PDF_RESULT_LIMIT, REPO_SEARCH_FILES, REPO_SEARCH_MATCHES, REPO_EXCERPT, DockerEngine, dockerJson, sandboxConfig, ACTIVE_SCAN_TASK_TIMEOUT_MS, ACTIVE_SCAN_TOOLS, ActiveScannersSchema, scannerEndpoint, validateActiveScanners, SCANNER_REQUEST_STRIP_HEADERS, scannerRequestAllowed, sqlmapCandidate, PORT_CONNECT_TIMEOUT_MS, PORT_SCAN_TASK_TIMEOUT_MS, PortScanProfileSchema, PortOutcomeSchema, portDestination, JOERN_QUERY, packageJavaScript, PackageImportObservationSchema, PackageImportResultSchema, SandboxReceiptSchema, taskRuntimeDigest, checkSandbox, sandboxFromEnvironment, SandboxWorker };