0sec-cli 0.21.4 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/0.js +56 -56
- package/README.md +48 -63
- package/chunks/{adapt-loop-IGBRQAXO.js → adapt-loop-J6QCZZTB.js} +4 -4
- package/chunks/{artifact-scraper-LNQJUORS.js → artifact-scraper-MFKT6OI4.js} +2 -2
- package/chunks/{assumption-mining-4MJTP6H5.js → assumption-mining-646LYSJY.js} +16 -16
- package/chunks/{chunk-Z3IKZX3X.js → chunk-23DG5YKS.js} +1 -1
- package/chunks/{chunk-2WSXFFJZ.js → chunk-3E2KALZ5.js} +2 -2
- package/chunks/{chunk-TF46UWFU.js → chunk-4G7OVYJX.js} +1 -1
- package/chunks/{chunk-E2UOY5PJ.js → chunk-5BTJMINS.js} +12 -70
- package/chunks/{chunk-QZVG3BG4.js → chunk-5IBCBQEH.js} +1 -2
- package/chunks/{chunk-YA4PUM4H.js → chunk-7XCKYNLO.js} +1 -1
- package/chunks/{chunk-BACXRTQ4.js → chunk-AV2LG2L7.js} +87 -767
- package/chunks/{chunk-IMGWPNQM.js → chunk-BCSRRQG5.js} +76 -593
- package/chunks/{chunk-NOPBXZUV.js → chunk-BOUGKEKA.js} +1 -1
- package/chunks/{chunk-RF5QGKLR.js → chunk-CPWA6RWE.js} +3 -3
- package/chunks/{chunk-P6YG6TEZ.js → chunk-DKSCN2AB.js} +1 -1
- package/chunks/{chunk-YPOA3W4O.js → chunk-GOEA3NW6.js} +877 -110
- package/chunks/{chunk-ANDKY54G.js → chunk-HHPWRY2M.js} +4019 -7299
- package/chunks/{chunk-NDJU5ZZ7.js → chunk-IFME676H.js} +3 -3
- package/chunks/{chunk-HBKCGJAV.js → chunk-IVQV574Y.js} +1 -1
- package/chunks/{chunk-W4JOXE4N.js → chunk-IXT64DV5.js} +709 -1929
- package/chunks/{chunk-PC6RCKQV.js → chunk-KAU6NLGP.js} +59 -26
- package/chunks/{chunk-FAH6V4QM.js → chunk-LGCASF2C.js} +1117 -2291
- package/chunks/{chunk-CAJJRUTV.js → chunk-NLH4LMWH.js} +1 -1
- package/chunks/chunk-OHHCGDJ2.js +5523 -0
- package/chunks/{chunk-YHWWF4QY.js → chunk-OSBPGWTE.js} +3 -3
- package/chunks/{chunk-G34QP2XW.js → chunk-TM74SSHC.js} +1 -1
- package/chunks/{chunk-VOAXLO4A.js → chunk-TO35Y326.js} +5 -721
- package/chunks/{chunk-FWAYJ2IV.js → chunk-TV27OWIW.js} +8 -8
- package/chunks/chunk-UELXHK2Y.js +1950 -0
- package/chunks/{chunk-CL5KAUM6.js → chunk-V6GHX34G.js} +274 -8
- package/chunks/{chunk-WZISK5VC.js → chunk-VWITLWXB.js} +53 -7
- package/chunks/{chunk-ER3SUMYJ.js → chunk-W6UWZFKL.js} +0 -195
- package/chunks/chunk-WSDDGFA6.js +114 -0
- package/chunks/{chunk-5T6D5BVY.js → chunk-WZRVS7SB.js} +1 -1
- package/chunks/chunk-X7LAEEUW.js +2143 -0
- package/chunks/chunk-XA2O4WXA.js +1228 -0
- package/chunks/{chunk-Y5H6E24H.js → chunk-XCISCI4T.js} +1 -1
- package/chunks/{chunk-WPQLBN4S.js → chunk-XDKB2P5X.js} +1 -1
- package/chunks/{chunk-SBMB4OL3.js → chunk-YL6YAGLL.js} +62 -8
- package/chunks/{chunk-O6SD4A36.js → chunk-ZKQ2I3S2.js} +1 -1
- package/chunks/{codex-models-TYYV4JNN.js → codex-models-NHQW45SO.js} +5 -3
- package/chunks/{commands-WF5RG4I2.js → commands-N6GN5SBF.js} +2152 -3643
- package/chunks/{db-U3WQZVAO.js → db-5IWEJUYV.js} +3 -3
- package/chunks/{disclose-67GZCDBO.js → disclose-LDNKAVRH.js} +10 -8
- package/chunks/{dist-ACC3DZUZ.js → dist-BS4CY4RG.js} +3 -1
- package/chunks/{dist-EVTN3UTT.js → dist-FSS7MBDF.js} +2 -2
- package/chunks/{dist-F6X62TQK.js → dist-L3XJWWZ6.js} +225 -192
- package/chunks/{eval-runner-SA6OUGHZ.js → eval-runner-EMEGF24P.js} +11 -11
- package/chunks/{exploit-agent-36Q7XQFP.js → exploit-agent-KW4EB7BE.js} +1 -1
- package/chunks/{exploit-autoclimb-J4OTSTZ6.js → exploit-autoclimb-CXJ2ERRO.js} +1 -1
- package/chunks/{exploit-climb-PYEJDIC4.js → exploit-climb-TJWLY4XC.js} +2 -2
- package/chunks/fix-UUKPXCFB.js +26 -0
- package/chunks/{harness-GDUMKGSE.js → harness-C5HOKIEV.js} +3 -3
- package/chunks/{hunt-scan-UDJSCISU.js → hunt-scan-KZZ444V2.js} +14 -14
- package/chunks/{identity-JTIVJSV7.js → identity-62SVRW4H.js} +1 -1
- package/chunks/{kernel-primitive-F2IXUFAX.js → kernel-primitive-Z77IR7YS.js} +2 -2
- package/chunks/{kernel-vm-runner-276LHIOV.js → kernel-vm-runner-JW77P276.js} +2 -2
- package/chunks/{memsafety-scan-NHUOGTEX.js → memsafety-scan-FCPAXIZE.js} +2 -2
- package/chunks/{native-loop-3BSYVDUI.js → native-loop-P6P5GYVJ.js} +10 -10
- package/chunks/{npm-detectors-VIYJXKFQ.js → npm-detectors-Z2ZIQCN4.js} +2 -2
- package/chunks/orchestrate-DWWUT5RC.js +58 -0
- package/chunks/{process-PNMC36JI.js → process-N245QIQG.js} +1 -4
- package/chunks/{run-CBGE6DRQ.js → run-GAFKBCTN.js} +14935 -17598
- package/chunks/{runtime-PXO5G6UV.js → runtime-H7YBR5IZ.js} +4 -5
- package/chunks/{scope-NJHIDDTN.js → scope-QWY4VQZ7.js} +4 -1
- package/chunks/{session-store-46WBZM22.js → session-store-XIDFGG7K.js} +2 -2
- package/chunks/{variant-candidates-UH2WC2HA.js → variant-candidates-7D4F5E4H.js} +3 -3
- package/chunks/{web-recon-prepass-SFXU44SS.js → web-recon-prepass-VXXBPDAV.js} +3 -3
- package/cli-path-traversal-fixture.js +352 -0
- package/dashboard/assets/bot-Bg1Tp9_d.js +1 -0
- package/dashboard/assets/dist-B_dDCPbu.js +1 -0
- package/dashboard/assets/findings-page-mayGhNx6.js +5 -0
- package/dashboard/assets/format--susmHX7.js +1 -0
- package/dashboard/assets/input-TuVPR3Zl.js +1 -0
- package/dashboard/assets/jsx-runtime-MGseXjj5.js +3 -0
- package/dashboard/assets/live-page-HWWHX99U.js +1 -0
- package/dashboard/assets/meta-tile-ckHo-w7k.js +1 -0
- package/dashboard/assets/operations-BxZ0OoXO.js +10 -0
- package/dashboard/assets/operations-app-D3d35rgo.js +46 -0
- package/dashboard/assets/overview-page-W_4IteEr.js +1 -0
- package/dashboard/assets/page-header-Bw9AJgBV.js +1 -0
- package/dashboard/assets/scans-page-CgzVXHnn.js +1 -0
- package/dashboard/assets/siren-KQj8QAnJ.js +1 -0
- package/dashboard/assets/table-8EDklulN.js +1 -0
- package/dashboard/assets/tabs-CFHPOwg8.js +1 -0
- package/dashboard/index.html +2 -3
- package/node_modules/node-addon-api/LICENSE.md +9 -0
- package/node_modules/node-addon-api/README.md +95 -0
- package/node_modules/node-addon-api/common.gypi +21 -0
- package/node_modules/node-addon-api/except.gypi +25 -0
- package/node_modules/node-addon-api/index.js +14 -0
- package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/node_modules/node-addon-api/napi-inl.h +7250 -0
- package/node_modules/node-addon-api/napi.h +3463 -0
- package/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/node_modules/node-addon-api/node_api.gyp +9 -0
- package/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/node_modules/node-addon-api/nothing.c +0 -0
- package/node_modules/node-addon-api/package-support.json +21 -0
- package/node_modules/node-addon-api/package.json +481 -0
- package/node_modules/node-addon-api/tools/README.md +73 -0
- package/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/node_modules/node-gyp-build/LICENSE +21 -0
- package/node_modules/node-gyp-build/README.md +58 -0
- package/node_modules/node-gyp-build/SECURITY.md +5 -0
- package/node_modules/node-gyp-build/bin.js +84 -0
- package/node_modules/node-gyp-build/build-test.js +19 -0
- package/node_modules/node-gyp-build/index.js +6 -0
- package/node_modules/node-gyp-build/node-gyp-build.js +207 -0
- package/node_modules/node-gyp-build/optional.js +7 -0
- package/node_modules/node-gyp-build/package.json +43 -0
- package/node_modules/tree-sitter/LICENSE +21 -0
- package/node_modules/tree-sitter/README.md +90 -0
- package/node_modules/tree-sitter/binding.gyp +76 -0
- package/node_modules/tree-sitter/index.js +895 -0
- package/node_modules/tree-sitter/package.json +62 -0
- package/node_modules/tree-sitter/prebuilds/darwin-arm64/tree-sitter.node +0 -0
- package/node_modules/tree-sitter/prebuilds/darwin-x64/tree-sitter.node +0 -0
- package/node_modules/tree-sitter/prebuilds/linux-x64/tree-sitter.node +0 -0
- package/node_modules/tree-sitter/prebuilds/win32-x64/tree-sitter.node +0 -0
- package/node_modules/tree-sitter/src/addon_data.h +47 -0
- package/node_modules/tree-sitter/src/binding.cc +35 -0
- package/node_modules/tree-sitter/src/conversions.cc +140 -0
- package/node_modules/tree-sitter/src/conversions.h +22 -0
- package/node_modules/tree-sitter/src/language.cc +106 -0
- package/node_modules/tree-sitter/src/language.h +17 -0
- package/node_modules/tree-sitter/src/logger.cc +70 -0
- package/node_modules/tree-sitter/src/logger.h +19 -0
- package/node_modules/tree-sitter/src/lookaheaditerator.cc +122 -0
- package/node_modules/tree-sitter/src/lookaheaditerator.h +33 -0
- package/node_modules/tree-sitter/src/node.cc +1056 -0
- package/node_modules/tree-sitter/src/node.h +30 -0
- package/node_modules/tree-sitter/src/parser.cc +298 -0
- package/node_modules/tree-sitter/src/parser.h +35 -0
- package/node_modules/tree-sitter/src/query.cc +383 -0
- package/node_modules/tree-sitter/src/query.h +39 -0
- package/node_modules/tree-sitter/src/tree.cc +307 -0
- package/node_modules/tree-sitter/src/tree.h +45 -0
- package/node_modules/tree-sitter/src/tree_cursor.cc +213 -0
- package/node_modules/tree-sitter/src/tree_cursor.h +52 -0
- package/node_modules/tree-sitter/tree-sitter.d.ts +211 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/LICENSE +21 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/README.md +21 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/README.md +6 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/include/tree_sitter/api.h +1266 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/alloc.c +48 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/alloc.h +41 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/array.h +290 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/atomic.h +68 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/clock.h +146 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/error_costs.h +11 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/get_changed_ranges.c +501 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/get_changed_ranges.h +36 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/host.h +21 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/language.c +221 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/language.h +299 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/length.h +52 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/lexer.c +419 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/lexer.h +49 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/lib.c +14 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/node.c +774 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/parser.c +2151 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/parser.h +230 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/point.h +62 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/query.c +4134 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/reduce_action.h +34 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/reusable_node.h +95 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/stack.c +899 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/stack.h +133 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/subtree.c +1060 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/subtree.h +382 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/tree.c +165 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/tree.h +31 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/tree_cursor.c +714 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/tree_cursor.h +48 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/ICU_SHA +1 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/LICENSE +414 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/README.md +29 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/ptypes.h +1 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/umachine.h +448 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/urename.h +1 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/utf.h +1 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/utf16.h +733 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode/utf8.h +881 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/unicode.h +50 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/wasm/stdlib-symbols.txt +23 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/wasm/stdlib.c +109 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/wasm/wasm-stdlib.h +1294 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/wasm_store.c +1823 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/lib/src/wasm_store.h +31 -0
- package/node_modules/tree-sitter/vendor/tree-sitter/test/fixtures/grammars/.gitkeep +0 -0
- package/node_modules/tree-sitter-c/LICENSE +21 -0
- package/node_modules/tree-sitter-c/README.md +18 -0
- package/node_modules/tree-sitter-c/binding.gyp +20 -0
- package/node_modules/tree-sitter-c/bindings/node/binding.cc +20 -0
- package/node_modules/tree-sitter-c/bindings/node/index.d.ts +28 -0
- package/node_modules/tree-sitter-c/bindings/node/index.js +7 -0
- package/node_modules/tree-sitter-c/grammar.js +1463 -0
- package/node_modules/tree-sitter-c/package.json +108 -0
- package/node_modules/tree-sitter-c/prebuilds/darwin-arm64/tree-sitter-c.node +0 -0
- package/node_modules/tree-sitter-c/prebuilds/darwin-x64/tree-sitter-c.node +0 -0
- package/node_modules/tree-sitter-c/prebuilds/linux-x64/tree-sitter-c.node +0 -0
- package/node_modules/tree-sitter-c/prebuilds/win32-x64/tree-sitter-c.node +0 -0
- package/node_modules/tree-sitter-c/queries/highlights.scm +81 -0
- package/node_modules/tree-sitter-c/queries/tags.scm +9 -0
- package/node_modules/tree-sitter-c/src/grammar.json +9682 -0
- package/node_modules/tree-sitter-c/src/node-types.json +4556 -0
- package/node_modules/tree-sitter-c/src/parser.c +113852 -0
- package/node_modules/tree-sitter-c/src/tree_sitter/alloc.h +54 -0
- package/node_modules/tree-sitter-c/src/tree_sitter/array.h +290 -0
- package/node_modules/tree-sitter-c/src/tree_sitter/parser.h +265 -0
- package/package.json +10 -3
- package/vendor/tree-sitter-0.21.1-node24.json +16 -0
- package/vendor/tree-sitter-0.21.1-node24.tgz +0 -0
- package/chunks/chunk-CGHCEN7W.js +0 -169
- package/chunks/chunk-CXKDWFNO.js +0 -102
- package/chunks/chunk-GIQKESMN.js +0 -687
- package/chunks/chunk-HPQZHOIU.js +0 -67
- package/chunks/chunk-PD2BBBKT.js +0 -245
- package/chunks/chunk-WF5W75WY.js +0 -1070
- package/chunks/fix-J2KBRJDV.js +0 -12
- package/chunks/orchestrate-DHBFRTSI.js +0 -59
- package/chunks/replay-runner-LK2XFIVG.js +0 -39
- package/dashboard/assets/bot-yILPnRgD.js +0 -1
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +0 -1
- package/dashboard/assets/circle-alert-BEnMBMT-.js +0 -1
- package/dashboard/assets/client-DRYsuOGl.js +0 -9
- package/dashboard/assets/copy-Bi5vzoF9.js +0 -1
- package/dashboard/assets/desktop-BT7RkC4q.js +0 -32
- package/dashboard/assets/desktop-OaBI0E0e.css +0 -1
- package/dashboard/assets/dist-CmX-h7a4.js +0 -1
- package/dashboard/assets/dist-DAwYQmf_.js +0 -45
- package/dashboard/assets/findings-page-B4rwF6fZ.js +0 -5
- package/dashboard/assets/format-LNQSSpn6.js +0 -1
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +0 -1
- package/dashboard/assets/live-page-DoNwpPAO.js +0 -1
- package/dashboard/assets/meta-tile-BMtBC-Qq.js +0 -1
- package/dashboard/assets/operations-app-ER0gg7GN.js +0 -2
- package/dashboard/assets/operations-e2CGYKB1.js +0 -4
- package/dashboard/assets/overview-page-CmeWH17A.js +0 -1
- package/dashboard/assets/page-header-CIJyycfz.js +0 -1
- package/dashboard/assets/play-Dwo-9xt6.js +0 -1
- package/dashboard/assets/scans-page-CuhzWhHx.js +0 -1
- package/dashboard/assets/search-CiooTLck.js +0 -1
- package/dashboard/assets/siren-CCBwTSuk.js +0 -1
- package/dashboard/assets/table-BgsSBdke.js +0 -1
- package/dashboard/assets/tabs-vn6v7Bk6.js +0 -1
- package/dashboard/desktop.html +0 -21
package/chunks/chunk-WF5W75WY.js
DELETED
|
@@ -1,1070 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createRequire as __0CreateRequire } from "node:module";
|
|
3
|
-
const require = __0CreateRequire(import.meta.url);
|
|
4
|
-
import {
|
|
5
|
-
allowlistedChildEnv
|
|
6
|
-
} from "./chunk-QZVG3BG4.js";
|
|
7
|
-
import {
|
|
8
|
-
VERSION
|
|
9
|
-
} from "./chunk-PC6RCKQV.js";
|
|
10
|
-
|
|
11
|
-
// packages/core/dist/verify/replay-runner.js
|
|
12
|
-
import { spawn } from "node:child_process";
|
|
13
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
-
import { mkdirSync, mkdtempSync, writeFileSync, existsSync, statSync, readFileSync, rmSync } from "node:fs";
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
16
|
-
import { join, resolve, isAbsolute, posix, win32 } from "node:path";
|
|
17
|
-
import { arch as nodeArch, platform as nodePlatform } from "node:process";
|
|
18
|
-
import { gzipSync } from "node:zlib";
|
|
19
|
-
var STREAM_EXCERPT_BYTES = 8 * 1024;
|
|
20
|
-
var DEFAULT_STEP_TIMEOUT_MS = 3e4;
|
|
21
|
-
var MAX_STREAM_CAPTURE_BYTES = 1 * 1024 * 1024;
|
|
22
|
-
var promiseWithResolvers = Promise;
|
|
23
|
-
var LocalShellRunner = class {
|
|
24
|
-
kind = "local";
|
|
25
|
-
async exec(step, ctx) {
|
|
26
|
-
const start = Date.now();
|
|
27
|
-
if (step.action.type !== "shell") {
|
|
28
|
-
return {
|
|
29
|
-
argv: argvForStep(step),
|
|
30
|
-
exitCode: null,
|
|
31
|
-
stdoutFull: "",
|
|
32
|
-
stderrFull: "",
|
|
33
|
-
durationMs: Date.now() - start,
|
|
34
|
-
launchError: `LocalShellRunner only executes shell steps; got '${step.action.type}'`
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
const cmd = step.action.cmd;
|
|
38
|
-
const stepCwd = step.action.cwd ? resolveStepCwd(step.action.cwd, ctx.runDir) : ctx.runDir;
|
|
39
|
-
return new Promise((resolveP) => {
|
|
40
|
-
const argv = ["/bin/sh", "-c", cmd];
|
|
41
|
-
let stdout = "";
|
|
42
|
-
let stderr = "";
|
|
43
|
-
let stdoutBytes = 0;
|
|
44
|
-
let stderrBytes = 0;
|
|
45
|
-
let timedOut = false;
|
|
46
|
-
let settled = false;
|
|
47
|
-
let child;
|
|
48
|
-
try {
|
|
49
|
-
child = spawn("/bin/sh", ["-c", cmd], {
|
|
50
|
-
cwd: stepCwd,
|
|
51
|
-
// `cmd` is a PoC-authored shell step (model/target-derived), so this
|
|
52
|
-
// child must not inherit the harness's provider/cloud credentials.
|
|
53
|
-
// Build from the allowlist (PATH/HOME/TMPDIR + target-auth vars a
|
|
54
|
-
// reproduction legitimately needs) rather than copying process.env.
|
|
55
|
-
env: allowlistedChildEnv({ "ZERO_VERIFY": "1" }),
|
|
56
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
-
// On POSIX, isolate the shell and all descendants into a process
|
|
58
|
-
// group so a timeout cannot leave a grandchild holding stdout open.
|
|
59
|
-
detached: nodePlatform !== "win32"
|
|
60
|
-
});
|
|
61
|
-
} catch (err) {
|
|
62
|
-
resolveP({
|
|
63
|
-
argv,
|
|
64
|
-
exitCode: null,
|
|
65
|
-
stdoutFull: "",
|
|
66
|
-
stderrFull: "",
|
|
67
|
-
durationMs: Date.now() - start,
|
|
68
|
-
launchError: err instanceof Error ? err.message : String(err)
|
|
69
|
-
});
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const timer = setTimeout(() => {
|
|
73
|
-
timedOut = true;
|
|
74
|
-
try {
|
|
75
|
-
if (nodePlatform !== "win32" && child.pid) {
|
|
76
|
-
process.kill(-child.pid, "SIGKILL");
|
|
77
|
-
} else {
|
|
78
|
-
child.kill("SIGKILL");
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
}
|
|
82
|
-
}, ctx.stepTimeoutMs);
|
|
83
|
-
child.stdout?.on("data", (chunk) => {
|
|
84
|
-
if (stdoutBytes >= MAX_STREAM_CAPTURE_BYTES)
|
|
85
|
-
return;
|
|
86
|
-
const remaining = MAX_STREAM_CAPTURE_BYTES - stdoutBytes;
|
|
87
|
-
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
88
|
-
stdout += slice.toString("utf8");
|
|
89
|
-
stdoutBytes += slice.length;
|
|
90
|
-
});
|
|
91
|
-
child.stderr?.on("data", (chunk) => {
|
|
92
|
-
if (stderrBytes >= MAX_STREAM_CAPTURE_BYTES)
|
|
93
|
-
return;
|
|
94
|
-
const remaining = MAX_STREAM_CAPTURE_BYTES - stderrBytes;
|
|
95
|
-
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
96
|
-
stderr += slice.toString("utf8");
|
|
97
|
-
stderrBytes += slice.length;
|
|
98
|
-
});
|
|
99
|
-
child.on("error", (err) => {
|
|
100
|
-
if (settled)
|
|
101
|
-
return;
|
|
102
|
-
settled = true;
|
|
103
|
-
clearTimeout(timer);
|
|
104
|
-
resolveP({
|
|
105
|
-
argv,
|
|
106
|
-
exitCode: null,
|
|
107
|
-
stdoutFull: stdout,
|
|
108
|
-
stderrFull: stderr,
|
|
109
|
-
durationMs: Date.now() - start,
|
|
110
|
-
launchError: err.message
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
child.on("close", (code) => {
|
|
114
|
-
if (settled)
|
|
115
|
-
return;
|
|
116
|
-
settled = true;
|
|
117
|
-
clearTimeout(timer);
|
|
118
|
-
resolveP({
|
|
119
|
-
argv,
|
|
120
|
-
exitCode: typeof code === "number" ? code : null,
|
|
121
|
-
stdoutFull: stdout,
|
|
122
|
-
stderrFull: stderr,
|
|
123
|
-
durationMs: Date.now() - start,
|
|
124
|
-
timedOut
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
function resolveStepCwd(cwd, runDir) {
|
|
131
|
-
if (isAbsolute(cwd)) {
|
|
132
|
-
return runDir;
|
|
133
|
-
}
|
|
134
|
-
const resolved = resolve(runDir, cwd);
|
|
135
|
-
if (!resolved.startsWith(runDir))
|
|
136
|
-
return runDir;
|
|
137
|
-
return resolved;
|
|
138
|
-
}
|
|
139
|
-
var DEFAULT_DOCKER_SHELL_IMAGE = "alpine:3.20";
|
|
140
|
-
var DEFAULT_DOCKER_HTTP_IMAGE = "curlimages/curl:8.12.1";
|
|
141
|
-
var DOCKER_HTTP_STATUS_MARKER = "\n__ZERO_HTTP_STATUS__:";
|
|
142
|
-
var CONTAINER_ID_RE = /^[a-f0-9]{12,64}$/i;
|
|
143
|
-
var DOCKER_NETWORK_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
144
|
-
var DockerRunner = class {
|
|
145
|
-
kind = "docker";
|
|
146
|
-
dockerBinary;
|
|
147
|
-
shellImage;
|
|
148
|
-
httpImage;
|
|
149
|
-
network;
|
|
150
|
-
memoryMb;
|
|
151
|
-
pidsLimit;
|
|
152
|
-
cpus;
|
|
153
|
-
constructor(options = {}) {
|
|
154
|
-
this.dockerBinary = options.dockerBinary ?? "docker";
|
|
155
|
-
this.shellImage = options.shellImage ?? DEFAULT_DOCKER_SHELL_IMAGE;
|
|
156
|
-
this.httpImage = options.httpImage ?? DEFAULT_DOCKER_HTTP_IMAGE;
|
|
157
|
-
this.network = options.network ?? "none";
|
|
158
|
-
this.memoryMb = options.memoryMb ?? 256;
|
|
159
|
-
this.pidsLimit = options.pidsLimit ?? 128;
|
|
160
|
-
this.cpus = options.cpus ?? 1;
|
|
161
|
-
if (!this.dockerBinary || !this.shellImage || !this.httpImage || !Number.isInteger(this.memoryMb) || this.memoryMb < 16 || !Number.isInteger(this.pidsLimit) || this.pidsLimit < 1 || !Number.isFinite(this.cpus) || this.cpus <= 0) {
|
|
162
|
-
throw new Error("invalid DockerRunner configuration");
|
|
163
|
-
}
|
|
164
|
-
if (this.network !== "none" && this.network !== "bridge" && (!DOCKER_NETWORK_RE.test(this.network) || this.network === "host")) {
|
|
165
|
-
throw new Error("DockerRunner network must be none, bridge, or a simple custom network name (host is refused)");
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
async exec(step, ctx) {
|
|
169
|
-
const startedAt = Date.now();
|
|
170
|
-
const preflightError = this.networkPreflight(step, ctx.scope);
|
|
171
|
-
if (preflightError) {
|
|
172
|
-
return failedStep(step, startedAt, preflightError);
|
|
173
|
-
}
|
|
174
|
-
const command = this.commandForStep(step, ctx.stepTimeoutMs);
|
|
175
|
-
if (typeof command === "string") {
|
|
176
|
-
return failedStep(step, startedAt, command);
|
|
177
|
-
}
|
|
178
|
-
const safeStepId = step.id.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 64) || "step";
|
|
179
|
-
const cidPath = join(ctx.runDir, `.0-docker-${safeStepId}-${randomUUID()}.cid`);
|
|
180
|
-
const args = this.dockerRunArgs(command, ctx.runDir, cidPath);
|
|
181
|
-
const result = await runDockerCommand({
|
|
182
|
-
dockerBinary: this.dockerBinary,
|
|
183
|
-
args,
|
|
184
|
-
runDir: ctx.runDir,
|
|
185
|
-
cidPath,
|
|
186
|
-
stepTimeoutMs: ctx.stepTimeoutMs
|
|
187
|
-
});
|
|
188
|
-
if (!command.parseHttpStatus)
|
|
189
|
-
return result;
|
|
190
|
-
const parsed = parseDockerHttpStatus(result.stdoutFull);
|
|
191
|
-
return {
|
|
192
|
-
...result,
|
|
193
|
-
stdoutFull: parsed.stdout,
|
|
194
|
-
...parsed.httpStatus === void 0 ? {} : { httpStatus: parsed.httpStatus }
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
networkPreflight(step, scope) {
|
|
198
|
-
if (this.network === "none")
|
|
199
|
-
return void 0;
|
|
200
|
-
if (step.action.type !== "http") {
|
|
201
|
-
return "networked Docker replay only permits declarative HTTP steps; shell and docker actions always run with network=none";
|
|
202
|
-
}
|
|
203
|
-
let parsed;
|
|
204
|
-
try {
|
|
205
|
-
parsed = new URL(step.action.url);
|
|
206
|
-
} catch {
|
|
207
|
-
return `invalid HTTP replay URL: ${step.action.url}`;
|
|
208
|
-
}
|
|
209
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
210
|
-
return `Docker HTTP replay only permits http(s) URLs, got ${parsed.protocol}`;
|
|
211
|
-
}
|
|
212
|
-
if (!scope) {
|
|
213
|
-
return "networked Docker replay requires an explicit engagement scope";
|
|
214
|
-
}
|
|
215
|
-
const scopeMatch = scope.match(step.action.url);
|
|
216
|
-
return scopeMatch.allowed ? void 0 : `Docker replay refused ${step.action.url}: ${scopeMatch.reason}`;
|
|
217
|
-
}
|
|
218
|
-
commandForStep(step, stepTimeoutMs) {
|
|
219
|
-
switch (step.action.type) {
|
|
220
|
-
case "shell": {
|
|
221
|
-
const cwd = step.action.cwd ?? ".";
|
|
222
|
-
const workdir = posix.resolve("/work", cwd);
|
|
223
|
-
if (isAbsolute(cwd) || win32.isAbsolute(cwd) || /[\\\0]/.test(cwd) || workdir !== "/work" && !workdir.startsWith("/work/")) {
|
|
224
|
-
return "Docker shell cwd must be relative and remain inside the replay workspace";
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
image: this.shellImage,
|
|
228
|
-
command: ["/bin/sh", "-lc", step.action.cmd],
|
|
229
|
-
parseHttpStatus: false,
|
|
230
|
-
workdir
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
case "docker":
|
|
234
|
-
if (!step.action.image.trim())
|
|
235
|
-
return "Docker action is missing an image";
|
|
236
|
-
if (step.action.image.trimStart().startsWith("-"))
|
|
237
|
-
return "Docker image must not be a command-line option";
|
|
238
|
-
return {
|
|
239
|
-
image: step.action.image,
|
|
240
|
-
// These arguments come after the image and therefore cannot alter
|
|
241
|
-
// Docker's hardening flags (for example, they cannot add --privileged).
|
|
242
|
-
command: step.action.args,
|
|
243
|
-
parseHttpStatus: false
|
|
244
|
-
};
|
|
245
|
-
case "http": {
|
|
246
|
-
const method = step.action.method.trim().toUpperCase();
|
|
247
|
-
if (!/^[A-Z]+$/.test(method)) {
|
|
248
|
-
return `invalid HTTP method for Docker replay: ${step.action.method}`;
|
|
249
|
-
}
|
|
250
|
-
const headers = Object.entries(step.action.headers ?? {});
|
|
251
|
-
if (headers.some(([name, value]) => !name.trim() || /[\r\n]/.test(name) || /[\r\n]/.test(value))) {
|
|
252
|
-
return "HTTP replay headers must be non-empty single-line values";
|
|
253
|
-
}
|
|
254
|
-
const command = [
|
|
255
|
-
"--silent",
|
|
256
|
-
"--show-error",
|
|
257
|
-
"--request",
|
|
258
|
-
method,
|
|
259
|
-
"--max-time",
|
|
260
|
-
String(Math.max(1, Math.ceil(stepTimeoutMs / 1e3))),
|
|
261
|
-
"--output",
|
|
262
|
-
"-",
|
|
263
|
-
"--write-out",
|
|
264
|
-
`${DOCKER_HTTP_STATUS_MARKER}%{http_code}
|
|
265
|
-
`
|
|
266
|
-
];
|
|
267
|
-
for (const [name, value] of headers) {
|
|
268
|
-
command.push("--header", `${name}: ${value}`);
|
|
269
|
-
}
|
|
270
|
-
if (step.action.body !== void 0) {
|
|
271
|
-
command.push("--data-binary", step.action.body);
|
|
272
|
-
}
|
|
273
|
-
command.push(step.action.url);
|
|
274
|
-
return {
|
|
275
|
-
image: this.httpImage,
|
|
276
|
-
dockerOptions: ["--entrypoint", "curl"],
|
|
277
|
-
command,
|
|
278
|
-
parseHttpStatus: true
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
case "note":
|
|
282
|
-
return `DockerRunner only executes shell, docker, and http steps; got '${step.action.type}'`;
|
|
283
|
-
default: {
|
|
284
|
-
const _exhaustive = step.action;
|
|
285
|
-
void _exhaustive;
|
|
286
|
-
return "unsupported Docker replay action";
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
dockerRunArgs(command, runDir, cidPath) {
|
|
291
|
-
const args = [
|
|
292
|
-
"run",
|
|
293
|
-
"--rm",
|
|
294
|
-
"--pull",
|
|
295
|
-
"never",
|
|
296
|
-
"--init",
|
|
297
|
-
"--read-only",
|
|
298
|
-
"--cap-drop",
|
|
299
|
-
"ALL",
|
|
300
|
-
"--security-opt",
|
|
301
|
-
"no-new-privileges:true",
|
|
302
|
-
"--pids-limit",
|
|
303
|
-
String(this.pidsLimit),
|
|
304
|
-
"--memory",
|
|
305
|
-
`${this.memoryMb}m`,
|
|
306
|
-
"--cpus",
|
|
307
|
-
String(this.cpus),
|
|
308
|
-
"--network",
|
|
309
|
-
this.network,
|
|
310
|
-
"--workdir",
|
|
311
|
-
command.workdir ?? "/work",
|
|
312
|
-
"--tmpfs",
|
|
313
|
-
"/tmp:rw,noexec,nosuid,size=64m",
|
|
314
|
-
"--mount",
|
|
315
|
-
`type=bind,src=${resolve(runDir)},dst=/work`,
|
|
316
|
-
"--cidfile",
|
|
317
|
-
cidPath,
|
|
318
|
-
...command.dockerOptions ?? []
|
|
319
|
-
];
|
|
320
|
-
if (typeof process.getuid === "function" && typeof process.getgid === "function") {
|
|
321
|
-
args.push("--user", `${process.getuid()}:${process.getgid()}`);
|
|
322
|
-
}
|
|
323
|
-
args.push("--", command.image, ...command.command);
|
|
324
|
-
return args;
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
function failedStep(step, startedAt, launchError) {
|
|
328
|
-
return {
|
|
329
|
-
argv: argvForStep(step),
|
|
330
|
-
exitCode: null,
|
|
331
|
-
stdoutFull: "",
|
|
332
|
-
stderrFull: "",
|
|
333
|
-
durationMs: Date.now() - startedAt,
|
|
334
|
-
launchError
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
function parseDockerHttpStatus(stdout) {
|
|
338
|
-
const markerIndex = stdout.lastIndexOf(DOCKER_HTTP_STATUS_MARKER);
|
|
339
|
-
if (markerIndex === -1)
|
|
340
|
-
return { stdout };
|
|
341
|
-
const status = /^(\d{3})\r?\n?$/.exec(stdout.slice(markerIndex + DOCKER_HTTP_STATUS_MARKER.length));
|
|
342
|
-
if (!status)
|
|
343
|
-
return { stdout };
|
|
344
|
-
return {
|
|
345
|
-
stdout: stdout.slice(0, markerIndex),
|
|
346
|
-
httpStatus: Number(status[1])
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
async function runDockerCommand(args) {
|
|
350
|
-
const startedAt = Date.now();
|
|
351
|
-
const { promise, resolve: resolveResult } = promiseWithResolvers.withResolvers();
|
|
352
|
-
let timer;
|
|
353
|
-
const argv = [args.dockerBinary, ...args.args];
|
|
354
|
-
let stdout = "";
|
|
355
|
-
let stderr = "";
|
|
356
|
-
let stdoutBytes = 0;
|
|
357
|
-
let stderrBytes = 0;
|
|
358
|
-
let timedOut = false;
|
|
359
|
-
let settled = false;
|
|
360
|
-
let timeoutCleanup;
|
|
361
|
-
let child;
|
|
362
|
-
const finish = async (exitCode, launchError) => {
|
|
363
|
-
if (settled)
|
|
364
|
-
return;
|
|
365
|
-
settled = true;
|
|
366
|
-
clearTimeout(timer);
|
|
367
|
-
if (timeoutCleanup)
|
|
368
|
-
await timeoutCleanup;
|
|
369
|
-
const containerStarted = existsSync(args.cidPath);
|
|
370
|
-
rmSync(args.cidPath, { force: true });
|
|
371
|
-
const startError = launchError ?? (exitCode !== 0 && !timedOut && !containerStarted ? `Docker sandbox did not start (exit ${exitCode ?? "unknown"}): ${excerpt(stderr, 512)}` : void 0);
|
|
372
|
-
resolveResult({
|
|
373
|
-
argv,
|
|
374
|
-
exitCode,
|
|
375
|
-
stdoutFull: stdout,
|
|
376
|
-
stderrFull: stderr,
|
|
377
|
-
durationMs: Date.now() - startedAt,
|
|
378
|
-
...timedOut ? { timedOut: true } : {},
|
|
379
|
-
...startError ? { launchError: startError } : {}
|
|
380
|
-
});
|
|
381
|
-
};
|
|
382
|
-
try {
|
|
383
|
-
child = spawn(args.dockerBinary, args.args, {
|
|
384
|
-
cwd: args.runDir,
|
|
385
|
-
env: allowlistedChildEnv({ "ZERO_VERIFY": "1" }),
|
|
386
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
387
|
-
detached: nodePlatform !== "win32"
|
|
388
|
-
});
|
|
389
|
-
} catch (err) {
|
|
390
|
-
void finish(null, err instanceof Error ? err.message : String(err));
|
|
391
|
-
return promise;
|
|
392
|
-
}
|
|
393
|
-
timer = setTimeout(() => {
|
|
394
|
-
timedOut = true;
|
|
395
|
-
timeoutCleanup = stopDockerContainer(args.dockerBinary, args.cidPath, args.runDir).finally(() => killProcessGroup(child));
|
|
396
|
-
}, args.stepTimeoutMs);
|
|
397
|
-
child.stdout?.on("data", (chunk) => {
|
|
398
|
-
if (stdoutBytes >= MAX_STREAM_CAPTURE_BYTES)
|
|
399
|
-
return;
|
|
400
|
-
const remaining = MAX_STREAM_CAPTURE_BYTES - stdoutBytes;
|
|
401
|
-
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
402
|
-
stdout += slice.toString("utf8");
|
|
403
|
-
stdoutBytes += slice.length;
|
|
404
|
-
});
|
|
405
|
-
child.stderr?.on("data", (chunk) => {
|
|
406
|
-
if (stderrBytes >= MAX_STREAM_CAPTURE_BYTES)
|
|
407
|
-
return;
|
|
408
|
-
const remaining = MAX_STREAM_CAPTURE_BYTES - stderrBytes;
|
|
409
|
-
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
410
|
-
stderr += slice.toString("utf8");
|
|
411
|
-
stderrBytes += slice.length;
|
|
412
|
-
});
|
|
413
|
-
child.on("error", (err) => {
|
|
414
|
-
void finish(null, err.message);
|
|
415
|
-
});
|
|
416
|
-
child.on("close", (code) => {
|
|
417
|
-
void finish(typeof code === "number" ? code : null);
|
|
418
|
-
});
|
|
419
|
-
return promise;
|
|
420
|
-
}
|
|
421
|
-
async function stopDockerContainer(dockerBinary, cidPath, runDir) {
|
|
422
|
-
let containerId;
|
|
423
|
-
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
424
|
-
try {
|
|
425
|
-
const candidate = readFileSync(cidPath, "utf8").trim();
|
|
426
|
-
if (CONTAINER_ID_RE.test(candidate)) {
|
|
427
|
-
containerId = candidate;
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
} catch {
|
|
431
|
-
}
|
|
432
|
-
const { promise, resolve: resolve2 } = promiseWithResolvers.withResolvers();
|
|
433
|
-
setTimeout(resolve2, 25);
|
|
434
|
-
await promise;
|
|
435
|
-
}
|
|
436
|
-
if (!containerId)
|
|
437
|
-
return;
|
|
438
|
-
await runDockerControl(dockerBinary, ["kill", containerId], runDir);
|
|
439
|
-
await runDockerControl(dockerBinary, ["rm", "--force", containerId], runDir);
|
|
440
|
-
}
|
|
441
|
-
function runDockerControl(dockerBinary, args, cwd) {
|
|
442
|
-
const { promise, resolve: resolve2 } = promiseWithResolvers.withResolvers();
|
|
443
|
-
let child;
|
|
444
|
-
try {
|
|
445
|
-
child = spawn(dockerBinary, args, {
|
|
446
|
-
cwd,
|
|
447
|
-
env: allowlistedChildEnv({ "ZERO_VERIFY": "1" }),
|
|
448
|
-
stdio: "ignore",
|
|
449
|
-
detached: nodePlatform !== "win32"
|
|
450
|
-
});
|
|
451
|
-
} catch {
|
|
452
|
-
resolve2();
|
|
453
|
-
return promise;
|
|
454
|
-
}
|
|
455
|
-
const timer = setTimeout(() => {
|
|
456
|
-
killProcessGroup(child);
|
|
457
|
-
}, 5e3);
|
|
458
|
-
child.once("error", () => {
|
|
459
|
-
clearTimeout(timer);
|
|
460
|
-
resolve2();
|
|
461
|
-
});
|
|
462
|
-
child.once("close", () => {
|
|
463
|
-
clearTimeout(timer);
|
|
464
|
-
resolve2();
|
|
465
|
-
});
|
|
466
|
-
return promise;
|
|
467
|
-
}
|
|
468
|
-
function killProcessGroup(child) {
|
|
469
|
-
try {
|
|
470
|
-
if (nodePlatform !== "win32" && child.pid) {
|
|
471
|
-
process.kill(-child.pid, "SIGKILL");
|
|
472
|
-
} else {
|
|
473
|
-
child.kill("SIGKILL");
|
|
474
|
-
}
|
|
475
|
-
} catch {
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
var QemuRunner = class {
|
|
479
|
-
kind = "qemu";
|
|
480
|
-
qemuBinary;
|
|
481
|
-
kernelImage;
|
|
482
|
-
busyboxPath;
|
|
483
|
-
memoryMb;
|
|
484
|
-
cpus;
|
|
485
|
-
constructor(options = {}) {
|
|
486
|
-
this.qemuBinary = options.qemuBinary ?? process.env["ZERO_REPLAY_QEMU_BINARY"]?.trim() ?? (nodeArch === "arm64" ? "qemu-system-aarch64" : "qemu-system-x86_64");
|
|
487
|
-
this.kernelImage = options.kernelImage ?? process.env["ZERO_REPLAY_QEMU_KERNEL"]?.trim() ?? "";
|
|
488
|
-
this.busyboxPath = options.busyboxPath ?? process.env["ZERO_REPLAY_QEMU_BUSYBOX"]?.trim() ?? "";
|
|
489
|
-
this.memoryMb = options.memoryMb ?? 512;
|
|
490
|
-
this.cpus = options.cpus ?? 1;
|
|
491
|
-
if (!this.qemuBinary || !Number.isInteger(this.memoryMb) || this.memoryMb < 128 || !Number.isInteger(this.cpus) || this.cpus < 1) {
|
|
492
|
-
throw new Error("invalid QemuRunner configuration");
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
async exec(step, ctx) {
|
|
496
|
-
const startedAt = Date.now();
|
|
497
|
-
if (!this.kernelImage || !this.busyboxPath) {
|
|
498
|
-
return failedStep(step, startedAt, "QEMU replay requires kernelImage and busyboxPath (or ZERO_REPLAY_QEMU_KERNEL and ZERO_REPLAY_QEMU_BUSYBOX)");
|
|
499
|
-
}
|
|
500
|
-
if (!existsSync(this.kernelImage) || !statSync(this.kernelImage).isFile()) {
|
|
501
|
-
return failedStep(step, startedAt, `QEMU kernel image is not a file: ${this.kernelImage}`);
|
|
502
|
-
}
|
|
503
|
-
if (!existsSync(this.busyboxPath) || !statSync(this.busyboxPath).isFile()) {
|
|
504
|
-
return failedStep(step, startedAt, `QEMU BusyBox binary is not a file: ${this.busyboxPath}`);
|
|
505
|
-
}
|
|
506
|
-
if (step.action.type !== "shell") {
|
|
507
|
-
return failedStep(step, startedAt, `QemuRunner only executes shell steps in an offline guest; got '${step.action.type}'`);
|
|
508
|
-
}
|
|
509
|
-
const guestCwd = qemuGuestWorkingDirectory(step.action.cwd);
|
|
510
|
-
if (!guestCwd) {
|
|
511
|
-
return failedStep(step, startedAt, "QEMU replay refuses an absolute step cwd");
|
|
512
|
-
}
|
|
513
|
-
const safeStepId = step.id.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 64) || "step";
|
|
514
|
-
const workspaceName = `.0-qemu-${safeStepId}-${randomUUID()}`;
|
|
515
|
-
const workspace = join(ctx.runDir, workspaceName);
|
|
516
|
-
mkdirSync(workspace, { recursive: true });
|
|
517
|
-
writeFileSync(join(workspace, "step.sh"), step.action.cmd, "utf8");
|
|
518
|
-
const initrdPath = buildQemuInitramfs({
|
|
519
|
-
runDir: ctx.runDir,
|
|
520
|
-
busyboxPath: this.busyboxPath,
|
|
521
|
-
workspaceName,
|
|
522
|
-
guestCwd
|
|
523
|
-
});
|
|
524
|
-
const args = this.qemuArgs(ctx.runDir, initrdPath);
|
|
525
|
-
const qemu = await runQemuCommand({
|
|
526
|
-
qemuBinary: this.qemuBinary,
|
|
527
|
-
args,
|
|
528
|
-
runDir: ctx.runDir,
|
|
529
|
-
stepTimeoutMs: ctx.stepTimeoutMs
|
|
530
|
-
});
|
|
531
|
-
const stdout = readQemuCapture(join(workspace, "stdout.log"));
|
|
532
|
-
const guestStderr = readQemuCapture(join(workspace, "stderr.log"));
|
|
533
|
-
const guestExitCode = readQemuExitCode(join(workspace, "exit-code"));
|
|
534
|
-
const serial = qemu.stdoutFull ? `
|
|
535
|
-
[qemu serial]
|
|
536
|
-
${qemu.stdoutFull}` : "";
|
|
537
|
-
const stderr = `${guestStderr}${qemu.stderrFull}${serial}`;
|
|
538
|
-
const missingExitMarker = !qemu.timedOut && guestExitCode === null ? "QEMU guest did not write an exit marker; inspect serial evidence" : void 0;
|
|
539
|
-
return {
|
|
540
|
-
argv: [this.qemuBinary, ...args],
|
|
541
|
-
exitCode: guestExitCode,
|
|
542
|
-
stdoutFull: stdout,
|
|
543
|
-
stderrFull: stderr,
|
|
544
|
-
durationMs: qemu.durationMs,
|
|
545
|
-
...qemu.timedOut ? { timedOut: true } : {},
|
|
546
|
-
...qemu.launchError || missingExitMarker ? { launchError: qemu.launchError ?? missingExitMarker } : {}
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
qemuArgs(runDir, initrdPath) {
|
|
550
|
-
return [
|
|
551
|
-
"-nodefaults",
|
|
552
|
-
"-no-reboot",
|
|
553
|
-
"-display",
|
|
554
|
-
"none",
|
|
555
|
-
"-monitor",
|
|
556
|
-
"none",
|
|
557
|
-
"-serial",
|
|
558
|
-
"stdio",
|
|
559
|
-
"-m",
|
|
560
|
-
String(this.memoryMb),
|
|
561
|
-
"-smp",
|
|
562
|
-
String(this.cpus),
|
|
563
|
-
"-kernel",
|
|
564
|
-
this.kernelImage,
|
|
565
|
-
"-initrd",
|
|
566
|
-
initrdPath,
|
|
567
|
-
"-append",
|
|
568
|
-
"console=ttyS0 rdinit=/init panic=-1",
|
|
569
|
-
"-virtfs",
|
|
570
|
-
`local,path=${resolve(runDir)},mount_tag=0-replay,security_model=none,id=osecshare`,
|
|
571
|
-
"-net",
|
|
572
|
-
"none",
|
|
573
|
-
"-sandbox",
|
|
574
|
-
"on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny"
|
|
575
|
-
];
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
function qemuGuestWorkingDirectory(cwd) {
|
|
579
|
-
if (!cwd)
|
|
580
|
-
return "/mnt/0";
|
|
581
|
-
if (isAbsolute(cwd))
|
|
582
|
-
return void 0;
|
|
583
|
-
const relative = resolve("/", cwd).slice(1);
|
|
584
|
-
return relative ? `/mnt/0/${relative}` : "/mnt/0";
|
|
585
|
-
}
|
|
586
|
-
function buildQemuInitramfs(args) {
|
|
587
|
-
const empty = Buffer.alloc(0);
|
|
588
|
-
const init = Buffer.from(renderQemuInit(args.workspaceName, args.guestCwd), "utf8");
|
|
589
|
-
const entries = [
|
|
590
|
-
{ name: "bin", mode: 16877, body: empty },
|
|
591
|
-
{ name: "dev", mode: 16877, body: empty },
|
|
592
|
-
{ name: "mnt", mode: 16877, body: empty },
|
|
593
|
-
{ name: "mnt/0", mode: 16877, body: empty },
|
|
594
|
-
{ name: "proc", mode: 16877, body: empty },
|
|
595
|
-
{ name: "sys", mode: 16877, body: empty },
|
|
596
|
-
{ name: "tmp", mode: 16877, body: empty },
|
|
597
|
-
{ name: "bin/busybox", mode: 33261, body: readFileSync(args.busyboxPath) },
|
|
598
|
-
{ name: "init", mode: 33261, body: init }
|
|
599
|
-
];
|
|
600
|
-
const chunks = [];
|
|
601
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
602
|
-
appendNewcEntry(chunks, entries[index], index + 1);
|
|
603
|
-
}
|
|
604
|
-
appendNewcEntry(chunks, { name: "TRAILER!!!", mode: 0, body: empty }, 0);
|
|
605
|
-
const initrdPath = join(args.runDir, `.0-qemu-initrd-${randomUUID()}.cpio.gz`);
|
|
606
|
-
writeFileSync(initrdPath, gzipSync(Buffer.concat(chunks)));
|
|
607
|
-
return initrdPath;
|
|
608
|
-
}
|
|
609
|
-
function appendNewcEntry(chunks, entry, inode) {
|
|
610
|
-
const name = Buffer.from(`${entry.name}\0`, "utf8");
|
|
611
|
-
const fields = [
|
|
612
|
-
inode,
|
|
613
|
-
entry.mode,
|
|
614
|
-
0,
|
|
615
|
-
0,
|
|
616
|
-
1,
|
|
617
|
-
0,
|
|
618
|
-
entry.body.length,
|
|
619
|
-
0,
|
|
620
|
-
0,
|
|
621
|
-
0,
|
|
622
|
-
0,
|
|
623
|
-
name.length,
|
|
624
|
-
0
|
|
625
|
-
];
|
|
626
|
-
const header = Buffer.from(`070701${fields.map((field) => field.toString(16).padStart(8, "0")).join("")}`, "ascii");
|
|
627
|
-
chunks.push(header, name);
|
|
628
|
-
const headerPadding = (4 - (header.length + name.length) % 4) % 4;
|
|
629
|
-
if (headerPadding > 0)
|
|
630
|
-
chunks.push(Buffer.alloc(headerPadding));
|
|
631
|
-
chunks.push(entry.body);
|
|
632
|
-
const bodyPadding = (4 - entry.body.length % 4) % 4;
|
|
633
|
-
if (bodyPadding > 0)
|
|
634
|
-
chunks.push(Buffer.alloc(bodyPadding));
|
|
635
|
-
}
|
|
636
|
-
function renderQemuInit(workspaceName, guestCwd) {
|
|
637
|
-
const workspace = `/mnt/0/${workspaceName}`;
|
|
638
|
-
const step = `${workspace}/step.sh`;
|
|
639
|
-
const stdout = `${workspace}/stdout.log`;
|
|
640
|
-
const stderr = `${workspace}/stderr.log`;
|
|
641
|
-
const exitCode = `${workspace}/exit-code`;
|
|
642
|
-
return [
|
|
643
|
-
"#!/bin/busybox sh",
|
|
644
|
-
"/bin/busybox mkdir -p /proc /sys /dev /tmp /mnt/0",
|
|
645
|
-
"/bin/busybox mount -t proc proc /proc",
|
|
646
|
-
"/bin/busybox mount -t sysfs sysfs /sys",
|
|
647
|
-
"/bin/busybox mount -t devtmpfs devtmpfs /dev 2>/dev/null || true",
|
|
648
|
-
"if ! /bin/busybox mount -t 9p -o trans=virtio,version=9p2000.L 0-replay /mnt/0; then",
|
|
649
|
-
' echo "__ZERO_QEMU_MOUNT_FAILED__"',
|
|
650
|
-
" /bin/busybox poweroff -f",
|
|
651
|
-
"fi",
|
|
652
|
-
`(
|
|
653
|
-
cd ${shellQuote(guestCwd)} || exit 125
|
|
654
|
-
/bin/busybox sh ${shellQuote(step)}
|
|
655
|
-
) > ${shellQuote(stdout)} 2> ${shellQuote(stderr)}`,
|
|
656
|
-
"rc=$?",
|
|
657
|
-
`printf '%s\\n' "$rc" > ${shellQuote(exitCode)}`,
|
|
658
|
-
"/bin/busybox sync",
|
|
659
|
-
"/bin/busybox poweroff -f"
|
|
660
|
-
].join("\n");
|
|
661
|
-
}
|
|
662
|
-
function shellQuote(value) {
|
|
663
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
664
|
-
}
|
|
665
|
-
function readQemuCapture(path) {
|
|
666
|
-
try {
|
|
667
|
-
return readFileSync(path).subarray(0, MAX_STREAM_CAPTURE_BYTES).toString("utf8");
|
|
668
|
-
} catch {
|
|
669
|
-
return "";
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
function readQemuExitCode(path) {
|
|
673
|
-
try {
|
|
674
|
-
const raw = readFileSync(path, "utf8").trim();
|
|
675
|
-
if (!/^(0|[1-9][0-9]{0,2})$/.test(raw))
|
|
676
|
-
return null;
|
|
677
|
-
const value = Number(raw);
|
|
678
|
-
return value <= 255 ? value : null;
|
|
679
|
-
} catch {
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
function runQemuCommand(args) {
|
|
684
|
-
const startedAt = Date.now();
|
|
685
|
-
const { promise, resolve: resolveResult } = promiseWithResolvers.withResolvers();
|
|
686
|
-
let timer;
|
|
687
|
-
let stdout = "";
|
|
688
|
-
let stderr = "";
|
|
689
|
-
let stdoutBytes = 0;
|
|
690
|
-
let stderrBytes = 0;
|
|
691
|
-
let timedOut = false;
|
|
692
|
-
let settled = false;
|
|
693
|
-
let child;
|
|
694
|
-
const finish = (exitCode, launchError) => {
|
|
695
|
-
if (settled)
|
|
696
|
-
return;
|
|
697
|
-
settled = true;
|
|
698
|
-
clearTimeout(timer);
|
|
699
|
-
resolveResult({
|
|
700
|
-
exitCode,
|
|
701
|
-
stdoutFull: stdout,
|
|
702
|
-
stderrFull: stderr,
|
|
703
|
-
durationMs: Date.now() - startedAt,
|
|
704
|
-
...timedOut ? { timedOut: true } : {},
|
|
705
|
-
...launchError ? { launchError } : {}
|
|
706
|
-
});
|
|
707
|
-
};
|
|
708
|
-
try {
|
|
709
|
-
child = spawn(args.qemuBinary, args.args, {
|
|
710
|
-
cwd: args.runDir,
|
|
711
|
-
env: allowlistedChildEnv({ "ZERO_VERIFY": "1" }),
|
|
712
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
713
|
-
detached: nodePlatform !== "win32"
|
|
714
|
-
});
|
|
715
|
-
} catch (err) {
|
|
716
|
-
finish(null, err instanceof Error ? err.message : String(err));
|
|
717
|
-
return promise;
|
|
718
|
-
}
|
|
719
|
-
timer = setTimeout(() => {
|
|
720
|
-
timedOut = true;
|
|
721
|
-
killProcessGroup(child);
|
|
722
|
-
}, args.stepTimeoutMs);
|
|
723
|
-
child.stdout?.on("data", (chunk) => {
|
|
724
|
-
if (stdoutBytes >= MAX_STREAM_CAPTURE_BYTES)
|
|
725
|
-
return;
|
|
726
|
-
const remaining = MAX_STREAM_CAPTURE_BYTES - stdoutBytes;
|
|
727
|
-
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
728
|
-
stdout += slice.toString("utf8");
|
|
729
|
-
stdoutBytes += slice.length;
|
|
730
|
-
});
|
|
731
|
-
child.stderr?.on("data", (chunk) => {
|
|
732
|
-
if (stderrBytes >= MAX_STREAM_CAPTURE_BYTES)
|
|
733
|
-
return;
|
|
734
|
-
const remaining = MAX_STREAM_CAPTURE_BYTES - stderrBytes;
|
|
735
|
-
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
736
|
-
stderr += slice.toString("utf8");
|
|
737
|
-
stderrBytes += slice.length;
|
|
738
|
-
});
|
|
739
|
-
child.on("error", (err) => {
|
|
740
|
-
finish(null, err.message);
|
|
741
|
-
});
|
|
742
|
-
child.on("close", (code) => {
|
|
743
|
-
const exitCode = typeof code === "number" ? code : null;
|
|
744
|
-
finish(exitCode, exitCode !== 0 && !timedOut ? `QEMU exited before guest completion (exit ${exitCode ?? "unknown"})` : void 0);
|
|
745
|
-
});
|
|
746
|
-
return promise;
|
|
747
|
-
}
|
|
748
|
-
function argvForStep(step) {
|
|
749
|
-
switch (step.action.type) {
|
|
750
|
-
case "shell":
|
|
751
|
-
return ["/bin/sh", "-c", step.action.cmd];
|
|
752
|
-
case "docker":
|
|
753
|
-
return ["docker", "run", "--rm", ...step.action.args, step.action.image];
|
|
754
|
-
case "http":
|
|
755
|
-
return [step.action.method, step.action.url];
|
|
756
|
-
case "note":
|
|
757
|
-
return ["note", step.id];
|
|
758
|
-
default: {
|
|
759
|
-
const _exhaustive = step.action;
|
|
760
|
-
void _exhaustive;
|
|
761
|
-
return ["unknown"];
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
function assertionFromStepExpect(step, expect, result) {
|
|
766
|
-
switch (expect.type) {
|
|
767
|
-
case "exit-zero": {
|
|
768
|
-
const passed = result.exitCode === 0;
|
|
769
|
-
return {
|
|
770
|
-
kind: "exit_code",
|
|
771
|
-
target: step.id,
|
|
772
|
-
expected: 0,
|
|
773
|
-
actual: result.exitCode,
|
|
774
|
-
passed
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
case "http-status": {
|
|
778
|
-
const expectedStatuses = Array.isArray(expect.status) ? expect.status : [expect.status];
|
|
779
|
-
const actual = result.httpStatus ?? null;
|
|
780
|
-
return {
|
|
781
|
-
kind: "http_status",
|
|
782
|
-
target: step.id,
|
|
783
|
-
expected: Array.isArray(expect.status) ? expect.status.join(",") : expect.status,
|
|
784
|
-
actual,
|
|
785
|
-
passed: actual !== null && expectedStatuses.includes(actual)
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
case "body-contains": {
|
|
789
|
-
const haystack = result.stdoutFull;
|
|
790
|
-
const passed = haystack.includes(expect.text);
|
|
791
|
-
return {
|
|
792
|
-
kind: "string_in_output",
|
|
793
|
-
target: step.id,
|
|
794
|
-
expected: expect.text,
|
|
795
|
-
actual: passed ? expect.text : null,
|
|
796
|
-
passed
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
case "body-matches": {
|
|
800
|
-
let passed = false;
|
|
801
|
-
try {
|
|
802
|
-
passed = new RegExp(expect.pattern).test(result.stdoutFull);
|
|
803
|
-
} catch {
|
|
804
|
-
passed = false;
|
|
805
|
-
}
|
|
806
|
-
return {
|
|
807
|
-
kind: "string_in_output",
|
|
808
|
-
target: step.id,
|
|
809
|
-
expected: expect.pattern,
|
|
810
|
-
actual: passed ? "matched" : null,
|
|
811
|
-
passed
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
case "file-exists": {
|
|
815
|
-
const passed = existsSync(expect.path);
|
|
816
|
-
return {
|
|
817
|
-
kind: "file_exists",
|
|
818
|
-
target: expect.path,
|
|
819
|
-
expected: true,
|
|
820
|
-
actual: passed,
|
|
821
|
-
passed
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
default: {
|
|
825
|
-
const _exhaustive = expect;
|
|
826
|
-
void _exhaustive;
|
|
827
|
-
return {
|
|
828
|
-
kind: "string_in_output",
|
|
829
|
-
target: step.id,
|
|
830
|
-
expected: "",
|
|
831
|
-
actual: null,
|
|
832
|
-
passed: false
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
function evaluateAssertion(input, ctx) {
|
|
838
|
-
switch (input.kind) {
|
|
839
|
-
case "exit_code": {
|
|
840
|
-
const actual = ctx.lastExitCode;
|
|
841
|
-
return {
|
|
842
|
-
...input,
|
|
843
|
-
actual,
|
|
844
|
-
passed: actual === input.expected
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
case "string_in_output": {
|
|
848
|
-
const needle = String(input.expected);
|
|
849
|
-
const passed = ctx.aggregatedStdout.includes(needle);
|
|
850
|
-
return {
|
|
851
|
-
...input,
|
|
852
|
-
actual: passed ? needle : null,
|
|
853
|
-
passed
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
case "file_exists": {
|
|
857
|
-
const path = isAbsolute(input.target) ? input.target : join(ctx.runDir, input.target);
|
|
858
|
-
const passed = existsSync(path);
|
|
859
|
-
return {
|
|
860
|
-
...input,
|
|
861
|
-
actual: passed,
|
|
862
|
-
passed
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
case "http_status": {
|
|
866
|
-
const actual = ctx.lastHttpStatus;
|
|
867
|
-
const expectedStatuses = String(input.expected).split(",").map((status) => Number(status.trim())).filter((status) => Number.isInteger(status));
|
|
868
|
-
return {
|
|
869
|
-
...input,
|
|
870
|
-
actual,
|
|
871
|
-
passed: actual !== null && expectedStatuses.includes(actual)
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
default: {
|
|
875
|
-
const _exhaustive = input.kind;
|
|
876
|
-
void _exhaustive;
|
|
877
|
-
return {
|
|
878
|
-
...input,
|
|
879
|
-
actual: null,
|
|
880
|
-
passed: false
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
function excerpt(text, max = STREAM_EXCERPT_BYTES) {
|
|
886
|
-
if (!text)
|
|
887
|
-
return "";
|
|
888
|
-
if (Buffer.byteLength(text, "utf8") <= max)
|
|
889
|
-
return text;
|
|
890
|
-
const buf = Buffer.from(text, "utf8");
|
|
891
|
-
return buf.subarray(0, max).toString("utf8") + "\u2026[truncated]";
|
|
892
|
-
}
|
|
893
|
-
function sha256Hex(input) {
|
|
894
|
-
return createHash("sha256").update(input).digest("hex");
|
|
895
|
-
}
|
|
896
|
-
function persistArtifact(args) {
|
|
897
|
-
if (!args.body)
|
|
898
|
-
return null;
|
|
899
|
-
const artifactsDir = join(args.runDir, "artifacts");
|
|
900
|
-
mkdirSync(artifactsDir, { recursive: true });
|
|
901
|
-
const sha = sha256Hex(args.body);
|
|
902
|
-
const safeHint = args.filenameHint.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
903
|
-
const fname = `${safeHint}-${sha.slice(0, 16)}`;
|
|
904
|
-
const fullPath = join(artifactsDir, fname);
|
|
905
|
-
writeFileSync(fullPath, args.body, "utf8");
|
|
906
|
-
return {
|
|
907
|
-
kind: args.kind,
|
|
908
|
-
path: join("artifacts", fname),
|
|
909
|
-
sha256: sha,
|
|
910
|
-
bytes: statSync(fullPath).size
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
async function runDeterministicReplay(finding, opts = {}) {
|
|
914
|
-
const runner = opts.runner ?? new LocalShellRunner();
|
|
915
|
-
const stepTimeoutMs = opts.stepTimeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
|
|
916
|
-
const engineVersion = opts.engineVersion ?? VERSION;
|
|
917
|
-
const runDir = opts.runDir ?? mkdtempSync(join(tmpdir(), "0-replay-"));
|
|
918
|
-
mkdirSync(runDir, { recursive: true });
|
|
919
|
-
const startedAt = /* @__PURE__ */ new Date();
|
|
920
|
-
const startMs = Date.now();
|
|
921
|
-
const commands = [];
|
|
922
|
-
const assertions = [];
|
|
923
|
-
const evidenceArtifacts = [];
|
|
924
|
-
const steps = finding.pocSteps ?? [];
|
|
925
|
-
if (steps.length === 0) {
|
|
926
|
-
const completedAt2 = /* @__PURE__ */ new Date();
|
|
927
|
-
return {
|
|
928
|
-
runDir,
|
|
929
|
-
result: {
|
|
930
|
-
status: "skipped",
|
|
931
|
-
mode: "deterministic_replay",
|
|
932
|
-
finding_id: finding.id,
|
|
933
|
-
engine_version: engineVersion,
|
|
934
|
-
started_at: startedAt.toISOString(),
|
|
935
|
-
completed_at: completedAt2.toISOString(),
|
|
936
|
-
duration_ms: completedAt2.getTime() - startMs,
|
|
937
|
-
commands: [],
|
|
938
|
-
assertions: [],
|
|
939
|
-
evidence_artifacts: [],
|
|
940
|
-
engine_metadata: {
|
|
941
|
-
os: nodePlatform,
|
|
942
|
-
arch: nodeArch,
|
|
943
|
-
runner: runner.kind
|
|
944
|
-
},
|
|
945
|
-
summary: "no pocSteps to execute"
|
|
946
|
-
}
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
let lastExit = null;
|
|
950
|
-
let lastHttpStatus = null;
|
|
951
|
-
let aggregatedStdout = "";
|
|
952
|
-
let runnerLaunchError = null;
|
|
953
|
-
for (const step of steps) {
|
|
954
|
-
let stepResult;
|
|
955
|
-
try {
|
|
956
|
-
stepResult = await runner.exec(step, { runDir, stepTimeoutMs, scope: opts.scope });
|
|
957
|
-
} catch (err) {
|
|
958
|
-
stepResult = {
|
|
959
|
-
argv: argvForStep(step),
|
|
960
|
-
exitCode: null,
|
|
961
|
-
stdoutFull: "",
|
|
962
|
-
stderrFull: "",
|
|
963
|
-
durationMs: 0,
|
|
964
|
-
launchError: err instanceof Error ? err.message : String(err)
|
|
965
|
-
};
|
|
966
|
-
runnerLaunchError = stepResult.launchError ?? "runner exec failed";
|
|
967
|
-
}
|
|
968
|
-
lastExit = stepResult.exitCode;
|
|
969
|
-
aggregatedStdout += stepResult.stdoutFull;
|
|
970
|
-
if (stepResult.launchError) {
|
|
971
|
-
runnerLaunchError = stepResult.launchError;
|
|
972
|
-
}
|
|
973
|
-
lastHttpStatus = stepResult.httpStatus ?? null;
|
|
974
|
-
commands.push({
|
|
975
|
-
argv: stepResult.argv,
|
|
976
|
-
exit_code: stepResult.exitCode,
|
|
977
|
-
stdout_excerpt: excerpt(stepResult.stdoutFull),
|
|
978
|
-
stderr_excerpt: excerpt(stepResult.stderrFull),
|
|
979
|
-
duration_ms: stepResult.durationMs
|
|
980
|
-
});
|
|
981
|
-
const stdoutArt = persistArtifact({
|
|
982
|
-
runDir,
|
|
983
|
-
kind: "stdout",
|
|
984
|
-
filenameHint: `step-${step.id}.stdout`,
|
|
985
|
-
body: stepResult.stdoutFull
|
|
986
|
-
});
|
|
987
|
-
if (stdoutArt)
|
|
988
|
-
evidenceArtifacts.push(stdoutArt);
|
|
989
|
-
const stderrArt = persistArtifact({
|
|
990
|
-
runDir,
|
|
991
|
-
kind: "stderr",
|
|
992
|
-
filenameHint: `step-${step.id}.stderr`,
|
|
993
|
-
body: stepResult.stderrFull
|
|
994
|
-
});
|
|
995
|
-
if (stderrArt)
|
|
996
|
-
evidenceArtifacts.push(stderrArt);
|
|
997
|
-
if (step.expect) {
|
|
998
|
-
assertions.push(assertionFromStepExpect(step, step.expect, stepResult));
|
|
999
|
-
}
|
|
1000
|
-
if (runnerLaunchError)
|
|
1001
|
-
break;
|
|
1002
|
-
}
|
|
1003
|
-
for (const a of opts.assertions ?? []) {
|
|
1004
|
-
assertions.push(evaluateAssertion(a, {
|
|
1005
|
-
lastExitCode: lastExit,
|
|
1006
|
-
aggregatedStdout,
|
|
1007
|
-
runDir,
|
|
1008
|
-
lastHttpStatus
|
|
1009
|
-
}));
|
|
1010
|
-
}
|
|
1011
|
-
const completedAt = /* @__PURE__ */ new Date();
|
|
1012
|
-
const allAssertionsPassed = assertions.length === 0 ? false : assertions.every((a) => a.passed);
|
|
1013
|
-
const status = runnerLaunchError ? "error" : allAssertionsPassed ? "reproduced" : "not_reproduced";
|
|
1014
|
-
return {
|
|
1015
|
-
runDir,
|
|
1016
|
-
result: {
|
|
1017
|
-
status,
|
|
1018
|
-
mode: "deterministic_replay",
|
|
1019
|
-
finding_id: finding.id,
|
|
1020
|
-
engine_version: engineVersion,
|
|
1021
|
-
started_at: startedAt.toISOString(),
|
|
1022
|
-
completed_at: completedAt.toISOString(),
|
|
1023
|
-
duration_ms: completedAt.getTime() - startMs,
|
|
1024
|
-
commands,
|
|
1025
|
-
assertions,
|
|
1026
|
-
evidence_artifacts: evidenceArtifacts,
|
|
1027
|
-
engine_metadata: {
|
|
1028
|
-
os: nodePlatform,
|
|
1029
|
-
arch: nodeArch,
|
|
1030
|
-
runner: runner.kind
|
|
1031
|
-
},
|
|
1032
|
-
error_reason: runnerLaunchError ?? null,
|
|
1033
|
-
summary: summariseOutcome({
|
|
1034
|
-
status,
|
|
1035
|
-
ran: commands.length,
|
|
1036
|
-
total: steps.length,
|
|
1037
|
-
runnerLaunchError
|
|
1038
|
-
})
|
|
1039
|
-
}
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
function summariseOutcome(args) {
|
|
1043
|
-
switch (args.status) {
|
|
1044
|
-
case "reproduced":
|
|
1045
|
-
return `replay reproduced the finding (${args.ran}/${args.total} steps executed)`;
|
|
1046
|
-
case "not_reproduced":
|
|
1047
|
-
return `replay completed but assertions failed (${args.ran}/${args.total} steps executed)`;
|
|
1048
|
-
case "error":
|
|
1049
|
-
return args.runnerLaunchError ? `runner error: ${args.runnerLaunchError}` : "runner error";
|
|
1050
|
-
case "skipped":
|
|
1051
|
-
return "no pocSteps to execute";
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
export {
|
|
1056
|
-
STREAM_EXCERPT_BYTES,
|
|
1057
|
-
DEFAULT_STEP_TIMEOUT_MS,
|
|
1058
|
-
MAX_STREAM_CAPTURE_BYTES,
|
|
1059
|
-
LocalShellRunner,
|
|
1060
|
-
DEFAULT_DOCKER_SHELL_IMAGE,
|
|
1061
|
-
DEFAULT_DOCKER_HTTP_IMAGE,
|
|
1062
|
-
DockerRunner,
|
|
1063
|
-
QemuRunner,
|
|
1064
|
-
argvForStep,
|
|
1065
|
-
assertionFromStepExpect,
|
|
1066
|
-
evaluateAssertion,
|
|
1067
|
-
excerpt,
|
|
1068
|
-
persistArtifact,
|
|
1069
|
-
runDeterministicReplay
|
|
1070
|
-
};
|