0sec-cli 0.22.4 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/0.js +47 -41
- package/README.md +68 -60
- package/chunks/{adapt-loop-3CCOWE43.js → adapt-loop-V3JFWOKC.js} +4 -4
- package/chunks/{artifact-scraper-Z4UUIOH3.js → artifact-scraper-NYKUF2M5.js} +2 -2
- package/chunks/{assumption-mining-2UAKQB6V.js → assumption-mining-ZX3YVWEH.js} +17 -17
- package/chunks/backend-connections-H52NIVH2.js +23 -0
- package/chunks/{chunk-YED25PA4.js → chunk-3E54JYS4.js} +1 -1
- package/chunks/{chunk-FFHRANYC.js → chunk-3P3XXSIJ.js} +4 -88
- package/chunks/{chunk-5WAJGLCW.js → chunk-3ZNI7F42.js} +3 -3
- package/chunks/{chunk-OAS5SAVZ.js → chunk-4HGGNBKK.js} +703 -190
- package/chunks/{chunk-TM74SSHC.js → chunk-62ENS6XO.js} +1 -1
- package/chunks/{chunk-CPCHLL6A.js → chunk-65H3MRBF.js} +1 -1
- package/chunks/{chunk-6YZE552K.js → chunk-A2ADXAC5.js} +131 -405
- package/chunks/{chunk-4ZYYX5GY.js → chunk-A5QO7N3S.js} +1 -1
- package/chunks/{chunk-CF3MNS3O.js → chunk-AAA4PRWK.js} +2 -2
- package/chunks/{chunk-QVIRWJ45.js → chunk-BF5FIK7H.js} +3 -3
- package/chunks/{chunk-YAZARMJY.js → chunk-CP6HANBE.js} +2 -2
- package/chunks/{chunk-H6Q2QAIY.js → chunk-EH2WTNY7.js} +35 -35
- package/chunks/{chunk-ELI3BVIK.js → chunk-EJH2TNVH.js} +3 -3
- package/chunks/chunk-EWXOG6D3.js +1695 -0
- package/chunks/{chunk-RCRTSKQN.js → chunk-F344NYUN.js} +1 -1
- package/chunks/chunk-FXR3BT2G.js +226 -0
- package/chunks/{chunk-XODDLNBI.js → chunk-HFDSIHTE.js} +92 -13
- package/chunks/{chunk-BGX53365.js → chunk-I3OCJ2OA.js} +1 -1
- package/chunks/{chunk-C6JPA32H.js → chunk-ITES7BCY.js} +1 -1
- package/chunks/{chunk-SCWWLABR.js → chunk-JEYIWMGL.js} +1 -1
- package/chunks/{chunk-G2BE2FC7.js → chunk-K55B4OH4.js} +1 -1
- package/chunks/{chunk-GQ3DGHWE.js → chunk-KHPOSLCF.js} +2 -2
- package/chunks/chunk-LT7BFGXA.js +733 -0
- package/chunks/{chunk-6KFB6AYN.js → chunk-LTEE2JWK.js} +286 -15
- package/chunks/{chunk-DPUXXMJW.js → chunk-LVGZGG63.js} +1 -1
- package/chunks/{chunk-PS5E7LUQ.js → chunk-MQJVU2GZ.js} +6 -6
- package/chunks/{chunk-2WWAPKAW.js → chunk-NI6QYQSK.js} +3 -3
- package/chunks/{chunk-5JNTMWPK.js → chunk-NX4H6L4M.js} +2 -2
- package/chunks/{chunk-WHUAUFSX.js → chunk-O6Z4XPOM.js} +1 -1
- package/chunks/{chunk-RTBZ36BD.js → chunk-OLSKXTQZ.js} +2 -2
- package/chunks/{chunk-H4DH7COV.js → chunk-P7KDN5PK.js} +25 -15
- package/chunks/chunk-PIR6BNE7.js +83 -0
- package/chunks/{chunk-E3OOBVOK.js → chunk-QMICC5O2.js} +2 -2
- package/chunks/chunk-R35KKJA4.js +90 -0
- package/chunks/{chunk-L2CCO4KE.js → chunk-T7ZEVY5G.js} +26 -1
- package/chunks/{chunk-IXAHQICR.js → chunk-TPUJZ74W.js} +7 -2
- package/chunks/{chunk-WBRGXCU2.js → chunk-UZUELPFW.js} +1 -1
- package/chunks/{chunk-QWK4R3DB.js → chunk-V2BWXPZP.js} +6 -6
- package/chunks/{chunk-3BPX75OR.js → chunk-W5VNCB57.js} +1 -1
- package/chunks/{chunk-E74LXQFY.js → chunk-WIYNYNL2.js} +1 -1
- package/chunks/chunk-WYNZFNMF.js +457 -0
- package/chunks/{chunk-IHPTOTLL.js → chunk-XORZUR2H.js} +1 -1
- package/chunks/{chunk-2OQOQ2FZ.js → chunk-Y4HJRUFE.js} +20 -4
- package/chunks/{chunk-GVY3S5E5.js → chunk-YAEJAZYW.js} +2 -2
- package/chunks/{chunk-6WQDXWQC.js → chunk-YYS2H33A.js} +2 -2
- package/chunks/{codex-models-Z43V3427.js → codex-models-Y457ND6Q.js} +6 -6
- package/chunks/{commands-EUGF4FUZ.js → commands-PBDAOZKA.js} +1842 -2509
- package/chunks/{db-NVAW4WWR.js → db-TPSI552V.js} +3 -3
- package/chunks/deep-review-DKED6EN5.js +81 -0
- package/chunks/{dist-27BNG6WD.js → dist-4LGRIBOC.js} +44 -30
- package/chunks/{dist-7RUOKPRJ.js → dist-7Q5A6TQM.js} +2 -2
- package/chunks/{dist-NG2HT5XU.js → dist-RLPNLRZD.js} +53 -1
- package/chunks/{eval-runner-6Q3H52SP.js → eval-runner-RZKDOQ2G.js} +13 -13
- package/chunks/{exploit-agent-FCCR2HYX.js → exploit-agent-RMFYNG7H.js} +1 -1
- package/chunks/{exploit-autoclimb-X5ZS5FAO.js → exploit-autoclimb-2A2PQI4E.js} +1 -1
- package/chunks/{exploit-climb-3JHGEO36.js → exploit-climb-DEJ232QN.js} +2 -2
- package/chunks/{fix-NSNPU33B.js → fix-JGBW7XUN.js} +6 -6
- package/chunks/{harness-KXQOVIDQ.js → harness-CBOLMUHB.js} +3 -3
- package/chunks/{hunt-scan-ELTKGKC4.js → hunt-scan-MIRMABY4.js} +15 -15
- package/chunks/{kernel-primitive-MT7YZSIG.js → kernel-primitive-NAPDMJC7.js} +2 -2
- package/chunks/{kernel-vm-runner-YFRHYC6G.js → kernel-vm-runner-PR5OSDQ2.js} +2 -2
- package/chunks/managed-operation-YNQQEA5V.js +59 -0
- package/chunks/{memsafety-scan-FCPAXIZE.js → memsafety-scan-RPS446CD.js} +2 -2
- package/chunks/{native-loop-OFJDRHPI.js → native-loop-CGOJMNHV.js} +12 -12
- package/chunks/{orchestrate-DRZ2SVCP.js → orchestrate-5NK34H3W.js} +30 -30
- package/chunks/{prepare-72GWTRVI.js → prepare-YVNENPNL.js} +5 -5
- package/chunks/remote-workflow-runtime-RGQ5ECMN.js +96 -0
- package/chunks/{run-WTJCZZHV.js → run-VH4OIDKF.js} +73 -56
- package/chunks/{runtime-LCJ4XSKE.js → runtime-4TAJFVF7.js} +4 -2
- package/chunks/{runtime-FCKOUB7V.js → runtime-HWL6HG7A.js} +6 -6
- package/chunks/{scan-stream-GT6GKQ3U.js → scan-stream-HJAZJONE.js} +2 -2
- package/chunks/{scope-E5QZUOOH.js → scope-Q5PIX2IS.js} +2 -2
- package/chunks/service-plugins-QYFITODK.js +63 -0
- package/chunks/{session-store-H3Q7KCYG.js → session-store-2FEIZGSV.js} +2 -2
- package/chunks/{variant-candidates-SZKDCOLK.js → variant-candidates-TX5V6XOT.js} +6 -6
- package/chunks/{web-recon-prepass-IO3GXQMW.js → web-recon-prepass-6HOUGF7Q.js} +5 -5
- package/chunks/workflow-runtime-ZEEBJFQG.js +64 -0
- package/dashboard/assets/abnfDiagram-VCTEODGH-BuvumFYR.js +1 -0
- package/dashboard/assets/arc-DZzWDuCG.js +1 -0
- package/dashboard/assets/architecture-7GRP2DOG-DK9bv1Md.js +1 -0
- package/dashboard/assets/architectureDiagram-5GKGNRK7-Bw5el_EL.js +36 -0
- package/dashboard/assets/array-BifhSqXX.js +1 -0
- package/dashboard/assets/blockDiagram-I7D4REHJ-BjVLEApp.js +129 -0
- package/dashboard/assets/c4Diagram-7LVT6UL2-B9nNzx2J.js +38 -0
- package/dashboard/assets/channel-Ck17eKdX.js +1 -0
- package/dashboard/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dashboard/assets/chunk-4HAMMTFA-y72Q95nW.js +62 -0
- package/dashboard/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dashboard/assets/chunk-75Z2AOVW-BOhrOGMO.js +2 -0
- package/dashboard/assets/chunk-DU6HZSFF-B0EhAaNb.js +127 -0
- package/dashboard/assets/chunk-F27PBJKO-BR-hiDvM.js +1 -0
- package/dashboard/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- package/dashboard/assets/chunk-GMAD6QVW-H15vJYXg.js +72 -0
- package/dashboard/assets/chunk-GVQU2GXP-BRndps0o.js +1 -0
- package/dashboard/assets/chunk-IMKFNOWR-CIzC_OWx.js +231 -0
- package/dashboard/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dashboard/assets/chunk-L3NEJ4N5-NR4P1Vko.js +1 -0
- package/dashboard/assets/chunk-OSK3NFVY-DtjTDf8S.js +10 -0
- package/dashboard/assets/chunk-P2QGCYS3-CYT11Qjx.js +1 -0
- package/dashboard/assets/chunk-POPQ4Y6H-Duj2Ygwu.js +1 -0
- package/dashboard/assets/chunk-PWAF6VOD-z4p5Tf_y.js +1 -0
- package/dashboard/assets/chunk-SHT3W25Y-DYeLYLS3.js +168 -0
- package/dashboard/assets/chunk-SVP7TREG-BKowxCVI.js +88 -0
- package/dashboard/assets/chunk-TICWLB2K-GDAwEFlH.js +206 -0
- package/dashboard/assets/chunk-XXDRQBXY-DRD79Mjx.js +1 -0
- package/dashboard/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dashboard/assets/classDiagram-ZZMXUADV-BaeZA6LE.js +1 -0
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-BaeZA6LE.js +1 -0
- package/dashboard/assets/common-vnzd7UOF.js +5 -0
- package/dashboard/assets/cose-bilkent-JH36ORCC-GqCLsRyf.js +1 -0
- package/dashboard/assets/cynefin-OW5HDTMX-CJN9YwWN.js +1 -0
- package/dashboard/assets/cynefinDiagram-5FMLGOSQ-DVJHCYQY.js +62 -0
- package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +321 -0
- package/dashboard/assets/dagre-GXQ25YYZ-BMCrrJJR.js +4 -0
- package/dashboard/assets/dagre-PrKaheQc.js +1 -0
- package/dashboard/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dashboard/assets/diagram-S7CK7UJ4-CLYDdOEm.js +30 -0
- package/dashboard/assets/diagram-UQ7AKVKN-CnPuAZK7.js +41 -0
- package/dashboard/assets/diagram-VSXAHHWV-Cr2I2DnG.js +3 -0
- package/dashboard/assets/diagram-VX7I27RA-_mGW1wpp.js +24 -0
- package/dashboard/assets/diagram-Z3DM3KII-igybB0l9.js +24 -0
- package/dashboard/assets/dist-Bb7_-x5y.js +1 -0
- package/dashboard/assets/ebnfDiagram-PWID7BFC-CVCICf2R.js +1 -0
- package/dashboard/assets/erDiagram-RLTQ6QDP-CRjnfgxj.js +99 -0
- package/dashboard/assets/eventmodeling-NTZA5JFV-po5oDydv.js +1 -0
- package/dashboard/assets/flowDiagram-HODETNUW-TSV9s7q4.js +1 -0
- package/dashboard/assets/ganttDiagram-EL5Y4UJY-DppD7sD7.js +292 -0
- package/dashboard/assets/gitGraph-4MIJSDKK-Tw0vNnQc.js +1 -0
- package/dashboard/assets/gitGraphDiagram-WWUBYQGX-Cpf96-z3.js +106 -0
- package/dashboard/assets/graphlib-DS17s2tU.js +1 -0
- package/dashboard/assets/info-A6RAGUB7-DLyt9arx.js +1 -0
- package/dashboard/assets/infoDiagram-27XIBGKW-B6_piSNS.js +2 -0
- package/dashboard/assets/init-C-OQMol4.js +1 -0
- package/dashboard/assets/ishikawaDiagram-5VMMS53U-DvMO0B0X.js +70 -0
- package/dashboard/assets/journeyDiagram-3NMN7TZE-f7ZK98b6.js +139 -0
- package/dashboard/assets/kanban-definition-UXKFOSKX-Dso0nAxL.js +89 -0
- package/dashboard/assets/katex-CXMH3UgJ.js +257 -0
- package/dashboard/assets/line-VcKaFG0G.js +1 -0
- package/dashboard/assets/linear-D7YnpiWh.js +1 -0
- package/dashboard/assets/mermaid-parser.core-DriL2hrV.js +7 -0
- package/dashboard/assets/mermaid.core-DJWYDh_P.js +44 -0
- package/dashboard/assets/mindmap-definition-YA3MSWOX-DZsoIeac.js +96 -0
- package/dashboard/assets/operations-BIaUSnYv.js +121 -0
- package/dashboard/assets/operations-BeCdkT4S.css +1 -0
- package/dashboard/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dashboard/assets/packet-AYTQ26CC-BQBIXKxk.js +1 -0
- package/dashboard/assets/path-fybaL0A-.js +1 -0
- package/dashboard/assets/pegDiagram-XKGWAZYB-B0-lslgW.js +1 -0
- package/dashboard/assets/pie-WAS4IAKB-DoY6dwQs.js +1 -0
- package/dashboard/assets/pieDiagram-E7YTZNPT-ElmdRjp5.js +39 -0
- package/dashboard/assets/quadrantDiagram-AXDQQJYC-Ci1h98Q2.js +7 -0
- package/dashboard/assets/radar-RG4KPBEZ-DHod2WgB.js +1 -0
- package/dashboard/assets/railroad-74A4TZTK-90JxB4xV.js +1 -0
- package/dashboard/assets/railroad-abnf-HS5TGJTU-BkXGl114.js +1 -0
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-QVAERn2C.js +1 -0
- package/dashboard/assets/railroad-peg-WCYAUIDC-B1-PM01z.js +1 -0
- package/dashboard/assets/railroadDiagram-O6MQD6OU-k3190A6L.js +1 -0
- package/dashboard/assets/requirementDiagram-BXWQKSXE-C3-eR7I5.js +84 -0
- package/dashboard/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dashboard/assets/sankeyDiagram-P5KCCOFB-Bc86pi8w.js +40 -0
- package/dashboard/assets/sequenceDiagram-WJ2MYXX4-Bv_MvEmz.js +162 -0
- package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dashboard/assets/src-D7Dqq5yP.js +1 -0
- package/dashboard/assets/stateDiagram-D77RDMKH-Bq_GVmeW.js +1 -0
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-z1XRaftz.js +1 -0
- package/dashboard/assets/swimlanes-42K2YHIH-q0iK-e0P.js +1 -0
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-C9L6XBQj.js +8 -0
- package/dashboard/assets/timeline-definition-24CTP7MA-CNfF07hM.js +120 -0
- package/dashboard/assets/treeView-Q6P3EWNA-m629ePnS.js +1 -0
- package/dashboard/assets/treemap-WGGIJYW6-D9Cb2ETv.js +1 -0
- package/dashboard/assets/vennDiagram-4TSXK5OY-v4dKF-FE.js +34 -0
- package/dashboard/assets/wardley-WFR3VGLG-DkwH_nRe.js +1 -0
- package/dashboard/assets/wardleyDiagram-VM6X3IG4-3ImohVu4.js +78 -0
- package/dashboard/assets/xychartDiagram-S5SC5T6Z-QJu3dm7K.js +7 -0
- package/dashboard/assets/zero-connect-BGDY0PMZ.webp +0 -0
- package/dashboard/assets/zero-curious-DovTLOs8.webp +0 -0
- package/dashboard/assets/zero-planning-CBeHDrG_.webp +0 -0
- package/dashboard/index.html +2 -2
- package/package.json +1 -1
- package/chunks/service-plugins-MSK2EQP6.js +0 -62
- package/dashboard/assets/operations-BvB5Qpmk.css +0 -1
- package/dashboard/assets/operations-D8Ii01SV.js +0 -114
|
@@ -0,0 +1,1695 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0CreateRequire } from "node:module";
|
|
3
|
+
const require = __0CreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
executeManagedOperation
|
|
6
|
+
} from "./chunk-PIR6BNE7.js";
|
|
7
|
+
import {
|
|
8
|
+
loadFindingFocus
|
|
9
|
+
} from "./chunk-FXR3BT2G.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveEngagement
|
|
12
|
+
} from "./chunk-R35KKJA4.js";
|
|
13
|
+
import {
|
|
14
|
+
LinuxBootMatrixImportAdapter,
|
|
15
|
+
LinuxKernelResearchAdapter,
|
|
16
|
+
MobileStaticResearchAdapter,
|
|
17
|
+
UnifiedPipelineResearchAdapter,
|
|
18
|
+
WorkflowService,
|
|
19
|
+
createReproductionBundle,
|
|
20
|
+
executeAssessment,
|
|
21
|
+
runCliPathTraversalReplayFixture,
|
|
22
|
+
runPipeline,
|
|
23
|
+
runReproductionBundle,
|
|
24
|
+
runResearch
|
|
25
|
+
} from "./chunk-4HGGNBKK.js";
|
|
26
|
+
import {
|
|
27
|
+
oracleForCategory
|
|
28
|
+
} from "./chunk-62ENS6XO.js";
|
|
29
|
+
import {
|
|
30
|
+
DockerRunner,
|
|
31
|
+
LocalShellRunner,
|
|
32
|
+
QemuRunner,
|
|
33
|
+
SmolvmRunner,
|
|
34
|
+
createSmolvmPocTargetRunner,
|
|
35
|
+
runDeterministicReplay,
|
|
36
|
+
workflowPolicyRuntime
|
|
37
|
+
} from "./chunk-EH2WTNY7.js";
|
|
38
|
+
import {
|
|
39
|
+
applySourceFixCandidate,
|
|
40
|
+
runSourceFix,
|
|
41
|
+
verifySourceFixCandidate
|
|
42
|
+
} from "./chunk-YYS2H33A.js";
|
|
43
|
+
import {
|
|
44
|
+
SecurityWorkflowStore
|
|
45
|
+
} from "./chunk-T7ZEVY5G.js";
|
|
46
|
+
import {
|
|
47
|
+
evidenceKindForFinding
|
|
48
|
+
} from "./chunk-TO35Y326.js";
|
|
49
|
+
import {
|
|
50
|
+
executePocSteps
|
|
51
|
+
} from "./chunk-IXT64DV5.js";
|
|
52
|
+
import {
|
|
53
|
+
budgetNativeRuntime
|
|
54
|
+
} from "./chunk-CP6HANBE.js";
|
|
55
|
+
import {
|
|
56
|
+
LlmApiRuntime
|
|
57
|
+
} from "./chunk-BF5FIK7H.js";
|
|
58
|
+
import {
|
|
59
|
+
WORKBENCH_BROKER_WORKSPACE,
|
|
60
|
+
isAdmittedSmolvmWorkbench,
|
|
61
|
+
runWorkbenchBrokerProgram
|
|
62
|
+
} from "./chunk-NI6QYQSK.js";
|
|
63
|
+
import {
|
|
64
|
+
getScopeEnforcementState,
|
|
65
|
+
loadScope,
|
|
66
|
+
withScopeEnforcement
|
|
67
|
+
} from "./chunk-ITES7BCY.js";
|
|
68
|
+
import {
|
|
69
|
+
SECURITY_WORKFLOW_TEMPLATES,
|
|
70
|
+
SecurityWorkflowBindingsSchema,
|
|
71
|
+
VERSION,
|
|
72
|
+
VerificationResultSchema,
|
|
73
|
+
checkSecurityWorkflowTemplateTarget,
|
|
74
|
+
createSecurityWorkflowTemplate,
|
|
75
|
+
findingSchema,
|
|
76
|
+
formatZodError,
|
|
77
|
+
getSecurityWorkflowTemplate,
|
|
78
|
+
parseSecurityWorkflowInput
|
|
79
|
+
} from "./chunk-LTEE2JWK.js";
|
|
80
|
+
import {
|
|
81
|
+
external_exports
|
|
82
|
+
} from "./chunk-VIT5ALXF.js";
|
|
83
|
+
|
|
84
|
+
// packages/cli/src/workflow-runtime.ts
|
|
85
|
+
import { realpath as realpath3 } from "node:fs/promises";
|
|
86
|
+
import { isAbsolute as isAbsolute3, relative as relative3, sep as sep4 } from "node:path";
|
|
87
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
88
|
+
|
|
89
|
+
// packages/cli/src/finding-workflow-executors.ts
|
|
90
|
+
import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
91
|
+
import { execFile } from "node:child_process";
|
|
92
|
+
import { promisify } from "node:util";
|
|
93
|
+
import { randomUUID } from "node:crypto";
|
|
94
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
95
|
+
import { isAbsolute, join as join2, relative, resolve as resolve2, sep as sep2 } from "node:path";
|
|
96
|
+
|
|
97
|
+
// packages/cli/src/commands/verify.ts
|
|
98
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
99
|
+
import { createHash } from "node:crypto";
|
|
100
|
+
import { tmpdir } from "node:os";
|
|
101
|
+
import { join, resolve, posix, sep } from "node:path";
|
|
102
|
+
var EXCERPT_BYTES = 4 * 1024;
|
|
103
|
+
function excerpt(text2, max = EXCERPT_BYTES) {
|
|
104
|
+
if (!text2) return "";
|
|
105
|
+
if (text2.length <= max) return text2;
|
|
106
|
+
return text2.slice(0, max) + "\u2026[truncated]";
|
|
107
|
+
}
|
|
108
|
+
function statusFromVerdict(verdict) {
|
|
109
|
+
switch (verdict) {
|
|
110
|
+
case "exploit_still_works":
|
|
111
|
+
return "reproduced";
|
|
112
|
+
case "exploit_broken":
|
|
113
|
+
return "not_reproduced";
|
|
114
|
+
case "could_not_run":
|
|
115
|
+
return "skipped";
|
|
116
|
+
default: {
|
|
117
|
+
const _exhaustive = verdict;
|
|
118
|
+
void _exhaustive;
|
|
119
|
+
return "skipped";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function exitCodeForStatus(status) {
|
|
124
|
+
switch (status) {
|
|
125
|
+
case "reproduced":
|
|
126
|
+
return 0;
|
|
127
|
+
case "not_reproduced":
|
|
128
|
+
return 1;
|
|
129
|
+
case "skipped":
|
|
130
|
+
return 2;
|
|
131
|
+
case "error":
|
|
132
|
+
return 3;
|
|
133
|
+
default: {
|
|
134
|
+
const _exhaustive = status;
|
|
135
|
+
void _exhaustive;
|
|
136
|
+
return 3;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function argvForStep(step) {
|
|
141
|
+
switch (step.action.type) {
|
|
142
|
+
case "shell":
|
|
143
|
+
return ["/bin/sh", "-c", step.action.cmd];
|
|
144
|
+
case "docker":
|
|
145
|
+
return ["docker", "run", "--rm", ...step.action.args, step.action.image];
|
|
146
|
+
case "http":
|
|
147
|
+
return [step.action.method, step.action.url];
|
|
148
|
+
case "note":
|
|
149
|
+
return ["note", step.id];
|
|
150
|
+
default: {
|
|
151
|
+
const _exhaustive = step.action;
|
|
152
|
+
void _exhaustive;
|
|
153
|
+
return ["unknown"];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function defaultSummary(status, ran, total) {
|
|
158
|
+
switch (status) {
|
|
159
|
+
case "reproduced":
|
|
160
|
+
return `Replay reproduced the finding (${ran}/${total} steps executed).`;
|
|
161
|
+
case "not_reproduced":
|
|
162
|
+
return `Replay completed but the exploit no longer reproduces (${ran}/${total} steps executed).`;
|
|
163
|
+
case "skipped":
|
|
164
|
+
return total === 0 ? "No PoC steps to execute" : `Replay was skipped (${ran}/${total} steps executed).`;
|
|
165
|
+
case "error":
|
|
166
|
+
return "Verifier failed before reaching a verdict.";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function durationMs(startedAt, completedAt) {
|
|
170
|
+
const started = Date.parse(startedAt);
|
|
171
|
+
const completed = Date.parse(completedAt);
|
|
172
|
+
if (!Number.isFinite(started) || !Number.isFinite(completed)) return 0;
|
|
173
|
+
return Math.max(0, completed - started);
|
|
174
|
+
}
|
|
175
|
+
function engineMetadata() {
|
|
176
|
+
return {
|
|
177
|
+
os: process.platform,
|
|
178
|
+
arch: process.arch,
|
|
179
|
+
runner: isAdmittedSmolvmWorkbench() ? "smolvm" : "local"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function actualOutput(result) {
|
|
183
|
+
return result.observedStdout ?? result.observedResponseBody ?? "";
|
|
184
|
+
}
|
|
185
|
+
function assertionForStep(result, step) {
|
|
186
|
+
const expect = step.expect;
|
|
187
|
+
if (!expect) return null;
|
|
188
|
+
switch (expect.type) {
|
|
189
|
+
case "exit-zero":
|
|
190
|
+
return {
|
|
191
|
+
kind: "exit_code",
|
|
192
|
+
target: step.id,
|
|
193
|
+
expected: 0,
|
|
194
|
+
actual: result.observedExit ?? null,
|
|
195
|
+
passed: result.kind === "passed"
|
|
196
|
+
};
|
|
197
|
+
case "http-status":
|
|
198
|
+
return {
|
|
199
|
+
kind: "http_status",
|
|
200
|
+
target: step.id,
|
|
201
|
+
expected: Array.isArray(expect.status) ? expect.status.join(",") : expect.status,
|
|
202
|
+
actual: result.observedStatus ?? null,
|
|
203
|
+
passed: result.kind === "passed"
|
|
204
|
+
};
|
|
205
|
+
case "body-contains":
|
|
206
|
+
return {
|
|
207
|
+
kind: "string_in_output",
|
|
208
|
+
target: step.id,
|
|
209
|
+
expected: expect.text,
|
|
210
|
+
actual: excerpt(actualOutput(result)),
|
|
211
|
+
passed: result.kind === "passed"
|
|
212
|
+
};
|
|
213
|
+
case "body-matches":
|
|
214
|
+
return {
|
|
215
|
+
kind: "string_in_output",
|
|
216
|
+
target: step.id,
|
|
217
|
+
expected: `/${expect.pattern}/`,
|
|
218
|
+
actual: excerpt(actualOutput(result)),
|
|
219
|
+
passed: result.kind === "passed"
|
|
220
|
+
};
|
|
221
|
+
case "file-exists":
|
|
222
|
+
return {
|
|
223
|
+
kind: "file_exists",
|
|
224
|
+
target: expect.path,
|
|
225
|
+
expected: true,
|
|
226
|
+
actual: result.kind === "passed",
|
|
227
|
+
passed: result.kind === "passed"
|
|
228
|
+
};
|
|
229
|
+
default: {
|
|
230
|
+
const _exhaustive = expect;
|
|
231
|
+
void _exhaustive;
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function findingOastConfirmed(finding) {
|
|
237
|
+
if (finding.oastConfirmed === true) return true;
|
|
238
|
+
if (oracleForCategory(finding.category) !== "oast-callback") return false;
|
|
239
|
+
return (finding.layerVerdicts ?? []).some(
|
|
240
|
+
(v) => v?.verdict === "pass" && (v.layer === "pov_gate" || v.layer === "oracle")
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
function oastEvidenceFields(finding) {
|
|
244
|
+
const oast_confirmed = findingOastConfirmed(finding);
|
|
245
|
+
const findingKind = evidenceKindForFinding(finding);
|
|
246
|
+
const evidence_kind = oast_confirmed ? "reproduced-poc" : findingKind === "source-only" ? void 0 : findingKind;
|
|
247
|
+
return {
|
|
248
|
+
...evidence_kind ? { evidence_kind } : {},
|
|
249
|
+
...oast_confirmed ? { oast_confirmed: true } : {}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function buildVerificationResult(args) {
|
|
253
|
+
const { finding, report: report2, startedAt, completedAt } = args;
|
|
254
|
+
const stepsById = new Map(
|
|
255
|
+
(finding.pocSteps ?? []).map((s) => [s.id, s])
|
|
256
|
+
);
|
|
257
|
+
const status = statusFromVerdict(report2.overallVerdict);
|
|
258
|
+
const commands = report2.steps.filter((r) => r.kind !== "skipped").map((r) => {
|
|
259
|
+
const step = stepsById.get(r.stepId);
|
|
260
|
+
const argv = step ? argvForStep(step) : ["unknown", r.stepId];
|
|
261
|
+
const exit_code = typeof r.observedExit === "number" ? r.observedExit : typeof r.observedStatus === "number" ? r.observedStatus : null;
|
|
262
|
+
const stdout = r.observedStdout ?? r.observedResponseBody ?? "";
|
|
263
|
+
const stderr = r.observedStderr ?? r.error ?? "";
|
|
264
|
+
return {
|
|
265
|
+
argv,
|
|
266
|
+
exit_code,
|
|
267
|
+
stdout_excerpt: excerpt(stdout),
|
|
268
|
+
stderr_excerpt: excerpt(stderr),
|
|
269
|
+
duration_ms: r.durationMs
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
const assertions = [];
|
|
273
|
+
for (const r of report2.steps) {
|
|
274
|
+
const step = stepsById.get(r.stepId);
|
|
275
|
+
if (!step) continue;
|
|
276
|
+
const assertion = assertionForStep(r, step);
|
|
277
|
+
if (assertion) assertions.push(assertion);
|
|
278
|
+
}
|
|
279
|
+
const ran = report2.steps.filter((r) => r.kind !== "skipped").length;
|
|
280
|
+
const total = (finding.pocSteps ?? []).length;
|
|
281
|
+
return {
|
|
282
|
+
status,
|
|
283
|
+
mode: "deterministic_replay",
|
|
284
|
+
finding_id: finding.id,
|
|
285
|
+
engine_version: VERSION,
|
|
286
|
+
started_at: startedAt,
|
|
287
|
+
completed_at: completedAt,
|
|
288
|
+
duration_ms: durationMs(startedAt, completedAt),
|
|
289
|
+
commands,
|
|
290
|
+
assertions,
|
|
291
|
+
evidence_artifacts: [],
|
|
292
|
+
engine_metadata: engineMetadata(),
|
|
293
|
+
summary: defaultSummary(status, ran, total),
|
|
294
|
+
error_reason: null,
|
|
295
|
+
...oastEvidenceFields(finding)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function buildNoStepsResult(args) {
|
|
299
|
+
return {
|
|
300
|
+
status: "skipped",
|
|
301
|
+
mode: "deterministic_replay",
|
|
302
|
+
finding_id: args.finding.id,
|
|
303
|
+
engine_version: VERSION,
|
|
304
|
+
started_at: args.startedAt,
|
|
305
|
+
completed_at: args.completedAt,
|
|
306
|
+
duration_ms: durationMs(args.startedAt, args.completedAt),
|
|
307
|
+
commands: [],
|
|
308
|
+
assertions: [],
|
|
309
|
+
evidence_artifacts: [],
|
|
310
|
+
engine_metadata: engineMetadata(),
|
|
311
|
+
summary: "No PoC steps to execute",
|
|
312
|
+
error_reason: null,
|
|
313
|
+
// A finding proven purely out-of-band (an OAST callback) may carry no
|
|
314
|
+
// runnable pocSteps — the callback IS the proof. Stamp the provenance so
|
|
315
|
+
// the cloud still promotes it instead of reading `skipped` as no-evidence.
|
|
316
|
+
...oastEvidenceFields(args.finding)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function buildErrorResult(args) {
|
|
320
|
+
const reason = args.error instanceof Error ? args.error.message : String(args.error);
|
|
321
|
+
return {
|
|
322
|
+
status: "error",
|
|
323
|
+
mode: "deterministic_replay",
|
|
324
|
+
finding_id: args.finding?.id ?? "unknown",
|
|
325
|
+
engine_version: VERSION,
|
|
326
|
+
started_at: args.startedAt,
|
|
327
|
+
completed_at: args.completedAt,
|
|
328
|
+
duration_ms: durationMs(args.startedAt, args.completedAt),
|
|
329
|
+
commands: [],
|
|
330
|
+
assertions: [],
|
|
331
|
+
evidence_artifacts: [],
|
|
332
|
+
engine_metadata: engineMetadata(),
|
|
333
|
+
summary: "Verifier failed before reaching a verdict.",
|
|
334
|
+
error_reason: reason
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function legacyFixtureStatus(status) {
|
|
338
|
+
return status === "reproduced" || status === "not_reproduced" || status === "error" ? status : "skipped";
|
|
339
|
+
}
|
|
340
|
+
function legacyAssertionKind(kind) {
|
|
341
|
+
if (kind === "command_exit_zero") return "exit_code";
|
|
342
|
+
if (kind.startsWith("filesystem_")) return "file_exists";
|
|
343
|
+
return "string_in_output";
|
|
344
|
+
}
|
|
345
|
+
function artifactFromPath(kind, path) {
|
|
346
|
+
try {
|
|
347
|
+
const stat = statSync(path);
|
|
348
|
+
if (!stat.isFile()) return null;
|
|
349
|
+
const body = readFileSync(path);
|
|
350
|
+
return {
|
|
351
|
+
kind: kind.replace(/_ref$/, ""),
|
|
352
|
+
path,
|
|
353
|
+
sha256: createHash("sha256").update(body).digest("hex"),
|
|
354
|
+
bytes: stat.size
|
|
355
|
+
};
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function fixtureResultToShared(result) {
|
|
361
|
+
return {
|
|
362
|
+
status: legacyFixtureStatus(result.status),
|
|
363
|
+
mode: "deterministic_replay",
|
|
364
|
+
finding_id: result.finding_id,
|
|
365
|
+
engine_version: result.engine_version,
|
|
366
|
+
started_at: result.started_at,
|
|
367
|
+
completed_at: result.completed_at,
|
|
368
|
+
duration_ms: durationMs(result.started_at, result.completed_at),
|
|
369
|
+
commands: result.commands.map((command) => ({
|
|
370
|
+
argv: command.argv,
|
|
371
|
+
exit_code: command.exit_code,
|
|
372
|
+
stdout_excerpt: command.stdout_excerpt,
|
|
373
|
+
stderr_excerpt: command.stderr_excerpt,
|
|
374
|
+
duration_ms: 0
|
|
375
|
+
})),
|
|
376
|
+
assertions: result.assertions.map((assertion) => ({
|
|
377
|
+
kind: legacyAssertionKind(assertion.kind),
|
|
378
|
+
target: assertion.kind,
|
|
379
|
+
expected: true,
|
|
380
|
+
actual: assertion.passed,
|
|
381
|
+
passed: assertion.passed
|
|
382
|
+
})),
|
|
383
|
+
evidence_artifacts: Object.entries(result.artifacts).map(([kind, path]) => artifactFromPath(kind, path)).filter((artifact) => artifact !== null),
|
|
384
|
+
engine_metadata: engineMetadata(),
|
|
385
|
+
summary: result.summary,
|
|
386
|
+
error_reason: result.error_reason
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function readJson(path, kind) {
|
|
390
|
+
const abs = resolve(path);
|
|
391
|
+
let raw;
|
|
392
|
+
try {
|
|
393
|
+
raw = readFileSync(abs, "utf8");
|
|
394
|
+
} catch (err) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`failed to read ${kind} from ${abs}: ${err instanceof Error ? err.message : String(err)}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
return JSON.parse(raw);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`failed to parse ${kind} as JSON (${abs}): ${err instanceof Error ? err.message : String(err)}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function parseFixtureCommand(raw) {
|
|
408
|
+
if (!raw) return void 0;
|
|
409
|
+
let parsed;
|
|
410
|
+
try {
|
|
411
|
+
parsed = JSON.parse(raw);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
`failed to parse --fixture-command as JSON argv array: ${err instanceof Error ? err.message : String(err)}`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every((item) => typeof item === "string" && item.length > 0)) {
|
|
418
|
+
throw new Error("--fixture-command must be a non-empty JSON array of strings");
|
|
419
|
+
}
|
|
420
|
+
return parsed;
|
|
421
|
+
}
|
|
422
|
+
function parseDurationMs(raw) {
|
|
423
|
+
const trimmed = raw.trim();
|
|
424
|
+
if (trimmed === "") throw new Error("duration is empty");
|
|
425
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i);
|
|
426
|
+
if (!match) {
|
|
427
|
+
throw new Error(`invalid duration '${raw}' (expected e.g. 30m, 90s, 2h)`);
|
|
428
|
+
}
|
|
429
|
+
const value = parseFloat(match[1]);
|
|
430
|
+
const unit = (match[2] ?? "ms").toLowerCase();
|
|
431
|
+
switch (unit) {
|
|
432
|
+
case "ms":
|
|
433
|
+
return value;
|
|
434
|
+
case "s":
|
|
435
|
+
return value * 1e3;
|
|
436
|
+
case "m":
|
|
437
|
+
return value * 60 * 1e3;
|
|
438
|
+
case "h":
|
|
439
|
+
return value * 60 * 60 * 1e3;
|
|
440
|
+
default:
|
|
441
|
+
throw new Error(`invalid duration unit '${unit}'`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function runKernelFindingVerify(opts) {
|
|
445
|
+
const { verifyStaticKernelFinding, applyVerificationToFinding } = await import("./dist-4LGRIBOC.js");
|
|
446
|
+
const rawFinding = readJson(opts.findingPath, "finding");
|
|
447
|
+
let finding;
|
|
448
|
+
try {
|
|
449
|
+
finding = findingSchema.parse(rawFinding);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (err instanceof external_exports.ZodError) {
|
|
452
|
+
throw new Error(formatZodError(err, "finding JSON"));
|
|
453
|
+
}
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
const result = await verifyStaticKernelFinding(finding, {
|
|
457
|
+
kernelTree: opts.kernelTree,
|
|
458
|
+
kernelConfig: opts.kernelConfig,
|
|
459
|
+
attempts: opts.attempts,
|
|
460
|
+
wallClockMs: opts.wallClockMs
|
|
461
|
+
});
|
|
462
|
+
const promotedFinding = applyVerificationToFinding(
|
|
463
|
+
finding,
|
|
464
|
+
result
|
|
465
|
+
);
|
|
466
|
+
const exitCode = result.status === "confirmed" ? 0 : result.status === "soft_hit" ? 1 : result.status === "error" ? 3 : 2;
|
|
467
|
+
return {
|
|
468
|
+
exitCode,
|
|
469
|
+
result: {
|
|
470
|
+
mode: "kernel_finding_verify",
|
|
471
|
+
finding_id: finding.id,
|
|
472
|
+
status: result.status,
|
|
473
|
+
new_confidence: result.new_confidence,
|
|
474
|
+
signature: result.signature,
|
|
475
|
+
generated_program: result.generated_program,
|
|
476
|
+
generated_program_lang: result.generated_program_lang,
|
|
477
|
+
attempts: result.attempts.map((a) => ({
|
|
478
|
+
index: a.index,
|
|
479
|
+
program_lang: a.programLang,
|
|
480
|
+
expected_signature: a.expectedSignature,
|
|
481
|
+
rejected: a.rejected,
|
|
482
|
+
duration_ms: a.durationMs,
|
|
483
|
+
oracle: a.oracle
|
|
484
|
+
})),
|
|
485
|
+
reason: result.reason,
|
|
486
|
+
error: result.errorMessage,
|
|
487
|
+
finding: promotedFinding
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function allocateIsolatedWorkspace() {
|
|
492
|
+
const cwd = mkdtempSync(join(tmpdir(), "0-verify-"));
|
|
493
|
+
return {
|
|
494
|
+
cwd,
|
|
495
|
+
cleanup: () => {
|
|
496
|
+
try {
|
|
497
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
498
|
+
} catch {
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async function runVerify(opts) {
|
|
504
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
505
|
+
let finding = null;
|
|
506
|
+
let cleanup;
|
|
507
|
+
try {
|
|
508
|
+
if (opts.fixture) {
|
|
509
|
+
if (opts.fixture !== "cli-path-traversal") {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`unsupported fixture '${opts.fixture}', supported fixtures: cli-path-traversal`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (opts.fixtureMode && opts.fixtureMode !== "vulnerable" && opts.fixtureMode !== "patched") {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`unsupported --fixture-mode '${opts.fixtureMode}', expected 'vulnerable' or 'patched'`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (!opts.fixtureCommand || opts.fixtureCommand.length === 0) {
|
|
520
|
+
throw new Error("--fixture-command is required with --fixture");
|
|
521
|
+
}
|
|
522
|
+
if (isAdmittedSmolvmWorkbench()) {
|
|
523
|
+
const runDir = resolve(opts.artifactDir ?? mkdtempSync(join(tmpdir(), "0-smol-fixture-")));
|
|
524
|
+
mkdirSync(runDir, { recursive: true });
|
|
525
|
+
const fixtureArgv = opts.fixtureCommand.map((argument) => argument === runDir ? WORKBENCH_BROKER_WORKSPACE : argument.startsWith(runDir + sep) ? `${WORKBENCH_BROKER_WORKSPACE}/${argument.slice(runDir.length + 1)}` : argument);
|
|
526
|
+
const execution = await runWorkbenchBrokerProgram({
|
|
527
|
+
profile: "offline",
|
|
528
|
+
workspaceRoot: runDir,
|
|
529
|
+
command: [
|
|
530
|
+
"0",
|
|
531
|
+
"verify",
|
|
532
|
+
"--fixture",
|
|
533
|
+
"cli-path-traversal",
|
|
534
|
+
"--fixture-command",
|
|
535
|
+
JSON.stringify(fixtureArgv),
|
|
536
|
+
"--fixture-mode",
|
|
537
|
+
opts.fixtureMode ?? "vulnerable",
|
|
538
|
+
"--artifact-dir",
|
|
539
|
+
WORKBENCH_BROKER_WORKSPACE,
|
|
540
|
+
"--output",
|
|
541
|
+
`${WORKBENCH_BROKER_WORKSPACE}/result.json`
|
|
542
|
+
],
|
|
543
|
+
timeoutMs: 3e4,
|
|
544
|
+
memoryMb: 512,
|
|
545
|
+
cpus: 1,
|
|
546
|
+
maxOutputBytes: 1024 * 1024
|
|
547
|
+
}, opts.signal);
|
|
548
|
+
if (execution.error || execution.timedOut || execution.cleanupFailed) {
|
|
549
|
+
throw new Error(execution.error ?? (execution.timedOut ? "SmolVM fixture replay timed out" : "SmolVM fixture teardown was not confirmed"));
|
|
550
|
+
}
|
|
551
|
+
const result3 = VerificationResultSchema.parse(JSON.parse(readFileSync(join(runDir, "result.json"), "utf8")));
|
|
552
|
+
if (result3.finding_id !== "fixture:cli-path-traversal" || execution.exitCode !== exitCodeForStatus(result3.status)) {
|
|
553
|
+
throw new Error("SmolVM fixture result does not match its process outcome");
|
|
554
|
+
}
|
|
555
|
+
for (const artifact of result3.evidence_artifacts) {
|
|
556
|
+
const path = posix.relative(WORKBENCH_BROKER_WORKSPACE, artifact.path);
|
|
557
|
+
if (!path || path === ".." || path.startsWith("../") || posix.isAbsolute(path)) throw new Error("fixture artifact escaped the replay workspace");
|
|
558
|
+
const returnedPath = join(runDir, path);
|
|
559
|
+
const stat = lstatSync(returnedPath);
|
|
560
|
+
if (!stat.isFile() || stat.size > 4 * 1024 * 1024) throw new Error("invalid returned fixture artifact");
|
|
561
|
+
const body = readFileSync(returnedPath);
|
|
562
|
+
if (body.length !== artifact.bytes || createHash("sha256").update(body).digest("hex") !== artifact.sha256) {
|
|
563
|
+
throw new Error("returned fixture artifact digest mismatch");
|
|
564
|
+
}
|
|
565
|
+
artifact.path = returnedPath;
|
|
566
|
+
}
|
|
567
|
+
result3.engine_metadata = { os: "linux", arch: process.arch, runner: "smolvm" };
|
|
568
|
+
return { result: result3, exitCode: exitCodeForStatus(result3.status) };
|
|
569
|
+
}
|
|
570
|
+
const legacyResult = await runCliPathTraversalReplayFixture({
|
|
571
|
+
commandArgv: opts.fixtureCommand,
|
|
572
|
+
fixtureMode: opts.fixtureMode === "patched" ? "patched" : "vulnerable",
|
|
573
|
+
retainArtifacts: opts.retainArtifacts,
|
|
574
|
+
artifactDir: opts.artifactDir,
|
|
575
|
+
engineVersion: VERSION
|
|
576
|
+
});
|
|
577
|
+
const result2 = fixtureResultToShared(legacyResult);
|
|
578
|
+
VerificationResultSchema.parse(result2);
|
|
579
|
+
return { result: result2, exitCode: exitCodeForStatus(result2.status) };
|
|
580
|
+
}
|
|
581
|
+
if (opts.fixtureMode) {
|
|
582
|
+
throw new Error("--fixture-mode is only supported with --fixture");
|
|
583
|
+
}
|
|
584
|
+
if (!opts.findingPath) {
|
|
585
|
+
throw new Error("missing required flag: --finding <path>");
|
|
586
|
+
}
|
|
587
|
+
const rawFinding = readJson(opts.findingPath, "finding");
|
|
588
|
+
try {
|
|
589
|
+
finding = findingSchema.parse(rawFinding);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
if (err instanceof external_exports.ZodError) {
|
|
592
|
+
throw new Error(formatZodError(err, "finding JSON"));
|
|
593
|
+
}
|
|
594
|
+
throw err;
|
|
595
|
+
}
|
|
596
|
+
const baseTarget = opts.targetPath ? readJson(opts.targetPath, "target") : {};
|
|
597
|
+
let target = { ...baseTarget, allowProcessActions: false };
|
|
598
|
+
if (!baseTarget.cwd) {
|
|
599
|
+
const isolated = allocateIsolatedWorkspace();
|
|
600
|
+
cleanup = isolated.cleanup;
|
|
601
|
+
target = { ...target, cwd: isolated.cwd };
|
|
602
|
+
}
|
|
603
|
+
if (!finding.pocSteps || finding.pocSteps.length === 0) {
|
|
604
|
+
const completedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
605
|
+
const result2 = buildNoStepsResult({ finding, startedAt, completedAt: completedAt2 });
|
|
606
|
+
return { result: result2, exitCode: exitCodeForStatus(result2.status) };
|
|
607
|
+
}
|
|
608
|
+
if (isAdmittedSmolvmWorkbench()) {
|
|
609
|
+
const runner2 = createSmolvmPocTargetRunner(target);
|
|
610
|
+
const { result: result2, runDir } = await runDeterministicReplay(finding, {
|
|
611
|
+
runner: runner2,
|
|
612
|
+
runDir: target.cwd,
|
|
613
|
+
stepTimeoutMs: target.timeoutMs,
|
|
614
|
+
signal: opts.signal,
|
|
615
|
+
scope: opts.scopeFile ? loadScope(opts.scopeFile) : void 0
|
|
616
|
+
});
|
|
617
|
+
cleanup = void 0;
|
|
618
|
+
for (const artifact of result2.evidence_artifacts) artifact.path = join(runDir, artifact.path);
|
|
619
|
+
VerificationResultSchema.parse(result2);
|
|
620
|
+
return { result: result2, exitCode: exitCodeForStatus(result2.status) };
|
|
621
|
+
}
|
|
622
|
+
const report2 = await executePocSteps(finding, target);
|
|
623
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
624
|
+
const result = buildVerificationResult({
|
|
625
|
+
finding,
|
|
626
|
+
report: report2,
|
|
627
|
+
startedAt,
|
|
628
|
+
completedAt
|
|
629
|
+
});
|
|
630
|
+
return { result, exitCode: exitCodeForStatus(result.status) };
|
|
631
|
+
} catch (err) {
|
|
632
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
633
|
+
const result = buildErrorResult({
|
|
634
|
+
finding,
|
|
635
|
+
startedAt,
|
|
636
|
+
completedAt,
|
|
637
|
+
error: err
|
|
638
|
+
});
|
|
639
|
+
return { result, exitCode: exitCodeForStatus(result.status) };
|
|
640
|
+
} finally {
|
|
641
|
+
if (cleanup) cleanup();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function parseRunnerKind(raw) {
|
|
645
|
+
const v = (raw ?? (isAdmittedSmolvmWorkbench() ? "smolvm" : "local")).toLowerCase();
|
|
646
|
+
if (v === "local" || v === "smolvm" || v === "docker" || v === "qemu") return v;
|
|
647
|
+
throw new Error(
|
|
648
|
+
`unsupported --runner '${raw}', expected one of local|smolvm|docker|qemu`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
async function runDeterministicReplayCli(args) {
|
|
652
|
+
const runnerKind = args.runner ?? parseRunnerKind(void 0);
|
|
653
|
+
const rawFinding = readJson(args.findingPath, "finding");
|
|
654
|
+
let finding;
|
|
655
|
+
try {
|
|
656
|
+
finding = findingSchema.parse(rawFinding);
|
|
657
|
+
} catch (err) {
|
|
658
|
+
if (err instanceof external_exports.ZodError) {
|
|
659
|
+
throw new Error(formatZodError(err, "finding JSON"));
|
|
660
|
+
}
|
|
661
|
+
throw err;
|
|
662
|
+
}
|
|
663
|
+
let runDir;
|
|
664
|
+
if (args.outDir) {
|
|
665
|
+
runDir = resolve(args.outDir);
|
|
666
|
+
mkdirSync(runDir, { recursive: true });
|
|
667
|
+
}
|
|
668
|
+
if (args.dockerNetwork && runnerKind !== "docker") {
|
|
669
|
+
throw new Error("--docker-network is only valid with --runner docker");
|
|
670
|
+
}
|
|
671
|
+
if ((args.qemuBinary || args.qemuKernel || args.qemuBusybox) && runnerKind !== "qemu") {
|
|
672
|
+
throw new Error("--qemu-binary / --qemu-kernel / --qemu-busybox require --runner qemu");
|
|
673
|
+
}
|
|
674
|
+
const scope = args.scopeFile ? loadScope(args.scopeFile) : void 0;
|
|
675
|
+
const runner2 = runnerKind === "local" ? new LocalShellRunner() : runnerKind === "smolvm" ? new SmolvmRunner() : runnerKind === "docker" ? new DockerRunner({ network: args.dockerNetwork }) : new QemuRunner({
|
|
676
|
+
qemuBinary: args.qemuBinary,
|
|
677
|
+
kernelImage: args.qemuKernel,
|
|
678
|
+
busyboxPath: args.qemuBusybox
|
|
679
|
+
});
|
|
680
|
+
const { result } = await runDeterministicReplay(finding, {
|
|
681
|
+
runner: runner2,
|
|
682
|
+
runDir,
|
|
683
|
+
scope,
|
|
684
|
+
signal: args.signal,
|
|
685
|
+
engineVersion: VERSION
|
|
686
|
+
});
|
|
687
|
+
VerificationResultSchema.parse(result);
|
|
688
|
+
const exitCode = result.status === "reproduced" ? 0 : result.status === "not_reproduced" ? 1 : result.status === "skipped" ? 2 : 3;
|
|
689
|
+
return { result, exitCode };
|
|
690
|
+
}
|
|
691
|
+
async function verifyAction(opts, positionalFinding) {
|
|
692
|
+
if ((opts.bundle || opts.createBundle) && (opts.target || opts.fixtureCommand || opts.fixtureMode || opts.retainArtifacts || opts.artifactDir || opts.kernelTree || opts.attempts || opts.wallClock || opts.qemuBinary || opts.qemuKernel || opts.qemuBusybox)) {
|
|
693
|
+
throw new Error("Bundle modes cannot be combined with target, fixture, or kernel execution options");
|
|
694
|
+
}
|
|
695
|
+
if ((opts.bundle || opts.createBundle) && opts.format && opts.format !== "json") {
|
|
696
|
+
throw new Error("Bundle modes support only JSON output");
|
|
697
|
+
}
|
|
698
|
+
if (opts.createBundle) {
|
|
699
|
+
if (opts.finding || opts.fixture || opts.bundle || opts.runner || opts.kernelFinding || positionalFinding || opts.scope || opts.dockerNetwork) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
"--create-bundle cannot be combined with --finding / --fixture / --bundle / --runner / --kernel-finding / <finding> positional"
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (!opts.out) {
|
|
705
|
+
throw new Error("--create-bundle requires --out <bundle-dir>");
|
|
706
|
+
}
|
|
707
|
+
const { manifest, bundleDir } = await createReproductionBundle(
|
|
708
|
+
opts.createBundle,
|
|
709
|
+
opts.out
|
|
710
|
+
);
|
|
711
|
+
const result = JSON.stringify(
|
|
712
|
+
{
|
|
713
|
+
status: "bundle_created",
|
|
714
|
+
bundle_dir: bundleDir,
|
|
715
|
+
finding_id: manifest.finding.id,
|
|
716
|
+
engine_version: VERSION,
|
|
717
|
+
created_at: manifest.created_at,
|
|
718
|
+
runner: manifest.runner_compatibility.runner
|
|
719
|
+
},
|
|
720
|
+
null,
|
|
721
|
+
2
|
|
722
|
+
);
|
|
723
|
+
if (opts.output) {
|
|
724
|
+
writeFileSync(resolve(opts.output), result + "\n", "utf8");
|
|
725
|
+
} else {
|
|
726
|
+
process.stdout.write(result + "\n");
|
|
727
|
+
}
|
|
728
|
+
process.exitCode = 0;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (opts.kernelFinding) {
|
|
732
|
+
if (process.env["ZERO_KERNEL_VERIFY"] !== "1") {
|
|
733
|
+
throw new Error(
|
|
734
|
+
"--kernel-finding requires ZERO_KERNEL_VERIFY=1 (CI cost gate, #271). Run the command through `env ZERO_KERNEL_VERIFY=1 0 ...` to opt in."
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
if (!opts.kernelTree) {
|
|
738
|
+
throw new Error("--kernel-finding requires --kernel-tree <path>");
|
|
739
|
+
}
|
|
740
|
+
if (opts.finding || opts.fixture || opts.bundle || positionalFinding || opts.runner) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
"--kernel-finding cannot be combined with --finding / --fixture / --bundle / <finding> positional / --runner"
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
const attempts = opts.attempts ? parseInt(opts.attempts, 10) : void 0;
|
|
746
|
+
if (opts.attempts && (!Number.isFinite(attempts) || attempts <= 0)) {
|
|
747
|
+
throw new Error(`invalid --attempts '${opts.attempts}' (expected positive integer)`);
|
|
748
|
+
}
|
|
749
|
+
const wallClockMs = opts.wallClock ? parseDurationMs(opts.wallClock) : void 0;
|
|
750
|
+
const kernelOutcome = await executeManagedOperation({
|
|
751
|
+
type: "verify",
|
|
752
|
+
name: "Kernel finding verification",
|
|
753
|
+
target: resolve(opts.kernelTree),
|
|
754
|
+
timeCapMs: wallClockMs ?? 18e5,
|
|
755
|
+
inputs: { findingPath: resolve(opts.kernelFinding), kernelTree: resolve(opts.kernelTree), kernelConfig: opts.kernelConfig ?? "kasan" },
|
|
756
|
+
output: (outcome2) => outcome2.result,
|
|
757
|
+
status: (outcome2) => outcome2.exitCode === 3 ? "failed" : "completed",
|
|
758
|
+
execute: () => runKernelFindingVerify({
|
|
759
|
+
findingPath: opts.kernelFinding,
|
|
760
|
+
kernelTree: opts.kernelTree,
|
|
761
|
+
kernelConfig: opts.kernelConfig ?? "kasan",
|
|
762
|
+
attempts,
|
|
763
|
+
wallClockMs
|
|
764
|
+
})
|
|
765
|
+
});
|
|
766
|
+
const kernelJson = JSON.stringify(kernelOutcome.result, null, 2);
|
|
767
|
+
if (opts.output) {
|
|
768
|
+
writeFileSync(resolve(opts.output), kernelJson + "\n", "utf8");
|
|
769
|
+
} else {
|
|
770
|
+
process.stdout.write(kernelJson + "\n");
|
|
771
|
+
}
|
|
772
|
+
process.exitCode = kernelOutcome.exitCode;
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (!opts.bundle && (positionalFinding || opts.runner || isAdmittedSmolvmWorkbench() && opts.finding && !opts.target)) {
|
|
776
|
+
if (positionalFinding && opts.finding) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
"pass a finding path EITHER as a positional argument OR via --finding, not both"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
const findingPath = positionalFinding ?? opts.finding;
|
|
782
|
+
if (!findingPath) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
"missing finding path. Usage: 0 verify <finding.json> [--runner local|smolvm|docker|qemu]"
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
const runner2 = parseRunnerKind(opts.runner);
|
|
788
|
+
const { result, exitCode } = await executeManagedOperation({
|
|
789
|
+
type: "verify",
|
|
790
|
+
name: "Finding replay verification",
|
|
791
|
+
target: resolve(findingPath),
|
|
792
|
+
timeCapMs: 36e5,
|
|
793
|
+
inputs: { findingPath: resolve(findingPath), runner: runner2 },
|
|
794
|
+
status: (outcome2) => outcome2.result.status === "error" ? "failed" : "completed",
|
|
795
|
+
output: (outcome2) => outcome2.result,
|
|
796
|
+
execute: (context) => runDeterministicReplayCli({
|
|
797
|
+
findingPath,
|
|
798
|
+
runner: runner2,
|
|
799
|
+
outDir: opts.out,
|
|
800
|
+
scopeFile: opts.scope,
|
|
801
|
+
dockerNetwork: opts.dockerNetwork,
|
|
802
|
+
qemuBinary: opts.qemuBinary,
|
|
803
|
+
qemuKernel: opts.qemuKernel,
|
|
804
|
+
qemuBusybox: opts.qemuBusybox,
|
|
805
|
+
signal: context.signal
|
|
806
|
+
})
|
|
807
|
+
});
|
|
808
|
+
const json2 = JSON.stringify(result, null, 2);
|
|
809
|
+
if (opts.output) {
|
|
810
|
+
writeFileSync(resolve(opts.output), json2 + "\n", "utf8");
|
|
811
|
+
} else {
|
|
812
|
+
process.stdout.write(json2 + "\n");
|
|
813
|
+
}
|
|
814
|
+
process.exitCode = exitCode;
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (opts.bundle) {
|
|
818
|
+
if (opts.finding || opts.fixture || positionalFinding || opts.kernelFinding) {
|
|
819
|
+
throw new Error(
|
|
820
|
+
"--bundle cannot be combined with --finding / --fixture / --kernel-finding / <finding> positional"
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const bundleRunner = opts.runner ?? (isAdmittedSmolvmWorkbench() ? "smolvm" : void 0);
|
|
824
|
+
if (bundleRunner !== "local" && bundleRunner !== "smolvm" && bundleRunner !== "docker") {
|
|
825
|
+
throw new Error("--bundle requires --runner local|smolvm|docker (defaults to smolvm in an admitted workbench; qemu is not supported)");
|
|
826
|
+
}
|
|
827
|
+
const bundleResult = await executeManagedOperation({
|
|
828
|
+
type: "verify",
|
|
829
|
+
name: "Reproduction bundle verification",
|
|
830
|
+
target: resolve(opts.bundle),
|
|
831
|
+
timeCapMs: 36e5,
|
|
832
|
+
inputs: { bundleDir: resolve(opts.bundle), runner: bundleRunner },
|
|
833
|
+
execute: (context) => runReproductionBundle({
|
|
834
|
+
bundleDir: opts.bundle,
|
|
835
|
+
runner: bundleRunner,
|
|
836
|
+
outDir: opts.out,
|
|
837
|
+
scope: opts.scope ? loadScope(opts.scope) : void 0,
|
|
838
|
+
dockerNetwork: opts.dockerNetwork,
|
|
839
|
+
signal: context.signal
|
|
840
|
+
})
|
|
841
|
+
});
|
|
842
|
+
const json2 = JSON.stringify(bundleResult, null, 2);
|
|
843
|
+
if (opts.output) {
|
|
844
|
+
writeFileSync(resolve(opts.output), json2 + "\n", "utf8");
|
|
845
|
+
} else {
|
|
846
|
+
process.stdout.write(json2 + "\n");
|
|
847
|
+
}
|
|
848
|
+
process.exitCode = bundleResult.exitCode;
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (opts.fixture && opts.finding) {
|
|
852
|
+
throw new Error("--fixture and --finding are mutually exclusive");
|
|
853
|
+
}
|
|
854
|
+
if (opts.fixtureCommand && !opts.fixture) {
|
|
855
|
+
throw new Error("--fixture-command is only supported with --fixture");
|
|
856
|
+
}
|
|
857
|
+
if (opts.fixtureMode && !opts.fixture) {
|
|
858
|
+
throw new Error("--fixture-mode is only supported with --fixture");
|
|
859
|
+
}
|
|
860
|
+
if ((opts.retainArtifacts || opts.artifactDir) && !opts.fixture) {
|
|
861
|
+
throw new Error("--retain-artifacts / --artifact-dir are only supported with --fixture");
|
|
862
|
+
}
|
|
863
|
+
if (!opts.finding && !opts.fixture) {
|
|
864
|
+
throw new Error("missing required flag: --finding <path>");
|
|
865
|
+
}
|
|
866
|
+
if (opts.format && opts.format !== "json") {
|
|
867
|
+
throw new Error(`unsupported --format '${opts.format}', only 'json' is supported`);
|
|
868
|
+
}
|
|
869
|
+
const fixtureCommand = parseFixtureCommand(opts.fixtureCommand);
|
|
870
|
+
const outcome = await executeManagedOperation({
|
|
871
|
+
type: "verify",
|
|
872
|
+
name: opts.fixture ? "Verification fixture replay" : "Finding replay verification",
|
|
873
|
+
target: opts.finding ? resolve(opts.finding) : `fixture:${opts.fixture}`,
|
|
874
|
+
timeCapMs: 36e5,
|
|
875
|
+
inputs: { ...opts.finding ? { findingPath: resolve(opts.finding) } : { fixture: opts.fixture } },
|
|
876
|
+
status: (value) => value.result.status === "error" ? "failed" : "completed",
|
|
877
|
+
output: (value) => value.result,
|
|
878
|
+
execute: (context) => runVerify({
|
|
879
|
+
findingPath: opts.finding,
|
|
880
|
+
targetPath: opts.target,
|
|
881
|
+
fixture: opts.fixture,
|
|
882
|
+
fixtureCommand,
|
|
883
|
+
fixtureMode: opts.fixtureMode,
|
|
884
|
+
retainArtifacts: opts.retainArtifacts,
|
|
885
|
+
artifactDir: opts.artifactDir,
|
|
886
|
+
scopeFile: opts.scope,
|
|
887
|
+
signal: context.signal
|
|
888
|
+
})
|
|
889
|
+
});
|
|
890
|
+
const json = JSON.stringify(outcome.result, null, 2);
|
|
891
|
+
if (opts.output) {
|
|
892
|
+
writeFileSync(resolve(opts.output), json + "\n", "utf8");
|
|
893
|
+
} else {
|
|
894
|
+
process.stdout.write(json + "\n");
|
|
895
|
+
}
|
|
896
|
+
process.exitCode = outcome.exitCode;
|
|
897
|
+
}
|
|
898
|
+
function registerVerifyCommand(program) {
|
|
899
|
+
program.command("verify").description(
|
|
900
|
+
"Deterministically replay a finding's PoC steps and emit a verification_result JSON."
|
|
901
|
+
).argument(
|
|
902
|
+
"[finding]",
|
|
903
|
+
"Path to a finding.json (0#193 deterministic-replay path). Equivalent to --finding when --runner is supplied."
|
|
904
|
+
).option(
|
|
905
|
+
"--runner <kind>",
|
|
906
|
+
"Deterministic replay runner: local|smolvm|docker|qemu (default smolvm in an admitted workbench, local outside)."
|
|
907
|
+
).option(
|
|
908
|
+
"--docker-network <name>",
|
|
909
|
+
"Docker network for --runner docker. Defaults to none; bridge/custom networks require --scope and only permit HTTP steps."
|
|
910
|
+
).option(
|
|
911
|
+
"--scope <path>",
|
|
912
|
+
"Engagement scope JSON required for HTTP replay; SmolVM also refuses private/loopback destinations."
|
|
913
|
+
).option("--qemu-binary <path>", "QEMU emulator for --runner qemu.").option("--qemu-kernel <path>", "Guest kernel image for --runner qemu.").option(
|
|
914
|
+
"--qemu-busybox <path>",
|
|
915
|
+
"Static BusyBox binary used to build the offline QEMU guest."
|
|
916
|
+
).option(
|
|
917
|
+
"--out <dir>",
|
|
918
|
+
"0#193 run directory (artifacts go under <out>/artifacts/). Defaults to a fresh tmpdir."
|
|
919
|
+
).option("--finding <path>", "Path to a finding.json.").option(
|
|
920
|
+
"--bundle <path>",
|
|
921
|
+
"Path to a reproduction bundle directory; requires --runner local|smolvm|docker outside an admitted workbench. Replays the bundle's vulnerable and patched snapshots through the configured runner and emits an aggregate ReproductionBundleResult."
|
|
922
|
+
).option(
|
|
923
|
+
"--create-bundle <plan.json>",
|
|
924
|
+
"Path to a BundlePlan JSON. Creates a reproduction bundle without executing any PoC steps. Requires --out <bundle-dir>."
|
|
925
|
+
).option(
|
|
926
|
+
"--target <path>",
|
|
927
|
+
"Path to a target.json (PocExecutionTarget: baseUrl, env, cwd, timeoutMs, personas)."
|
|
928
|
+
).option(
|
|
929
|
+
"--fixture <name>",
|
|
930
|
+
"Run a built-in deterministic replay fixture. Supported: cli-path-traversal."
|
|
931
|
+
).option(
|
|
932
|
+
"--fixture-command <json>",
|
|
933
|
+
"JSON argv array for the CLI under test. Supports {{apiUrl}}, {{exportDir}}, and {{fixtureMode}} placeholders."
|
|
934
|
+
).option(
|
|
935
|
+
"--fixture-mode <mode>",
|
|
936
|
+
"Fixture behavior for --fixture: vulnerable or patched."
|
|
937
|
+
).option(
|
|
938
|
+
"--retain-artifacts",
|
|
939
|
+
"Keep the fixture sandbox, harness metadata, and stdout/stderr logs.",
|
|
940
|
+
false
|
|
941
|
+
).option(
|
|
942
|
+
"--artifact-dir <path>",
|
|
943
|
+
"Use this directory as the fixture sandbox root."
|
|
944
|
+
).option("--format <fmt>", "Output format. Only 'json' is supported.", "json").option(
|
|
945
|
+
"--output <path>",
|
|
946
|
+
"Write the verification_result JSON to this path instead of stdout."
|
|
947
|
+
).option(
|
|
948
|
+
"--kernel-finding <path>",
|
|
949
|
+
"Path to a kernel-review finding.json. Runs the Tier 2 agent loop to produce a reproducer and promote the finding via the kernel oracle. Requires ZERO_KERNEL_VERIFY=1."
|
|
950
|
+
).option(
|
|
951
|
+
"--kernel-tree <path>",
|
|
952
|
+
"Linux source tree used by --kernel-finding for Tier 1 kernel build."
|
|
953
|
+
).option(
|
|
954
|
+
"--kernel-config <profile>",
|
|
955
|
+
"Kernel build config profile for --kernel-finding (only 'kasan' supported).",
|
|
956
|
+
"kasan"
|
|
957
|
+
).option(
|
|
958
|
+
"--attempts <N>",
|
|
959
|
+
"Max reproducer attempts for --kernel-finding (default 5)."
|
|
960
|
+
).option(
|
|
961
|
+
"--wall-clock <duration>",
|
|
962
|
+
"Wall-clock budget for --kernel-finding (e.g. 30m, 90s; default 30m)."
|
|
963
|
+
).action(async (positionalFinding, opts) => {
|
|
964
|
+
try {
|
|
965
|
+
await verifyAction(opts, positionalFinding);
|
|
966
|
+
} catch (err) {
|
|
967
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
968
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
969
|
+
const result = {
|
|
970
|
+
status: "error",
|
|
971
|
+
mode: "deterministic_replay",
|
|
972
|
+
finding_id: "unknown",
|
|
973
|
+
engine_version: VERSION,
|
|
974
|
+
started_at: now,
|
|
975
|
+
completed_at: now,
|
|
976
|
+
duration_ms: 0,
|
|
977
|
+
commands: [],
|
|
978
|
+
assertions: [],
|
|
979
|
+
evidence_artifacts: [],
|
|
980
|
+
engine_metadata: engineMetadata(),
|
|
981
|
+
summary: "Verifier failed before reaching a verdict.",
|
|
982
|
+
error_reason: reason
|
|
983
|
+
};
|
|
984
|
+
const json = JSON.stringify(result, null, 2);
|
|
985
|
+
if (opts.output) {
|
|
986
|
+
try {
|
|
987
|
+
writeFileSync(resolve(opts.output), json + "\n", "utf8");
|
|
988
|
+
} catch {
|
|
989
|
+
process.stderr.write(json + "\n");
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
process.stdout.write(json + "\n");
|
|
993
|
+
}
|
|
994
|
+
process.exitCode = 3;
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// packages/cli/src/finding-workflow-executors.ts
|
|
1000
|
+
var defaults = {
|
|
1001
|
+
propose: runSourceFix,
|
|
1002
|
+
verifyCandidate: verifySourceFixCandidate,
|
|
1003
|
+
applyCandidate: applySourceFixCandidate,
|
|
1004
|
+
replay: runDeterministicReplayCli,
|
|
1005
|
+
finding: loadFindingFocus
|
|
1006
|
+
};
|
|
1007
|
+
var text = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1008
|
+
function positive(value, fallback, name) {
|
|
1009
|
+
if (value === void 0) return fallback;
|
|
1010
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer.`);
|
|
1011
|
+
return value;
|
|
1012
|
+
}
|
|
1013
|
+
function runner(inputs, options) {
|
|
1014
|
+
const selected = text(inputs.runner) ?? options.runner;
|
|
1015
|
+
if (selected !== "local" && selected !== "smolvm" && selected !== "docker" && selected !== "qemu") throw new Error("Verification requires an explicit local, smolvm, docker, or qemu runner.");
|
|
1016
|
+
return selected;
|
|
1017
|
+
}
|
|
1018
|
+
async function authorizedPath(path, options, explicitlyAllowed) {
|
|
1019
|
+
const resolved = await realpath(resolve2(path));
|
|
1020
|
+
if (explicitlyAllowed && resolved === await realpath(resolve2(explicitlyAllowed))) return resolved;
|
|
1021
|
+
const root = await realpath(options.workspace);
|
|
1022
|
+
const suffix = relative(root, resolved);
|
|
1023
|
+
if (isAbsolute(suffix) || suffix === ".." || suffix.startsWith(`..${sep2}`)) throw new Error("Workflow input is outside the authorized workspace.");
|
|
1024
|
+
return resolved;
|
|
1025
|
+
}
|
|
1026
|
+
function hasFindingInput(inputs) {
|
|
1027
|
+
return !!(inputs.finding || text(inputs.findingPath) || text(inputs.findingId) || text(inputs.fromStep));
|
|
1028
|
+
}
|
|
1029
|
+
async function validateFindingWorkflowInputs(workflow, runInputs, options) {
|
|
1030
|
+
for (const node of workflow.nodes.filter((node2) => node2.enabled && (node2.type === "fix" || node2.type === "verify"))) {
|
|
1031
|
+
const inputs = { ...runInputs, ...node.inputs, ...node.input };
|
|
1032
|
+
if (node.inputs?.applyApproval !== void 0 || node.inputs?.allowApply === true) throw new Error("Application approval must be supplied by the host and current run, never stored in a workflow.");
|
|
1033
|
+
if (node.type === "fix") {
|
|
1034
|
+
await authorizedPath(workflow.target, options);
|
|
1035
|
+
if (node.fix?.mode === "apply") {
|
|
1036
|
+
if (!options.allowApply) throw new Error("This host has not authorized repository application.");
|
|
1037
|
+
const approval = runInputs.applyApproval;
|
|
1038
|
+
if (!text(approval?.candidateId) || approval?.approval !== "apply-to-repository") throw new Error("Repository application requires approval of an exact live candidate.");
|
|
1039
|
+
} else if (!text(inputs.testCommand)) throw new Error("Source fix requires an explicit regression testCommand.");
|
|
1040
|
+
}
|
|
1041
|
+
if (!hasFindingInput(inputs) && !text(inputs.candidateId) && !text(inputs.artifactId)) throw new Error("A finding input or connected candidate artifact is required.");
|
|
1042
|
+
const sourceStep = workflow.nodes.find((step) => step.id === text(inputs.fromStep));
|
|
1043
|
+
if (node.type === "verify" && !text(inputs.candidateId) && !text(inputs.artifactId) && sourceStep?.type !== "fix") runner(inputs, options);
|
|
1044
|
+
if (text(inputs.findingId) && !inputs.finding && !text(inputs.findingPath) && !text(inputs.fromStep) && !text(inputs.dbPath) && !options.dbPath) throw new Error("Persisted finding lookup requires an explicitly admitted database path.");
|
|
1045
|
+
if (inputs.finding) findingSchema.parse(inputs.finding);
|
|
1046
|
+
if (text(inputs.findingPath)) findingSchema.parse(JSON.parse(await readFile(await authorizedPath(text(inputs.findingPath), options), "utf8")));
|
|
1047
|
+
if (text(inputs.dbPath)) await authorizedPath(text(inputs.dbPath), options, options.dbPath);
|
|
1048
|
+
if (text(inputs.scopeFile)) await authorizedPath(text(inputs.scopeFile), options, options.scopeFile);
|
|
1049
|
+
positive(inputs.testTimeoutMs, 3e5, "testTimeoutMs");
|
|
1050
|
+
positive(inputs.maxAttempts, 3, "maxAttempts");
|
|
1051
|
+
for (const key of ["qemuBinary", "qemuKernel", "qemuBusybox"]) if (text(inputs[key])) await authorizedPath(text(inputs[key]), options);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function createFindingCandidateStore(cleanup = async (repoRoot, worktree) => {
|
|
1055
|
+
await promisify(execFile)("git", ["-C", repoRoot, "worktree", "remove", "--force", worktree]);
|
|
1056
|
+
}) {
|
|
1057
|
+
const live = /* @__PURE__ */ new Map();
|
|
1058
|
+
let disposed = false;
|
|
1059
|
+
return {
|
|
1060
|
+
get: (id) => disposed ? void 0 : live.get(id),
|
|
1061
|
+
async put(id, result, repoRoot) {
|
|
1062
|
+
if (disposed || live.size >= 32) {
|
|
1063
|
+
if (result.candidate?.worktree) await cleanup(repoRoot, result.candidate.worktree);
|
|
1064
|
+
throw new Error("Live candidate retention is unavailable; dispose completed candidate worktrees first.");
|
|
1065
|
+
}
|
|
1066
|
+
live.set(id, { result, repoRoot, worktree: result.candidate?.worktree });
|
|
1067
|
+
},
|
|
1068
|
+
async dispose() {
|
|
1069
|
+
disposed = true;
|
|
1070
|
+
const errors = [];
|
|
1071
|
+
for (const [id, candidate] of live) {
|
|
1072
|
+
if (candidate.worktree) {
|
|
1073
|
+
try {
|
|
1074
|
+
await cleanup(candidate.repoRoot, candidate.worktree);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
errors.push(`${id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
live.clear();
|
|
1081
|
+
if (errors.length) throw new Error(`Candidate cleanup failed: ${errors.join("; ")}`);
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function createFindingWorkflowExecutors(options, dependencies = defaults) {
|
|
1086
|
+
const candidates = options.candidateStore ?? createFindingCandidateStore();
|
|
1087
|
+
const connectedCandidate = (context) => {
|
|
1088
|
+
const explicit = text(context.inputs.candidateId) ?? text(context.inputs.artifactId);
|
|
1089
|
+
const refs = context.priorOutputs.flatMap((output) => output.outputs).filter((output) => output.kind === "source-fix-candidate" || output.kind === "source-fix-verification").map((output) => text(output.value?.candidateId)).filter((id) => !!id);
|
|
1090
|
+
if (explicit) {
|
|
1091
|
+
if (!refs.includes(explicit) && !candidates.get(explicit)) throw new Error("Candidate must belong to a connected predecessor or this owning live host.");
|
|
1092
|
+
return explicit;
|
|
1093
|
+
}
|
|
1094
|
+
return refs.length === 1 ? refs[0] : void 0;
|
|
1095
|
+
};
|
|
1096
|
+
const finding = async (context) => {
|
|
1097
|
+
const inputs = context.inputs;
|
|
1098
|
+
const findingId = text(inputs.findingId);
|
|
1099
|
+
let result;
|
|
1100
|
+
if (inputs.finding) result = findingSchema.parse(inputs.finding);
|
|
1101
|
+
else if (text(inputs.findingPath)) result = findingSchema.parse(JSON.parse(await readFile(await authorizedPath(text(inputs.findingPath), options), "utf8")));
|
|
1102
|
+
else {
|
|
1103
|
+
const prior = context.priorReports.flatMap((report2) => report2.findings).filter((value) => !findingId || value.id === findingId);
|
|
1104
|
+
if (prior.length === 1) result = findingSchema.parse(prior[0]);
|
|
1105
|
+
else if (findingId && !text(inputs.fromStep)) {
|
|
1106
|
+
const path = text(inputs.dbPath);
|
|
1107
|
+
const dbPath = path ? await authorizedPath(path, options, options.dbPath) : options.dbPath;
|
|
1108
|
+
if (!dbPath) throw new Error("Persisted finding lookup requires an explicitly admitted database path.");
|
|
1109
|
+
result = dependencies.finding(findingId, { dbPath }).finding;
|
|
1110
|
+
} else throw new Error("Select exactly one finding from connected assessment results.");
|
|
1111
|
+
}
|
|
1112
|
+
if (findingId && result.id !== findingId) throw new Error("Finding input identity does not match the selected finding.");
|
|
1113
|
+
const verification = context.priorOutputs.flatMap((output) => output.outputs).find((output) => output.kind === "verification-result" && output.value?.finding_id === result.id);
|
|
1114
|
+
return verification ? { ...result, verification_result: verification.value } : result;
|
|
1115
|
+
};
|
|
1116
|
+
const candidateOutput = (id, result) => ({ kind: "source-fix-candidate", value: {
|
|
1117
|
+
candidateId: id,
|
|
1118
|
+
findingId: result.findingId,
|
|
1119
|
+
status: result.status,
|
|
1120
|
+
patch: result.patch,
|
|
1121
|
+
diff: result.diff,
|
|
1122
|
+
sourceFile: result.sourceFile,
|
|
1123
|
+
test: result.test,
|
|
1124
|
+
applied: result.applied
|
|
1125
|
+
}, artifactRefs: [id] });
|
|
1126
|
+
return {
|
|
1127
|
+
fix: async (context) => {
|
|
1128
|
+
context.signal.throwIfAborted();
|
|
1129
|
+
const repoRoot = await authorizedPath(context.target, options);
|
|
1130
|
+
if (context.node.fix?.mode === "apply") {
|
|
1131
|
+
if (!options.allowApply || context.node.inputs?.applyApproval !== void 0) throw new Error("Repository application is not authorized by this host.");
|
|
1132
|
+
const id2 = connectedCandidate(context);
|
|
1133
|
+
const live = id2 ? candidates.get(id2) : void 0;
|
|
1134
|
+
const approval = context.inputs.applyApproval;
|
|
1135
|
+
if (!live || live.repoRoot !== repoRoot || approval?.candidateId !== id2 || approval?.approval !== "apply-to-repository") throw new Error("Application requires approval of the exact process-owned live candidate.");
|
|
1136
|
+
const result2 = await dependencies.applyCandidate(live.result, { approval: "apply-to-repository", signal: context.signal });
|
|
1137
|
+
return { status: result2.status === "applied_and_retested" ? "completed" : "failed", outputs: [candidateOutput(id2, result2)], ...result2.error ? { error: result2.error } : {} };
|
|
1138
|
+
}
|
|
1139
|
+
const testCommand = text(context.inputs.testCommand);
|
|
1140
|
+
if (!testCommand) throw new Error("Source fix requires an explicit regression testCommand.");
|
|
1141
|
+
const runtime = workflowPolicyRuntime(budgetNativeRuntime(options.runtime, context.costLedger, context.signal, context.plan.costCapUsd, context.plan));
|
|
1142
|
+
const result = await dependencies.propose({
|
|
1143
|
+
repoRoot,
|
|
1144
|
+
finding: await finding(context),
|
|
1145
|
+
runtime,
|
|
1146
|
+
testCommand,
|
|
1147
|
+
apply: false,
|
|
1148
|
+
keepWorktree: true,
|
|
1149
|
+
signal: context.signal,
|
|
1150
|
+
onProgress: options.onProgress,
|
|
1151
|
+
maxAttempts: positive(context.inputs.maxAttempts, 3, "maxAttempts"),
|
|
1152
|
+
testTimeoutMs: Math.min(positive(context.inputs.testTimeoutMs, 3e5, "testTimeoutMs"), Math.max(1, context.deadline - Date.now()))
|
|
1153
|
+
});
|
|
1154
|
+
const id = randomUUID();
|
|
1155
|
+
if (result.status === "validated_candidate") await candidates.put(id, result, repoRoot);
|
|
1156
|
+
return {
|
|
1157
|
+
status: result.status === "error" || result.status === "precondition_failed" ? "failed" : "completed",
|
|
1158
|
+
outputs: [{ ...candidateOutput(id, result), kind: result.status === "validated_candidate" ? "source-fix-candidate" : "source-fix-result" }],
|
|
1159
|
+
...result.error ? { error: result.error } : {}
|
|
1160
|
+
};
|
|
1161
|
+
},
|
|
1162
|
+
verify: async (context) => {
|
|
1163
|
+
context.signal.throwIfAborted();
|
|
1164
|
+
const id = connectedCandidate(context);
|
|
1165
|
+
if (id) {
|
|
1166
|
+
const live = candidates.get(id);
|
|
1167
|
+
if (!live || live.repoRoot !== await authorizedPath(context.target, options)) throw new Error("Verification requires a process-owned live candidate in this repository.");
|
|
1168
|
+
const result = await dependencies.verifyCandidate(live.result, {
|
|
1169
|
+
signal: context.signal,
|
|
1170
|
+
testTimeoutMs: Math.min(positive(context.inputs.testTimeoutMs, 3e5, "testTimeoutMs"), Math.max(1, context.deadline - Date.now()))
|
|
1171
|
+
});
|
|
1172
|
+
return { status: "completed", outputs: [{ kind: "source-fix-verification", value: { ...candidateOutput(id, result).value, error: result.error, precondition: result.precondition, postcondition: result.postcondition } }] };
|
|
1173
|
+
}
|
|
1174
|
+
if (context.priorOutputs.some((output) => output.outputs.some((value) => value.kind === "source-fix-result"))) throw new Error("The connected source-fix step did not produce a validated live candidate.");
|
|
1175
|
+
const selected = await finding(context);
|
|
1176
|
+
const directory = await mkdtemp(join2(tmpdir2(), "0-workflow-finding-"));
|
|
1177
|
+
try {
|
|
1178
|
+
const findingPath = join2(directory, "finding.json");
|
|
1179
|
+
await writeFile(findingPath, JSON.stringify(selected));
|
|
1180
|
+
const scopeFile = text(context.inputs.scopeFile);
|
|
1181
|
+
const replay = await dependencies.replay({
|
|
1182
|
+
findingPath,
|
|
1183
|
+
runner: runner(context.inputs, options),
|
|
1184
|
+
signal: context.signal,
|
|
1185
|
+
scopeFile: scopeFile ? await authorizedPath(scopeFile, options, options.scopeFile) : options.scopeFile,
|
|
1186
|
+
dockerNetwork: text(context.inputs.dockerNetwork),
|
|
1187
|
+
qemuBinary: text(context.inputs.qemuBinary) ? await authorizedPath(text(context.inputs.qemuBinary), options) : void 0,
|
|
1188
|
+
qemuKernel: text(context.inputs.qemuKernel) ? await authorizedPath(text(context.inputs.qemuKernel), options) : void 0,
|
|
1189
|
+
qemuBusybox: text(context.inputs.qemuBusybox) ? await authorizedPath(text(context.inputs.qemuBusybox), options) : void 0
|
|
1190
|
+
});
|
|
1191
|
+
return {
|
|
1192
|
+
status: replay.result.status === "error" ? "failed" : "completed",
|
|
1193
|
+
outputs: [{
|
|
1194
|
+
kind: "verification-result",
|
|
1195
|
+
value: replay.result,
|
|
1196
|
+
artifactRefs: replay.result.evidence_artifacts.map((artifact) => artifact.path)
|
|
1197
|
+
}],
|
|
1198
|
+
...replay.result.error_reason ? { error: replay.result.error_reason } : {}
|
|
1199
|
+
};
|
|
1200
|
+
} finally {
|
|
1201
|
+
await rm(directory, { recursive: true, force: true });
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// packages/cli/src/workflow-research-executors.ts
|
|
1208
|
+
import { mkdir, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
|
|
1209
|
+
import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
1210
|
+
var integer = external_exports.number().int().positive();
|
|
1211
|
+
var researchInputs = external_exports.object({
|
|
1212
|
+
engine: external_exports.enum(["pipeline", "mobile", "linux-matrix", "linux"]).default("pipeline"),
|
|
1213
|
+
artifactRoot: external_exports.string().min(1).optional(),
|
|
1214
|
+
targetType: external_exports.enum(["url", "web-app", "source-code", "npm-package", "pypi-package", "cargo-package", "oci-image"]).optional(),
|
|
1215
|
+
profile: external_exports.string().min(1).optional(),
|
|
1216
|
+
matrixPath: external_exports.string().min(1).optional(),
|
|
1217
|
+
reproducerPath: external_exports.string().min(1).optional(),
|
|
1218
|
+
expectedSignature: external_exports.string().trim().min(1).optional(),
|
|
1219
|
+
boots: integer.max(16).default(3),
|
|
1220
|
+
minHits: integer.max(16).default(2),
|
|
1221
|
+
findingId: external_exports.string().min(1).optional(),
|
|
1222
|
+
fromStep: external_exports.string().optional(),
|
|
1223
|
+
scanId: external_exports.string().optional(),
|
|
1224
|
+
artifactId: external_exports.string().optional()
|
|
1225
|
+
});
|
|
1226
|
+
var deepInputs = external_exports.object({
|
|
1227
|
+
profile: external_exports.string().min(1).optional(),
|
|
1228
|
+
subsystem: external_exports.string().min(1).optional(),
|
|
1229
|
+
models: external_exports.array(external_exports.string().min(1)).min(1).max(8).optional(),
|
|
1230
|
+
attemptsPerCandidate: integer.max(16).optional(),
|
|
1231
|
+
concurrency: integer.max(32).optional(),
|
|
1232
|
+
maxCandidates: integer.max(5e3).optional(),
|
|
1233
|
+
quorum: integer.max(32).optional(),
|
|
1234
|
+
useThreatModel: external_exports.boolean().optional(),
|
|
1235
|
+
fromStep: external_exports.string().optional(),
|
|
1236
|
+
findingId: external_exports.string().optional(),
|
|
1237
|
+
scanId: external_exports.string().optional(),
|
|
1238
|
+
artifactId: external_exports.string().optional()
|
|
1239
|
+
});
|
|
1240
|
+
function contained(root, path) {
|
|
1241
|
+
const suffix = relative2(root, path);
|
|
1242
|
+
return !isAbsolute2(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep3}`);
|
|
1243
|
+
}
|
|
1244
|
+
async function scopedPath(workspace, input, existing = true) {
|
|
1245
|
+
const root = await realpath2(workspace);
|
|
1246
|
+
const path = resolve3(root, input);
|
|
1247
|
+
let ancestor = path;
|
|
1248
|
+
while (true) {
|
|
1249
|
+
try {
|
|
1250
|
+
const canonical = await realpath2(ancestor);
|
|
1251
|
+
if (!contained(root, canonical)) throw new Error("Research path is outside the workflow workspace.");
|
|
1252
|
+
if (existing && ancestor !== path) throw new Error("Research input path does not exist.");
|
|
1253
|
+
return resolve3(canonical, relative2(ancestor, path));
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) throw error;
|
|
1256
|
+
const parent = dirname(ancestor);
|
|
1257
|
+
if (parent === ancestor) throw error;
|
|
1258
|
+
ancestor = parent;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async function validateMatrixPaths(workspace, matrix) {
|
|
1263
|
+
const manifest = JSON.parse(await readFile2(matrix, "utf8"));
|
|
1264
|
+
for (const side of [manifest.vulnerable, manifest.patched]) {
|
|
1265
|
+
if (!side || !Array.isArray(side.boots)) throw new Error("Invalid boot-matrix manifest sides.");
|
|
1266
|
+
for (const boot of side.boots) {
|
|
1267
|
+
if (typeof boot.logPath !== "string" || !boot.logPath) throw new Error("Boot-matrix logs require explicit paths.");
|
|
1268
|
+
await scopedPath(workspace, resolve3(dirname(matrix), boot.logPath));
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function report(context, findings, warnings, started) {
|
|
1273
|
+
return {
|
|
1274
|
+
target: context.target,
|
|
1275
|
+
scanDepth: context.plan.depth,
|
|
1276
|
+
startedAt: new Date(started).toISOString(),
|
|
1277
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1278
|
+
durationMs: Date.now() - started,
|
|
1279
|
+
findings,
|
|
1280
|
+
warnings: warnings.map((message) => ({ stage: "source-analysis", message })),
|
|
1281
|
+
summary: {
|
|
1282
|
+
totalAttacks: 0,
|
|
1283
|
+
totalFindings: findings.length,
|
|
1284
|
+
critical: findings.filter((f) => f.severity === "critical").length,
|
|
1285
|
+
high: findings.filter((f) => f.severity === "high").length,
|
|
1286
|
+
medium: findings.filter((f) => f.severity === "medium").length,
|
|
1287
|
+
low: findings.filter((f) => f.severity === "low").length,
|
|
1288
|
+
info: findings.filter((f) => f.severity === "info").length
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
function boundFinding(context, id, dbPath) {
|
|
1293
|
+
const candidates = context.priorReports.flatMap((report2) => report2.findings);
|
|
1294
|
+
if (id) return candidates.find((finding) => finding.id === id) ?? loadFindingFocus(id, { dbPath }).finding;
|
|
1295
|
+
if (candidates.length === 1) return candidates[0];
|
|
1296
|
+
throw new Error("Kernel research requires an exact findingId or one connected finding.");
|
|
1297
|
+
}
|
|
1298
|
+
async function validateResearchWorkflowInputs(type, inputs, scope) {
|
|
1299
|
+
if (type === "deep-review") {
|
|
1300
|
+
deepInputs.parse(inputs);
|
|
1301
|
+
if (scope?.target) await scopedPath(scope.workspace, scope.target);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const value = researchInputs.parse(inputs);
|
|
1305
|
+
if (value.engine === "linux") throw new Error("Managed Linux reproducer research is unavailable: the kernel VM/build runner does not yet support workflow cancellation and deadlines. Use the explicit research linux command.");
|
|
1306
|
+
if (scope) {
|
|
1307
|
+
if (value.engine === "pipeline" && value.targetType && value.targetType !== "source-code") throw new Error("Managed local research requires targetType source-code.");
|
|
1308
|
+
await scopedPath(scope.workspace, value.artifactRoot ?? ".0/workflow-artifacts", false);
|
|
1309
|
+
if ((value.engine === "mobile" || value.engine === "pipeline") && scope.target) await scopedPath(scope.workspace, scope.target);
|
|
1310
|
+
if (value.engine === "linux-matrix" && (value.matrixPath ?? scope.target)) {
|
|
1311
|
+
const matrix = await scopedPath(scope.workspace, value.matrixPath ?? scope.target);
|
|
1312
|
+
await validateMatrixPaths(scope.workspace, matrix);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function createResearchWorkflowExecutors(options) {
|
|
1317
|
+
return {
|
|
1318
|
+
research: async (context) => {
|
|
1319
|
+
context.signal.throwIfAborted();
|
|
1320
|
+
await validateResearchWorkflowInputs("research", context.inputs, { workspace: options.workspace, target: context.target });
|
|
1321
|
+
const input = researchInputs.parse(context.inputs);
|
|
1322
|
+
const artifactRoot = await scopedPath(options.workspace, input.artifactRoot ?? ".0/workflow-artifacts", false);
|
|
1323
|
+
await mkdir(artifactRoot, { recursive: true });
|
|
1324
|
+
const started = Date.now();
|
|
1325
|
+
let result;
|
|
1326
|
+
if (input.engine === "pipeline") {
|
|
1327
|
+
const sourceTarget = await scopedPath(options.workspace, context.target);
|
|
1328
|
+
const runtime = workflowPolicyRuntime(options.runtime);
|
|
1329
|
+
const adapter = new UnifiedPipelineResearchAdapter((config) => runPipeline({
|
|
1330
|
+
...config,
|
|
1331
|
+
nativeRuntime: runtime,
|
|
1332
|
+
signal: context.signal,
|
|
1333
|
+
costLedger: context.costLedger,
|
|
1334
|
+
costCeilingUsd: context.plan.costCapUsd,
|
|
1335
|
+
plan: context.plan,
|
|
1336
|
+
timeout: Math.max(1, context.deadline - Date.now()),
|
|
1337
|
+
model: options.model,
|
|
1338
|
+
priorFindings: context.priorFindings
|
|
1339
|
+
}));
|
|
1340
|
+
result = await runResearch(adapter, {
|
|
1341
|
+
kind: "pipeline.unified",
|
|
1342
|
+
id: context.node.id,
|
|
1343
|
+
location: sourceTarget,
|
|
1344
|
+
config: { options: { depth: context.plan.depth, format: "json", runtime: "api", targetType: "source-code", ...input.profile ? { reviewProfile: input.profile } : {} } }
|
|
1345
|
+
}, { artifactRoot, signal: context.signal });
|
|
1346
|
+
} else if (input.engine === "mobile") {
|
|
1347
|
+
const target = await scopedPath(options.workspace, context.target);
|
|
1348
|
+
result = await runResearch(new MobileStaticResearchAdapter(), { kind: "mobile.static-intake", id: context.node.id, location: target, config: {} }, { artifactRoot, signal: context.signal });
|
|
1349
|
+
} else {
|
|
1350
|
+
const finding = boundFinding(context, input.findingId, options.dbPath);
|
|
1351
|
+
if (input.engine === "linux-matrix") {
|
|
1352
|
+
const matrix = await scopedPath(options.workspace, input.matrixPath ?? context.target);
|
|
1353
|
+
result = await runResearch(new LinuxBootMatrixImportAdapter(), { kind: "linux.kernel-boot-matrix-import", id: context.node.id, location: matrix, config: { finding } }, { artifactRoot, signal: context.signal });
|
|
1354
|
+
if (!result.candidates.length) return { status: "failed", error: "External boot-matrix manifest failed validation.", outputs: [{ kind: "research", value: result }] };
|
|
1355
|
+
} else {
|
|
1356
|
+
if (!input.reproducerPath || !input.expectedSignature) throw new Error("Linux research requires reproducerPath and expectedSignature.");
|
|
1357
|
+
if (input.minHits > input.boots) throw new Error("minHits cannot exceed boots.");
|
|
1358
|
+
const tree = await scopedPath(options.workspace, context.target);
|
|
1359
|
+
const reproducer = await scopedPath(options.workspace, input.reproducerPath);
|
|
1360
|
+
const verify = {
|
|
1361
|
+
...reproducer.endsWith(".syz") ? { syzProgramPath: reproducer } : { reproducerPath: reproducer },
|
|
1362
|
+
boots: input.boots,
|
|
1363
|
+
minHits: input.minHits,
|
|
1364
|
+
expectedSignature: input.expectedSignature,
|
|
1365
|
+
signal: context.signal
|
|
1366
|
+
};
|
|
1367
|
+
result = await runResearch(new LinuxKernelResearchAdapter(), { kind: "linux.kernel-reproducer", id: context.node.id, location: tree, config: { finding, verify } }, { artifactRoot, signal: context.signal });
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
status: result.completed ? "completed" : "failed",
|
|
1372
|
+
reports: [report(context, result.findings.map((item) => item.finding), result.warnings, started)],
|
|
1373
|
+
outputs: [{ kind: "research", value: result, artifactRefs: result.envelopePath ? [result.envelopePath] : [] }]
|
|
1374
|
+
};
|
|
1375
|
+
},
|
|
1376
|
+
"deep-review": async (context) => {
|
|
1377
|
+
context.signal.throwIfAborted();
|
|
1378
|
+
const input = deepInputs.parse(context.inputs);
|
|
1379
|
+
const target = await scopedPath(options.workspace, context.target);
|
|
1380
|
+
const selected = options.model ?? options.runtime.resolvedModel?.();
|
|
1381
|
+
if (input.models?.some((model) => model !== selected)) throw new Error("Workflow deep review models must match its bound model connection.");
|
|
1382
|
+
const { runDeepReview } = await import("./deep-review-DKED6EN5.js");
|
|
1383
|
+
const result = await runDeepReview({
|
|
1384
|
+
...input,
|
|
1385
|
+
target,
|
|
1386
|
+
runtime: "api",
|
|
1387
|
+
nativeRuntime: workflowPolicyRuntime(options.runtime),
|
|
1388
|
+
signal: context.signal,
|
|
1389
|
+
costLedger: context.costLedger,
|
|
1390
|
+
plan: context.plan,
|
|
1391
|
+
costCeilingUsd: context.plan.costCapUsd,
|
|
1392
|
+
timeoutMs: Math.max(1, context.deadline - Date.now())
|
|
1393
|
+
});
|
|
1394
|
+
return {
|
|
1395
|
+
status: result.exitCode === 0 ? "completed" : "failed",
|
|
1396
|
+
reports: result.report ? [result.report] : [],
|
|
1397
|
+
...result.exitCode !== 0 ? { error: `Deep review did not complete (exit ${result.exitCode}).` } : {},
|
|
1398
|
+
outputs: [{ kind: "deep-review", value: result }]
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// packages/cli/src/workflow-runtime.ts
|
|
1405
|
+
var defaults2 = {
|
|
1406
|
+
createRuntime: (options) => new LlmApiRuntime({ type: "api", ...options }),
|
|
1407
|
+
assess: (options) => executeAssessment(options)
|
|
1408
|
+
};
|
|
1409
|
+
function bounded(value, fallback, maximum, name) {
|
|
1410
|
+
const selected = value === void 0 ? fallback : fallback === void 0 ? value : Math.min(value, fallback);
|
|
1411
|
+
if (selected !== void 0 && (!Number.isFinite(selected) || selected <= 0 || selected > maximum)) throw new Error(`${name} must be positive and at most ${maximum}.`);
|
|
1412
|
+
return selected;
|
|
1413
|
+
}
|
|
1414
|
+
function contains(root, path) {
|
|
1415
|
+
const suffix = relative3(root, path);
|
|
1416
|
+
return !isAbsolute3(suffix) && suffix !== ".." && !suffix.startsWith(`..${sep4}`);
|
|
1417
|
+
}
|
|
1418
|
+
function nodeLinks(run) {
|
|
1419
|
+
const nodes = Object.fromEntries(run.workflow.nodes.map((node) => [node.id, { status: node.enabled ? run.status === "failed" ? "blocked" : run.status === "cancelled" ? "cancelled" : "queued" : "skipped" }]));
|
|
1420
|
+
for (const event of run.events) {
|
|
1421
|
+
if (event.type === "node_started") nodes[event.nodeId] = { status: "running" };
|
|
1422
|
+
if (event.result) {
|
|
1423
|
+
const { status, scanIds, dbPaths, error } = event.result;
|
|
1424
|
+
nodes[event.nodeId] = { status, ...scanIds ? { scanIds } : {}, ...dbPaths ? { dbPaths } : {}, ...error ? { error } : {} };
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (run.result) for (const [id, result] of Object.entries(run.result.nodeResults)) {
|
|
1428
|
+
const { status, scanIds, dbPaths, error } = result;
|
|
1429
|
+
nodes[id] = { status, ...scanIds ? { scanIds } : {}, ...dbPaths ? { dbPaths } : {}, ...error ? { error } : {} };
|
|
1430
|
+
}
|
|
1431
|
+
return nodes;
|
|
1432
|
+
}
|
|
1433
|
+
function parseWorkflowRunInputs(value) {
|
|
1434
|
+
return structuredClone(SecurityWorkflowBindingsSchema.parse(value ?? {}));
|
|
1435
|
+
}
|
|
1436
|
+
async function createCliWorkflowRuntime(options, dependencies = defaults2) {
|
|
1437
|
+
if (!options.ownerId || options.ownerId.length > 128) throw new Error("A workflow runtime requires a valid owner.");
|
|
1438
|
+
if (options.workspace && !isAbsolute3(options.workspace)) throw new Error("The authorized workspace must be an absolute path.");
|
|
1439
|
+
const workspace = options.workspace ? await realpath3(options.workspace) : options.ownerId === "cli" ? await realpath3(process.cwd()) : void 0;
|
|
1440
|
+
const scope = options.scopePath ? loadScope(options.scopePath) : void 0;
|
|
1441
|
+
const scopeState = getScopeEnforcementState(workspace);
|
|
1442
|
+
const store = new SecurityWorkflowStore(options.dbPath);
|
|
1443
|
+
if (options.recoverInterrupted !== false) store.interruptActiveExecutions();
|
|
1444
|
+
const candidateStore = createFindingCandidateStore();
|
|
1445
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1446
|
+
const requests = /* @__PURE__ */ new Map();
|
|
1447
|
+
const launches = /* @__PURE__ */ new Map();
|
|
1448
|
+
let disposed = false;
|
|
1449
|
+
const service = new WorkflowService({ onChange: (run) => {
|
|
1450
|
+
let retentionError;
|
|
1451
|
+
if (run.result) {
|
|
1452
|
+
try {
|
|
1453
|
+
store.saveExecutionResults(run.id, run.result);
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
retentionError = `Workflow completed with results available in its owning host, but durable result retention failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const current = store.getExecution(run.id);
|
|
1459
|
+
if (current && ["queued", "running"].includes(current.status)) store.updateExecution(run.id, {
|
|
1460
|
+
status: run.status,
|
|
1461
|
+
nodeResults: nodeLinks(run),
|
|
1462
|
+
...run.error || retentionError ? { error: (run.error ?? retentionError).slice(0, 16e3) } : {}
|
|
1463
|
+
});
|
|
1464
|
+
} });
|
|
1465
|
+
const read = (id, operatorInspection = false) => {
|
|
1466
|
+
const execution = store.getExecution(id);
|
|
1467
|
+
if (!execution || execution.sessionId !== options.ownerId && !(operatorInspection && options.ownerId === "cli")) throw new Error("Workflow run was not found for this owner.");
|
|
1468
|
+
return execution;
|
|
1469
|
+
};
|
|
1470
|
+
const preflight = async (target) => {
|
|
1471
|
+
if (options.target && target.trim() !== options.target.trim()) throw new Error("Target does not match this workflow server's configured target.");
|
|
1472
|
+
const resolution = resolveEngagement(target);
|
|
1473
|
+
if (!resolution.ok) throw new Error(resolution.message);
|
|
1474
|
+
const resolved = resolution.plan;
|
|
1475
|
+
if (resolved.kind === "source") {
|
|
1476
|
+
if (!workspace) throw new Error("Source workflows require an explicitly authorized --workspace.");
|
|
1477
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(resolved.target) || resolved.target.startsWith("git@")) {
|
|
1478
|
+
if (options.ownerId !== "cli" && (!options.target || options.target.trim() !== target.trim())) throw new Error("Remote source workflows require a fixed --target and an authorized --workspace.");
|
|
1479
|
+
} else if (!contains(workspace, await realpath3(resolved.target))) throw new Error("Source target is outside the authorized workspace.");
|
|
1480
|
+
} else if (resolved.kind === "web") {
|
|
1481
|
+
if (!scope) throw new Error("Live workflow targets require --scope.");
|
|
1482
|
+
if (!scopeState.enabled) throw new Error("Live workflows require scope enforcement. Enable the scope plugin for this project with `0 plugin enable scope`.");
|
|
1483
|
+
const verdict = scope.match(resolved.target);
|
|
1484
|
+
if (!verdict.allowed) throw new Error(`Workflow target is out of scope: ${verdict.reason}`);
|
|
1485
|
+
}
|
|
1486
|
+
return resolved;
|
|
1487
|
+
};
|
|
1488
|
+
const start = async (request) => {
|
|
1489
|
+
if (disposed) throw new Error("Workflow runtime is shutting down.");
|
|
1490
|
+
if (Boolean(request.templateId) === Boolean(request.workflowId)) throw new Error("Select exactly one template or saved workflow.");
|
|
1491
|
+
const target = request.target?.trim();
|
|
1492
|
+
if (!target) throw new Error("A workflow target is required.");
|
|
1493
|
+
const resolved = await preflight(target);
|
|
1494
|
+
if (disposed) throw new Error("Workflow runtime is shutting down.");
|
|
1495
|
+
const workflow = request.templateId ? createSecurityWorkflowTemplate(request.templateId, { revision: request.revision, target, targetType: resolved.targetType }) : store.get(request.workflowId);
|
|
1496
|
+
if (!workflow) throw new Error("Workflow was not found.");
|
|
1497
|
+
if (request.workflowId && request.revision !== void 0 && workflow.revision !== request.revision) throw new Error("Workflow revision changed; refresh before running.");
|
|
1498
|
+
if (workflow.template) checkSecurityWorkflowTemplateTarget(getSecurityWorkflowTemplate(workflow.template.id, workflow.template.revision), resolved.targetType);
|
|
1499
|
+
if (!workflow.nodes.some((node) => node.enabled && node.type !== "trigger" && node.type !== "report")) throw new Error("Enable at least one executable step before running.");
|
|
1500
|
+
if (request.allowApply && !options.allowApply) throw new Error("This host does not authorize applying fixes.");
|
|
1501
|
+
const inputs = parseWorkflowRunInputs(request.inputs);
|
|
1502
|
+
const { createdAt: _createdAt, updatedAt: _updatedAt, ...portable } = workflow;
|
|
1503
|
+
const definition = parseSecurityWorkflowInput({ ...portable, target });
|
|
1504
|
+
const timeCapMs = bounded(request.timeCapMs, options.timeCapMs, 864e5, "Time cap");
|
|
1505
|
+
const costCapUsd = bounded(request.costCapUsd, options.costCapUsd, 1e3, "Cost cap");
|
|
1506
|
+
const fingerprint = createHash2("sha256").update(JSON.stringify({ definition, inputs, allowApply: request.allowApply === true, model: request.model ?? options.model, timeCapMs, costCapUsd })).digest("hex");
|
|
1507
|
+
if (request.idempotencyKey) {
|
|
1508
|
+
const previous = requests.get(request.idempotencyKey);
|
|
1509
|
+
if (previous) {
|
|
1510
|
+
if (previous.fingerprint !== fingerprint) throw new Error("Idempotency key belongs to a different workflow request.");
|
|
1511
|
+
return read(previous.id);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const specialized = definition.nodes.filter((node) => node.enabled && !["trigger", "audit", "report"].includes(node.type));
|
|
1515
|
+
if (specialized.length && (!workspace || resolved.kind !== "source" || /^[a-z][a-z0-9+.-]*:\/\//i.test(resolved.target) || resolved.target.startsWith("git@"))) {
|
|
1516
|
+
throw new Error("Specialized workflows require a local source target inside the authorized workspace.");
|
|
1517
|
+
}
|
|
1518
|
+
for (const node of specialized) if (node.type === "research" || node.type === "deep-review") {
|
|
1519
|
+
await validateResearchWorkflowInputs(node.type, { ...inputs, ...node.inputs, ...node.input }, { workspace, target: resolved.target });
|
|
1520
|
+
}
|
|
1521
|
+
const runtime = dependencies.createRuntime({ model: request.model ?? options.model, timeout: timeCapMs ?? 6e5 });
|
|
1522
|
+
const needsModel = definition.nodes.some((node) => {
|
|
1523
|
+
if (!node.enabled) return false;
|
|
1524
|
+
if (node.type === "audit" || node.type === "deep-review") return true;
|
|
1525
|
+
if (node.type === "fix") return node.fix?.mode !== "apply";
|
|
1526
|
+
if (node.type === "research") {
|
|
1527
|
+
const bindings = { ...inputs, ...node.inputs, ...node.input };
|
|
1528
|
+
return bindings.engine === void 0 || bindings.engine === "pipeline";
|
|
1529
|
+
}
|
|
1530
|
+
return false;
|
|
1531
|
+
});
|
|
1532
|
+
if (needsModel && !await runtime.isAvailable()) throw new Error("Configure 0's model provider before starting this workflow.");
|
|
1533
|
+
const findingOptions = { runtime, candidateStore, workspace: workspace ?? "", dbPath: options.dbPath, scopeFile: options.scopePath, allowApply: options.allowApply === true && request.allowApply === true };
|
|
1534
|
+
await validateFindingWorkflowInputs({ ...definition, target: resolved.target }, inputs, findingOptions);
|
|
1535
|
+
const executors = specialized.length ? { ...createFindingWorkflowExecutors(findingOptions), ...createResearchWorkflowExecutors({ runtime, workspace, dbPath: options.dbPath, model: request.model ?? options.model }) } : {};
|
|
1536
|
+
for (const [operation, executor] of Object.entries(executors)) {
|
|
1537
|
+
executors[operation] = (context) => withScopeEnforcement(scopeState, () => executor(context));
|
|
1538
|
+
}
|
|
1539
|
+
if (disposed) throw new Error("Workflow runtime is shutting down.");
|
|
1540
|
+
const execution = store.createExecutionFromSnapshot({ ...definition, id: request.workflowId ?? `template-${request.templateId}`, revision: workflow.revision ?? 1 }, options.ownerId);
|
|
1541
|
+
owned.add(execution.id);
|
|
1542
|
+
if (request.idempotencyKey) requests.set(request.idempotencyKey, { id: execution.id, fingerprint });
|
|
1543
|
+
service.start(options.ownerId, {
|
|
1544
|
+
workflow: definition,
|
|
1545
|
+
target: resolved.target,
|
|
1546
|
+
timeCapMs,
|
|
1547
|
+
costCapUsd,
|
|
1548
|
+
inputs,
|
|
1549
|
+
executors,
|
|
1550
|
+
executeAssessment: async (context) => {
|
|
1551
|
+
const scanIds = [];
|
|
1552
|
+
const { report: report2 } = await withScopeEnforcement(scopeState, () => dependencies.assess({
|
|
1553
|
+
target: resolved.target,
|
|
1554
|
+
targetType: resolved.targetType,
|
|
1555
|
+
reviewPackageEcosystem: resolved.ecosystem,
|
|
1556
|
+
runtime: "api",
|
|
1557
|
+
depth: context.plan.depth,
|
|
1558
|
+
format: "json",
|
|
1559
|
+
timeout: context.plan.timeCapMs,
|
|
1560
|
+
plan: context.plan,
|
|
1561
|
+
costCeilingUsd: context.plan.costCapUsd,
|
|
1562
|
+
costLedger: context.costLedger,
|
|
1563
|
+
nativeRuntime: workflowPolicyRuntime(runtime),
|
|
1564
|
+
model: request.model ?? options.model,
|
|
1565
|
+
scope,
|
|
1566
|
+
scopeFile: options.scopePath,
|
|
1567
|
+
signal: context.signal,
|
|
1568
|
+
dbPath: options.dbPath,
|
|
1569
|
+
priorFindings: context.priorFindings,
|
|
1570
|
+
onEvent: (event) => {
|
|
1571
|
+
const data = event?.data;
|
|
1572
|
+
if (data?.persisted === true && typeof data.scanId === "string" && !scanIds.includes(data.scanId)) scanIds.push(data.scanId);
|
|
1573
|
+
}
|
|
1574
|
+
}));
|
|
1575
|
+
const failed = report2.executionSuccessful === false || report2.costCeilingExceeded || ["failed", "error", "partial", "time_cap_exceeded", "cancelled"].includes(report2.exitReason ?? "");
|
|
1576
|
+
return { status: report2.exitReason === "cancelled" ? "cancelled" : failed ? "failed" : "completed", reports: [report2], scanIds, ...options.dbPath ? { dbPaths: [options.dbPath] } : {}, ...report2.error ? { error: report2.error } : {} };
|
|
1577
|
+
}
|
|
1578
|
+
}, { id: execution.id });
|
|
1579
|
+
return execution;
|
|
1580
|
+
};
|
|
1581
|
+
const getRun = (id) => {
|
|
1582
|
+
const execution = read(id, true);
|
|
1583
|
+
if (!owned.has(id)) return execution;
|
|
1584
|
+
let view;
|
|
1585
|
+
try {
|
|
1586
|
+
view = service.get(options.ownerId, id);
|
|
1587
|
+
} catch (error) {
|
|
1588
|
+
if (!["queued", "running"].includes(execution.status)) return execution;
|
|
1589
|
+
throw error;
|
|
1590
|
+
}
|
|
1591
|
+
return {
|
|
1592
|
+
...execution,
|
|
1593
|
+
status: view.status,
|
|
1594
|
+
nodeResults: nodeLinks(view),
|
|
1595
|
+
error: view.error ?? execution.error,
|
|
1596
|
+
...view.cancellationRequestedAt ? { cancellationRequested: true, cancellationAcknowledged: true, cancellationRequestedAt: view.cancellationRequestedAt } : {},
|
|
1597
|
+
events: view.events.map((event) => ({ ...event, result: event.result ? { status: event.result.status, scanIds: event.result.scanIds, dbPaths: event.result.dbPaths, error: event.result.error } : void 0, findings: void 0 })),
|
|
1598
|
+
oldestSequence: view.oldestSequence,
|
|
1599
|
+
eventsTruncated: view.eventsTruncated
|
|
1600
|
+
};
|
|
1601
|
+
};
|
|
1602
|
+
return {
|
|
1603
|
+
listTemplates: () => structuredClone(SECURITY_WORKFLOW_TEMPLATES),
|
|
1604
|
+
getTemplate: (id) => getSecurityWorkflowTemplate(id),
|
|
1605
|
+
listWorkflows: () => store.list(),
|
|
1606
|
+
listRuns: () => store.listExecutions(void 0, options.ownerId).map((execution) => getRun(execution.id)),
|
|
1607
|
+
getWorkflow: (id) => store.get(id),
|
|
1608
|
+
saveWorkflow: (definition, expectedRevision) => {
|
|
1609
|
+
const parsed = parseSecurityWorkflowInput(definition);
|
|
1610
|
+
return store.save({ ...parsed, ...expectedRevision !== void 0 ? { revision: expectedRevision } : {} });
|
|
1611
|
+
},
|
|
1612
|
+
startRun: (request) => {
|
|
1613
|
+
if (!request.idempotencyKey) return start(request);
|
|
1614
|
+
const key = request.idempotencyKey;
|
|
1615
|
+
const signature = JSON.stringify(request);
|
|
1616
|
+
const active = launches.get(key);
|
|
1617
|
+
if (active) {
|
|
1618
|
+
if (active.signature !== signature) return Promise.reject(new Error("Idempotency key belongs to a different workflow request."));
|
|
1619
|
+
return active.promise;
|
|
1620
|
+
}
|
|
1621
|
+
const launch = start(request);
|
|
1622
|
+
launches.set(key, { signature, promise: launch });
|
|
1623
|
+
void launch.finally(() => launches.delete(key)).catch(() => {
|
|
1624
|
+
});
|
|
1625
|
+
return launch;
|
|
1626
|
+
},
|
|
1627
|
+
getRun,
|
|
1628
|
+
getRunResults: (id, page = {}) => {
|
|
1629
|
+
const execution = read(id, true);
|
|
1630
|
+
const cursor = page.cursor ?? 0;
|
|
1631
|
+
const limit = page.limit ?? 100;
|
|
1632
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error("Invalid result page; limit must be 1\u2013100 and cursor nonnegative.");
|
|
1633
|
+
let liveResult;
|
|
1634
|
+
if (owned.has(id)) {
|
|
1635
|
+
try {
|
|
1636
|
+
liveResult = service.getResults(options.ownerId, id);
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
if (["queued", "running"].includes(execution.status)) throw error;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const result = liveResult ?? store.getExecutionResults(id);
|
|
1642
|
+
if (!result) return { runId: id, findings: [], reports: [], nextCursor: null, retained: false, pending: ["queued", "running"].includes(execution.status), error: execution.error };
|
|
1643
|
+
return {
|
|
1644
|
+
runId: id,
|
|
1645
|
+
status: result.status,
|
|
1646
|
+
retained: true,
|
|
1647
|
+
report: result.report ? { ...result.report, findings: result.findings.slice(cursor, cursor + limit) } : void 0,
|
|
1648
|
+
findings: result.findings.slice(cursor, cursor + limit),
|
|
1649
|
+
reports: result.reports.map((report2) => ({ ...report2, findings: void 0 })),
|
|
1650
|
+
outputs: result.outputs,
|
|
1651
|
+
nodeResults: nodeLinks({ result, workflow: result.workflow, events: [] }),
|
|
1652
|
+
totalFindings: result.findings.length,
|
|
1653
|
+
nextCursor: cursor + limit < result.findings.length ? cursor + limit : null,
|
|
1654
|
+
costUsd: result.costUsd,
|
|
1655
|
+
durationMs: result.durationMs,
|
|
1656
|
+
error: result.error
|
|
1657
|
+
};
|
|
1658
|
+
},
|
|
1659
|
+
cancelRun: (id) => {
|
|
1660
|
+
const execution = read(id);
|
|
1661
|
+
if (!owned.has(id)) {
|
|
1662
|
+
if (["queued", "running"].includes(execution.status)) throw new Error("This run belongs to another host. Cancel it in its owning engine.");
|
|
1663
|
+
return execution;
|
|
1664
|
+
}
|
|
1665
|
+
try {
|
|
1666
|
+
service.cancel(options.ownerId, id);
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
if (["queued", "running"].includes(execution.status)) throw error;
|
|
1669
|
+
}
|
|
1670
|
+
return getRun(id);
|
|
1671
|
+
},
|
|
1672
|
+
dispose: async () => {
|
|
1673
|
+
if (disposed) return;
|
|
1674
|
+
disposed = true;
|
|
1675
|
+
await Promise.allSettled([...launches.values()].map((launch) => launch.promise));
|
|
1676
|
+
await service.dispose();
|
|
1677
|
+
try {
|
|
1678
|
+
await candidateStore.dispose();
|
|
1679
|
+
} finally {
|
|
1680
|
+
store.close();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
export {
|
|
1687
|
+
validateResearchWorkflowInputs,
|
|
1688
|
+
createResearchWorkflowExecutors,
|
|
1689
|
+
registerVerifyCommand,
|
|
1690
|
+
validateFindingWorkflowInputs,
|
|
1691
|
+
createFindingCandidateStore,
|
|
1692
|
+
createFindingWorkflowExecutors,
|
|
1693
|
+
parseWorkflowRunInputs,
|
|
1694
|
+
createCliWorkflowRuntime
|
|
1695
|
+
};
|