0sec-cli 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/0sec.js +322 -0
  2. package/README.md +63 -52
  3. package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
  4. package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
  5. package/attacks/jailbreak/dan-roleplay.yaml +27 -0
  6. package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
  7. package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
  8. package/attacks/output-manipulation/harmful-content.yaml +25 -0
  9. package/attacks/prompt-injection/context-manipulation.yaml +32 -0
  10. package/attacks/prompt-injection/direct-injection.yaml +28 -0
  11. package/attacks/prompt-injection/indirect-injection.yaml +33 -0
  12. package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
  13. package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
  14. package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
  15. package/chunks/adapt-loop-ECQG7454.js +18 -0
  16. package/chunks/adgraph-JLGA6RYI.js +54 -0
  17. package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
  18. package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
  19. package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
  20. package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
  21. package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
  22. package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
  23. package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
  24. package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
  25. package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
  26. package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
  27. package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
  28. package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
  29. package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
  30. package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
  31. package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
  32. package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
  33. package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
  34. package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
  35. package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
  36. package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
  37. package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
  38. package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
  39. package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
  40. package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
  41. package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
  42. package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
  43. package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
  44. package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
  45. package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
  46. package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
  47. package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
  48. package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
  49. package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
  50. package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
  51. package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
  52. package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
  53. package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
  54. package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
  55. package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
  56. package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
  57. package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
  58. package/chunks/artifact-scraper-GJEZMOHF.js +34 -0
  59. package/chunks/assumption-mining-BVMMUZHB.js +79 -0
  60. package/chunks/chunk-2CJ776PV.js +2430 -0
  61. package/chunks/chunk-2RMLOJVB.js +47 -0
  62. package/chunks/chunk-2SANI5RH.js +4728 -0
  63. package/chunks/chunk-3MOLBTLS.js +953 -0
  64. package/chunks/chunk-3QFDYBZQ.js +5108 -0
  65. package/chunks/chunk-47TQJWDH.js +3729 -0
  66. package/chunks/chunk-4Y4KQJXE.js +600 -0
  67. package/chunks/chunk-53G27VPS.js +136 -0
  68. package/chunks/chunk-57ZENEX2.js +169 -0
  69. package/chunks/chunk-5G3ZXFBW.js +182 -0
  70. package/chunks/chunk-6LKRLK2R.js +542 -0
  71. package/chunks/chunk-7DQEV5QI.js +287 -0
  72. package/chunks/chunk-A6CLR72I.js +235 -0
  73. package/chunks/chunk-BKZFDZ23.js +361 -0
  74. package/chunks/chunk-CLHCDHP4.js +471 -0
  75. package/chunks/chunk-D6S3IIPG.js +518 -0
  76. package/chunks/chunk-DN25OQQA.js +1070 -0
  77. package/chunks/chunk-DW5UWPFY.js +275 -0
  78. package/chunks/chunk-F3WBKITT.js +687 -0
  79. package/chunks/chunk-H2FFLZNK.js +3 -0
  80. package/chunks/chunk-H44E2CRN.js +193 -0
  81. package/chunks/chunk-HDWV7PGI.js +596 -0
  82. package/chunks/chunk-HE7LCA7G.js +381 -0
  83. package/chunks/chunk-HT6P7RY3.js +2597 -0
  84. package/chunks/chunk-IOL7D5YV.js +515 -0
  85. package/chunks/chunk-IORC6MQX.js +72828 -0
  86. package/chunks/chunk-IR537GON.js +49 -0
  87. package/chunks/chunk-K26SZ37E.js +5735 -0
  88. package/chunks/chunk-KKGE5RQE.js +1094 -0
  89. package/chunks/chunk-KLTNTE2Z.js +121 -0
  90. package/chunks/chunk-KTQDLNSR.js +1732 -0
  91. package/chunks/chunk-LOHTE223.js +2572 -0
  92. package/chunks/chunk-LP3HHYQU.js +245 -0
  93. package/chunks/chunk-MMLDQR4H.js +852 -0
  94. package/chunks/chunk-MYVT64FN.js +16 -0
  95. package/chunks/chunk-O462Y7P2.js +33019 -0
  96. package/chunks/chunk-OEFNRYI2.js +150 -0
  97. package/chunks/chunk-P6WKNFWX.js +1968 -0
  98. package/chunks/chunk-QI233I24.js +333 -0
  99. package/chunks/chunk-QJQKHO7G.js +917 -0
  100. package/chunks/chunk-QKICO43A.js +808 -0
  101. package/chunks/chunk-QOKTUOU2.js +102 -0
  102. package/chunks/chunk-RDZYQQLW.js +596 -0
  103. package/chunks/chunk-RJWOYEOG.js +271 -0
  104. package/chunks/chunk-RRMJC3ZE.js +105 -0
  105. package/chunks/chunk-RWONANDA.js +973 -0
  106. package/chunks/chunk-SAFFWQW4.js +5921 -0
  107. package/chunks/chunk-SF4KZ4O3.js +502 -0
  108. package/chunks/chunk-SNKTC4BP.js +1422 -0
  109. package/chunks/chunk-SOG2U7B3.js +662 -0
  110. package/chunks/chunk-SZJCPG2I.js +110 -0
  111. package/chunks/chunk-UJ4IK5PO.js +2010 -0
  112. package/chunks/chunk-UM3ZNQIM.js +9329 -0
  113. package/chunks/chunk-VGRDNSHA.js +3249 -0
  114. package/chunks/chunk-VQG4FT5D.js +685 -0
  115. package/chunks/chunk-WKNNZVJS.js +2798 -0
  116. package/chunks/chunk-WU6AFRAZ.js +1167 -0
  117. package/chunks/chunk-WVTBZEQO.js +1531 -0
  118. package/chunks/chunk-YLMN3N25.js +4054 -0
  119. package/chunks/commands-U3AMV5ZU.js +16795 -0
  120. package/chunks/corpus-v1.json +403 -0
  121. package/chunks/cost-ledger-ZCKMBJEN.js +13 -0
  122. package/chunks/data/appsec-archetypes.json +102 -0
  123. package/chunks/data/chromium-archetypes.json +204 -0
  124. package/chunks/data/freebsd-archetypes.json +171 -0
  125. package/chunks/data/kernel-archetypes.json +611 -0
  126. package/chunks/db-26NGQFKO.js +16 -0
  127. package/chunks/disclose-A6AEIEDA.js +132 -0
  128. package/chunks/dist-FDKALV4R.js +2772 -0
  129. package/chunks/dist-GL66KMUX.js +74 -0
  130. package/chunks/dist-JR67XKYO.js +159 -0
  131. package/chunks/eval-runner-DDLE5RQ3.js +27 -0
  132. package/chunks/example-manifest.json +95 -0
  133. package/chunks/exploit-agent-XP23ISN7.js +98 -0
  134. package/chunks/exploit-autoclimb-E6T7RRSC.js +81 -0
  135. package/chunks/exploit-climb-2FAUP6XU.js +358 -0
  136. package/chunks/fix-IG7LYXH3.js +12 -0
  137. package/chunks/github-issues-MJ6OYOOU.js +157 -0
  138. package/chunks/harness-HWOYFKFY.js +22 -0
  139. package/chunks/http-conformance-DU66MZIU.js +11 -0
  140. package/chunks/http-sender-GWH2IYEA.js +10 -0
  141. package/chunks/hunt-scan-XPELSPQQ.js +38 -0
  142. package/chunks/identity-6ZAIIWOR.js +191 -0
  143. package/chunks/kernel-primitive-TENE3R7T.js +44 -0
  144. package/chunks/kernel-vm-runner-4F6QSNFY.js +62 -0
  145. package/chunks/memsafety-scan-J7SECV5W.js +16 -0
  146. package/chunks/native-loop-NTONUDP7.js +54 -0
  147. package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
  148. package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
  149. package/chunks/orchestrate-ZUWAUWBC.js +62 -0
  150. package/chunks/pipeline-FCARFZU3.js +14 -0
  151. package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
  152. package/chunks/prepare-MYY743TK.js +13 -0
  153. package/chunks/process-3Q7QJIOZ.js +13 -0
  154. package/chunks/replay-runner-RRR4E2A7.js +39 -0
  155. package/chunks/run-ESNN4V5W.js +32632 -0
  156. package/chunks/runtime-62ZQAH7H.js +43 -0
  157. package/chunks/runtime-J7PLXZNM.js +12 -0
  158. package/chunks/scan-stream-FHI2FYZE.js +96 -0
  159. package/chunks/scope-BI7BF4ZY.js +18 -0
  160. package/chunks/session-store-BCFQYDCE.js +28 -0
  161. package/chunks/source-files-PWRQR6LY.js +12 -0
  162. package/chunks/specdrift-WCLH6TTQ.js +19 -0
  163. package/chunks/variant-candidates-6KM6F6MD.js +13 -0
  164. package/chunks/web-recon-prepass-DPVCBRZA.js +1148 -0
  165. package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
  166. package/dashboard/assets/bot-yILPnRgD.js +1 -0
  167. package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
  168. package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
  169. package/dashboard/assets/client-DRYsuOGl.js +9 -0
  170. package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
  171. package/dashboard/assets/desktop-CXTQonNm.js +32 -0
  172. package/dashboard/assets/desktop-NOekk4QS.css +1 -0
  173. package/dashboard/assets/dist-CmX-h7a4.js +1 -0
  174. package/dashboard/assets/dist-DAwYQmf_.js +45 -0
  175. package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
  176. package/dashboard/assets/format-PoISqsES.js +1 -0
  177. package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  178. package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  179. package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  180. package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  181. package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  182. package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  183. package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  184. package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  185. package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  186. package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  187. package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  188. package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  189. package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  190. package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  191. package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  192. package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
  193. package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
  194. package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
  195. package/dashboard/assets/operations-BKE8T-vY.css +2 -0
  196. package/dashboard/assets/operations-D8nUP5_m.js +4 -0
  197. package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
  198. package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
  199. package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
  200. package/dashboard/assets/play-Dwo-9xt6.js +1 -0
  201. package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
  202. package/dashboard/assets/search-CiooTLck.js +1 -0
  203. package/dashboard/assets/siren-CCBwTSuk.js +1 -0
  204. package/dashboard/assets/table-BYS-nIVA.js +1 -0
  205. package/dashboard/assets/tabs-DivxbhQy.js +1 -0
  206. package/dashboard/desktop.html +21 -0
  207. package/dashboard/index.html +15 -0
  208. package/package.json +30 -18
  209. package/bin/0sec.cjs +0 -361
@@ -0,0 +1,2010 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __0secCreateRequire } from "node:module";
3
+ const require = __0secCreateRequire(import.meta.url);
4
+ import {
5
+ analyzeFindingForKnownMarkers,
6
+ detectKnownMarkers,
7
+ extractFileRefs,
8
+ redactSensitiveHeaders,
9
+ require_dist
10
+ } from "./chunk-UM3ZNQIM.js";
11
+ import {
12
+ __toESM
13
+ } from "./chunk-IR537GON.js";
14
+
15
+ // packages/core/dist/disclose/screenshots.js
16
+ import { execFileSync } from "node:child_process";
17
+ import { writeFileSync, mkdirSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ var DEFAULT_OPTS = {
20
+ binary: "freeze",
21
+ theme: "dracula",
22
+ width: 1200,
23
+ fontSize: 14,
24
+ background: "#0f1117"
25
+ };
26
+ function slugify(input, max = 40) {
27
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max);
28
+ }
29
+ function isFreezeAvailable(binary = DEFAULT_OPTS.binary) {
30
+ try {
31
+ execFileSync(process.platform === "win32" ? "where" : "which", [binary], { stdio: "ignore" });
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ function composeExploitSession(finding) {
38
+ const lines = [];
39
+ lines.push(`$ # PoC for: ${finding.title}`);
40
+ lines.push(`$ # Category: ${finding.category} | severity: ${finding.severity}`);
41
+ lines.push("");
42
+ const request = finding.evidence?.request?.trim();
43
+ const response = finding.evidence?.response?.trim();
44
+ if (request) {
45
+ const requestLines = redactSensitiveHeaders(request).split("\n");
46
+ lines.push(`$ ${requestLines[0]}`);
47
+ for (const line of requestLines.slice(1)) {
48
+ lines.push(` ${line}`);
49
+ }
50
+ lines.push("");
51
+ }
52
+ if (response) {
53
+ lines.push(redactSensitiveHeaders(response));
54
+ }
55
+ const analysis = finding.evidence?.analysis?.trim();
56
+ if (analysis) {
57
+ lines.push("");
58
+ lines.push("# Agent analysis:");
59
+ for (const line of analysis.split("\n")) {
60
+ lines.push(`# ${line}`);
61
+ }
62
+ }
63
+ return lines.join("\n");
64
+ }
65
+ function composeStepSession(finding, step, index, total, result) {
66
+ const lines = [];
67
+ lines.push(`$ # PoC step ${index + 1}/${total} \u2014 ${step.kind}: ${step.summary}`);
68
+ lines.push(`$ # Finding: ${finding.title}`);
69
+ lines.push("");
70
+ switch (step.action.type) {
71
+ case "shell": {
72
+ const cmdLines = step.action.cmd.split("\n");
73
+ lines.push(`$ ${cmdLines[0]}`);
74
+ for (const l of cmdLines.slice(1))
75
+ lines.push(` ${l}`);
76
+ break;
77
+ }
78
+ case "http": {
79
+ lines.push(`$ ${step.action.method.toUpperCase()} ${step.action.url}`);
80
+ if (step.action.headers) {
81
+ for (const [k, v] of Object.entries(step.action.headers)) {
82
+ const headerLine = redactSensitiveHeaders(`${k}: ${v}`);
83
+ lines.push(` ${headerLine}`);
84
+ }
85
+ }
86
+ if (step.action.body) {
87
+ lines.push("");
88
+ const redactedBody = redactSensitiveHeaders(step.action.body);
89
+ for (const l of redactedBody.split("\n"))
90
+ lines.push(` ${l}`);
91
+ }
92
+ break;
93
+ }
94
+ case "docker": {
95
+ lines.push(`$ docker run --rm ${step.action.args.join(" ")} ${step.action.image}`);
96
+ break;
97
+ }
98
+ case "note": {
99
+ lines.push(`$ # (note) ${step.action.text}`);
100
+ break;
101
+ }
102
+ }
103
+ if (result) {
104
+ lines.push("");
105
+ if (result.observedExit !== void 0) {
106
+ lines.push(`# exit=${result.observedExit}`);
107
+ }
108
+ if (result.observedStatus !== void 0) {
109
+ lines.push(`# http-status=${result.observedStatus}`);
110
+ }
111
+ if (result.observedStdout && result.observedStdout.trim().length > 0) {
112
+ for (const l of redactSensitiveHeaders(result.observedStdout).split("\n"))
113
+ lines.push(l);
114
+ }
115
+ if (result.observedResponseBody && result.observedResponseBody.trim().length > 0) {
116
+ for (const l of redactSensitiveHeaders(result.observedResponseBody).split("\n"))
117
+ lines.push(l);
118
+ }
119
+ if (result.observedStderr && result.observedStderr.trim().length > 0) {
120
+ lines.push("");
121
+ for (const l of redactSensitiveHeaders(result.observedStderr).split("\n"))
122
+ lines.push(`# stderr: ${l}`);
123
+ }
124
+ lines.push("");
125
+ lines.push(`# verdict: ${result.kind}${result.error ? ` (${result.error})` : ""}`);
126
+ } else {
127
+ if (step.expect) {
128
+ lines.push("");
129
+ lines.push(`# expected: ${describeExpect(step.expect)}`);
130
+ }
131
+ }
132
+ return lines.join("\n");
133
+ }
134
+ function describeExpect(expect) {
135
+ switch (expect.type) {
136
+ case "exit-zero":
137
+ return "exit-zero";
138
+ case "http-status": {
139
+ const s = Array.isArray(expect.status) ? expect.status.join(",") : expect.status;
140
+ return `http-status \u2208 {${s}}`;
141
+ }
142
+ case "body-contains":
143
+ return `body contains "${expect.text}"`;
144
+ case "body-matches":
145
+ return `body matches /${expect.pattern}/`;
146
+ case "file-exists":
147
+ return `file exists at ${expect.path}`;
148
+ }
149
+ }
150
+ function freezeSessionToPng(sessionFile, pngPath, opts) {
151
+ try {
152
+ execFileSync(opts.binary, [
153
+ sessionFile,
154
+ "--language",
155
+ "bash",
156
+ "--theme",
157
+ opts.theme,
158
+ "--window",
159
+ "--padding",
160
+ "20,30",
161
+ "--margin",
162
+ "10",
163
+ "--background",
164
+ opts.background,
165
+ "--font.size",
166
+ String(opts.fontSize),
167
+ "--width",
168
+ String(opts.width),
169
+ "-o",
170
+ pngPath
171
+ ], { stdio: "ignore" });
172
+ return true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ function relativiseFrom(pngPath, markdownDir) {
178
+ if (!markdownDir)
179
+ return pngPath;
180
+ if (pngPath.startsWith(markdownDir))
181
+ return "." + pngPath.slice(markdownDir.length);
182
+ return pngPath;
183
+ }
184
+ function renderExploitScreenshot(finding, options) {
185
+ const opts = { ...DEFAULT_OPTS, ...options };
186
+ const available = options.available ?? isFreezeAvailable(opts.binary);
187
+ if (options.pocSteps !== void 0) {
188
+ if (!available || options.pocSteps.length === 0)
189
+ return [];
190
+ mkdirSync(opts.outputDir, { recursive: true });
191
+ const baseSlug = slugify(`${finding.severity}-${finding.id.slice(0, 8)}-${finding.title}`);
192
+ const frames = [];
193
+ for (let i = 0; i < options.pocSteps.length; i++) {
194
+ const step = options.pocSteps[i];
195
+ const result = options.stepResults?.[step.id];
196
+ const stepSlug = `${baseSlug}-step-${i + 1}-${slugify(step.id, 30)}`;
197
+ const sessionText2 = composeStepSession(finding, step, i, options.pocSteps.length, result);
198
+ const sessionFile2 = join(opts.outputDir, `${stepSlug}.session.txt`);
199
+ const pngPath2 = join(opts.outputDir, `${stepSlug}.png`);
200
+ writeFileSync(sessionFile2, sessionText2, "utf8");
201
+ const ok2 = freezeSessionToPng(sessionFile2, pngPath2, opts);
202
+ if (!ok2)
203
+ continue;
204
+ frames.push({
205
+ alt: `exploit-${stepSlug}`,
206
+ path: pngPath2,
207
+ relativePath: relativiseFrom(pngPath2, options.markdownDir),
208
+ caption: `${i + 1}. ${step.summary}`,
209
+ sessionText: sessionText2,
210
+ stepId: step.id,
211
+ frame: i + 1
212
+ });
213
+ }
214
+ return frames;
215
+ }
216
+ if (!available)
217
+ return null;
218
+ mkdirSync(opts.outputDir, { recursive: true });
219
+ const slug = slugify(`${finding.severity}-${finding.id.slice(0, 8)}-${finding.title}`);
220
+ const sessionText = composeExploitSession(finding);
221
+ const sessionFile = join(opts.outputDir, `${slug}.session.txt`);
222
+ const pngPath = join(opts.outputDir, `${slug}.png`);
223
+ writeFileSync(sessionFile, sessionText, "utf8");
224
+ const ok = freezeSessionToPng(sessionFile, pngPath, opts);
225
+ if (!ok)
226
+ return null;
227
+ return {
228
+ alt: `exploit-${slug}`,
229
+ path: pngPath,
230
+ relativePath: relativiseFrom(pngPath, options.markdownDir),
231
+ caption: finding.title,
232
+ sessionText
233
+ };
234
+ }
235
+
236
+ // packages/core/dist/disclose/sibling-fix.js
237
+ import { existsSync, readFileSync } from "node:fs";
238
+ import { resolve, join as join2, extname } from "node:path";
239
+ var SIBLING_CUES = [
240
+ { pattern: /\bthe\s+right\s+gate\s+is\b/i, weight: 0.95, label: "the right gate is" },
241
+ { pattern: /\bcorrect\s+pattern\b/i, weight: 0.9, label: "correct pattern" },
242
+ { pattern: /\buses?\s+the\s+correct\b/i, weight: 0.9, label: "uses the correct" },
243
+ { pattern: /\bproperly\s+checks?\b/i, weight: 0.85, label: "properly checks" },
244
+ { pattern: /\bsafely\s+handles?\b/i, weight: 0.85, label: "safely handles" },
245
+ { pattern: /\bgated\s+on\b/i, weight: 0.8, label: "gated on" },
246
+ { pattern: /\bsibling\b/i, weight: 0.8, label: "sibling" },
247
+ { pattern: /\bcorrectly\s+(?:gated|checks?|handles?|validates?)\b/i, weight: 0.85, label: "correctly gated/checks/handles" },
248
+ { pattern: /\bsafe\s+(?:handler|sibling|version)\b/i, weight: 0.8, label: "safe handler/sibling/version" },
249
+ { pattern: /\bright\s+(?:check|guard|gate)\b/i, weight: 0.8, label: "right check/guard/gate" }
250
+ ];
251
+ var VULNERABLE_CUES = [
252
+ /\bgates?\s+on\s+\w+\s+instead\s+of\b/i,
253
+ /\bvulnerable\s+at\b/i,
254
+ /\bwhere\s+the\s+\w+\s+(?:happens|occurs)\b/i,
255
+ /\bmissing\s+\w+/i,
256
+ /\bbug\s+is\s+at\b/i,
257
+ /\binstead\s+of\s+\w+/i
258
+ ];
259
+ var LANGUAGE_BY_EXT = {
260
+ ".ts": "typescript",
261
+ ".tsx": "typescript",
262
+ ".js": "javascript",
263
+ ".jsx": "javascript",
264
+ ".mjs": "javascript",
265
+ ".cjs": "javascript",
266
+ ".py": "python",
267
+ ".rs": "rust",
268
+ ".go": "go",
269
+ ".rb": "ruby",
270
+ ".java": "java",
271
+ ".kt": "kotlin",
272
+ ".swift": "swift",
273
+ ".c": "c",
274
+ ".h": "c",
275
+ ".cc": "cpp",
276
+ ".cpp": "cpp",
277
+ ".php": "php",
278
+ ".sh": "bash",
279
+ ".sql": "sql"
280
+ };
281
+ function languageFor(filePath) {
282
+ return LANGUAGE_BY_EXT[extname(filePath).toLowerCase()] ?? "";
283
+ }
284
+ function buildHaystack(finding) {
285
+ return [
286
+ finding.description,
287
+ finding.evidence?.analysis,
288
+ finding.evidence?.request,
289
+ finding.evidence?.response
290
+ ].filter((s) => typeof s === "string" && s.length > 0).join("\n\n");
291
+ }
292
+ function sentenceAround(haystack, refToken) {
293
+ const idx = haystack.indexOf(refToken);
294
+ if (idx === -1)
295
+ return "";
296
+ let start = idx;
297
+ while (start > 0) {
298
+ const c = haystack[start - 1];
299
+ if (c === "." || c === "!" || c === "?" || c === "\n")
300
+ break;
301
+ start--;
302
+ }
303
+ let end = idx + refToken.length;
304
+ while (end < haystack.length) {
305
+ const c = haystack[end];
306
+ if (c === "." || c === "!" || c === "?" || c === "\n")
307
+ break;
308
+ end++;
309
+ }
310
+ return haystack.slice(start, end + 1);
311
+ }
312
+ function classifyRef(haystack, ref) {
313
+ const token = `${ref.file}${ref.line !== void 0 ? `:${ref.line}` : ""}`;
314
+ const window = sentenceAround(haystack, token);
315
+ let siblingHit;
316
+ for (const cue of SIBLING_CUES) {
317
+ if (cue.pattern.test(window)) {
318
+ if (!siblingHit || cue.weight > siblingHit.weight) {
319
+ siblingHit = { weight: cue.weight, label: cue.label };
320
+ }
321
+ }
322
+ }
323
+ const vulnerableHit = VULNERABLE_CUES.some((p) => p.test(window));
324
+ return { ref, siblingHit, vulnerableHit };
325
+ }
326
+ function readSnippet(repoPath, ref, linesOfContext) {
327
+ if (ref.line === void 0)
328
+ return null;
329
+ const abs = join2(repoPath, ref.file);
330
+ if (!existsSync(abs))
331
+ return null;
332
+ let raw;
333
+ try {
334
+ raw = readFileSync(abs, "utf8");
335
+ } catch {
336
+ return null;
337
+ }
338
+ const lines = raw.split("\n");
339
+ if (ref.line < 1 || ref.line > lines.length)
340
+ return null;
341
+ const start = Math.max(0, ref.line - 1 - linesOfContext);
342
+ const end = Math.min(lines.length, ref.line + linesOfContext);
343
+ return lines.slice(start, end).join("\n");
344
+ }
345
+ var CONFIDENCE_FLOOR = 0.6;
346
+ function extractSiblingFix(finding, options) {
347
+ if (!options.repoPath)
348
+ return null;
349
+ const repoPath = resolve(options.repoPath);
350
+ if (!existsSync(repoPath))
351
+ return null;
352
+ const linesOfContext = options.linesOfContext ?? 5;
353
+ const refs = extractFileRefs(finding);
354
+ if (refs.length === 0)
355
+ return null;
356
+ const haystack = buildHaystack(finding);
357
+ const classified = refs.map((ref) => classifyRef(haystack, ref));
358
+ let best = null;
359
+ for (const c of classified) {
360
+ if (!c.siblingHit)
361
+ continue;
362
+ const base = c.siblingHit.weight;
363
+ const confidence = c.vulnerableHit ? base * 0.5 : base;
364
+ const rationale = c.vulnerableHit ? `Matched sibling cue "${c.siblingHit.label}" in surrounding prose, but also matched a vulnerable-ref cue \u2014 confidence halved.` : `Matched sibling cue "${c.siblingHit.label}" in the sentence containing the ref.`;
365
+ if (!best || confidence > best.confidence) {
366
+ best = { c, confidence, rationale };
367
+ }
368
+ }
369
+ if (!best || best.confidence < CONFIDENCE_FLOOR)
370
+ return null;
371
+ const snippet = readSnippet(repoPath, best.c.ref, linesOfContext);
372
+ if (snippet === null)
373
+ return null;
374
+ return {
375
+ fileRef: best.c.ref,
376
+ snippet,
377
+ language: languageFor(best.c.ref.file),
378
+ confidence: best.confidence,
379
+ rationale: best.rationale
380
+ };
381
+ }
382
+
383
+ // packages/core/dist/disclose/poc-runtime.js
384
+ import { spawn } from "node:child_process";
385
+ var MAX_CAPTURE_BYTES = 1024 * 1024;
386
+ var DEFAULT_STEP_TIMEOUT_MS = 3e4;
387
+ var PERSONA_HEADER = "X-0sec-Persona";
388
+ var TRUNCATION_MARKER = "\n\u2026[truncated at 1MiB]";
389
+ var DEFAULT_RPS_PER_HOST = 2;
390
+ var MAX_REDIRECT_HOPS = 5;
391
+ var hostBuckets = /* @__PURE__ */ new Map();
392
+ function getOrInitBucket(host, rps) {
393
+ let bucket = hostBuckets.get(host);
394
+ if (!bucket) {
395
+ bucket = {
396
+ tokens: rps,
397
+ // full bucket on first use
398
+ refillRatePerMs: rps / 1e3,
399
+ capacity: rps,
400
+ lastRefill: Date.now(),
401
+ retryUntil: 0
402
+ };
403
+ hostBuckets.set(host, bucket);
404
+ }
405
+ return bucket;
406
+ }
407
+ function refill(bucket) {
408
+ const now = Date.now();
409
+ const elapsed = now - bucket.lastRefill;
410
+ if (elapsed > 0) {
411
+ bucket.tokens = Math.min(bucket.capacity, bucket.tokens + elapsed * bucket.refillRatePerMs);
412
+ bucket.lastRefill = now;
413
+ }
414
+ }
415
+ async function acquireHostToken(host, rps) {
416
+ const bucket = getOrInitBucket(host, rps);
417
+ while (Date.now() < bucket.retryUntil) {
418
+ const waitMs = bucket.retryUntil - Date.now();
419
+ await sleep(Math.min(waitMs, 1e3));
420
+ }
421
+ while (true) {
422
+ refill(bucket);
423
+ if (bucket.tokens >= 1) {
424
+ bucket.tokens -= 1;
425
+ return;
426
+ }
427
+ const tokensNeeded = 1 - bucket.tokens;
428
+ const waitMs = Math.max(1, Math.ceil(tokensNeeded / bucket.refillRatePerMs));
429
+ await sleep(waitMs);
430
+ }
431
+ }
432
+ function markHostRateLimited(host, retryAfterHeader) {
433
+ const bucket = hostBuckets.get(host);
434
+ if (!bucket)
435
+ return;
436
+ const parsed = retryAfterHeader ? parseInt(retryAfterHeader, 10) : NaN;
437
+ const retryAfterMs = Number.isFinite(parsed) && parsed > 0 ? parsed * 1e3 : 0;
438
+ bucket.retryUntil = Date.now() + Math.max(retryAfterMs, 6e4);
439
+ }
440
+ function sleep(ms) {
441
+ return new Promise((r) => setTimeout(r, ms));
442
+ }
443
+ function normalizeHostForMatch(value) {
444
+ const trimmed = value.trim().toLowerCase();
445
+ if (trimmed.startsWith("[")) {
446
+ const end = trimmed.indexOf("]");
447
+ return end === -1 ? trimmed : trimmed.slice(1, end);
448
+ }
449
+ if ((trimmed.match(/:/g) ?? []).length >= 2)
450
+ return trimmed;
451
+ return trimmed.replace(/:\d+$/, "");
452
+ }
453
+ function hostMatchesAllowlist(host, allowlist) {
454
+ if (!allowlist || allowlist.length === 0)
455
+ return true;
456
+ const target = normalizeHostForMatch(host);
457
+ for (const raw of allowlist) {
458
+ const pattern = normalizeHostForMatch(raw);
459
+ if (!pattern)
460
+ continue;
461
+ if (pattern.startsWith("*.")) {
462
+ const suffix = pattern.slice(1);
463
+ if (target.endsWith(suffix) && target.length > suffix.length)
464
+ return true;
465
+ } else if (target === pattern) {
466
+ return true;
467
+ }
468
+ }
469
+ return false;
470
+ }
471
+ function scopeRequiredResult(step, target, start) {
472
+ if (target.scopeAllowlist && target.scopeAllowlist.length > 0)
473
+ return void 0;
474
+ return {
475
+ stepId: step.id,
476
+ kind: "errored",
477
+ durationMs: Date.now() - start,
478
+ error: "PoC reverify requires a non-empty scope allowlist"
479
+ };
480
+ }
481
+ function extractUrlsFromShellCommand(cmd) {
482
+ const matches = cmd.match(/https?:\/\/[^\s'"]+/g);
483
+ return matches ?? [];
484
+ }
485
+ function dockerRegistryHost(image) {
486
+ const separator = image.indexOf("/");
487
+ if (separator === -1)
488
+ return void 0;
489
+ const firstSegment = image.slice(0, separator);
490
+ return firstSegment.includes(".") || firstSegment.includes(":") || firstSegment === "localhost" ? firstSegment : void 0;
491
+ }
492
+ var deps = { spawn, fetch: globalThis.fetch };
493
+ function setRuntimeDeps(overrides) {
494
+ const previous = deps;
495
+ deps = { ...previous, ...overrides };
496
+ return () => {
497
+ deps = previous;
498
+ };
499
+ }
500
+ async function executePocSteps(finding, target) {
501
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
502
+ const steps = [];
503
+ const graph = finding.pocSteps ?? [];
504
+ for (const step of graph) {
505
+ const result = await executeStep(step, target);
506
+ steps.push(result);
507
+ if (result.kind === "errored" && step.kind !== "verify") {
508
+ for (let i = graph.indexOf(step) + 1; i < graph.length; i++) {
509
+ steps.push({
510
+ stepId: graph[i].id,
511
+ kind: "skipped",
512
+ durationMs: 0,
513
+ error: "skipped: an earlier step errored"
514
+ });
515
+ }
516
+ break;
517
+ }
518
+ }
519
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
520
+ return {
521
+ findingId: finding.id,
522
+ startedAt,
523
+ endedAt,
524
+ steps,
525
+ overallVerdict: aggregateVerdict(graph, steps)
526
+ };
527
+ }
528
+ async function executeStep(step, target) {
529
+ const start = Date.now();
530
+ if (target.allowProcessActions === false && (step.action.type === "shell" || step.action.type === "docker")) {
531
+ return {
532
+ stepId: step.id,
533
+ kind: "errored",
534
+ durationMs: Date.now() - start,
535
+ error: `PoC ${step.action.type} actions require an isolated execution environment`
536
+ };
537
+ }
538
+ try {
539
+ switch (step.action.type) {
540
+ case "shell":
541
+ return await runShellStep(step, step.action, target, start);
542
+ case "http":
543
+ return await runHttpStep(step, step.action, target, start);
544
+ case "docker":
545
+ return await runDockerStep(step, step.action, target, start);
546
+ case "note":
547
+ return {
548
+ stepId: step.id,
549
+ kind: "skipped",
550
+ durationMs: Date.now() - start
551
+ };
552
+ default: {
553
+ const _exhaustive = step.action;
554
+ return {
555
+ stepId: step.id,
556
+ kind: "errored",
557
+ durationMs: Date.now() - start,
558
+ error: `unknown action type: ${JSON.stringify(_exhaustive)}`
559
+ };
560
+ }
561
+ }
562
+ } catch (err) {
563
+ return {
564
+ stepId: step.id,
565
+ kind: "errored",
566
+ durationMs: Date.now() - start,
567
+ error: errMessage(err)
568
+ };
569
+ }
570
+ }
571
+ async function runShellStep(step, action, target, start) {
572
+ const scopeError = scopeRequiredResult(step, target, start);
573
+ if (scopeError)
574
+ return scopeError;
575
+ if (target.scopeAllowlist && target.scopeAllowlist.length > 0) {
576
+ for (const u of extractUrlsFromShellCommand(action.cmd)) {
577
+ let host;
578
+ try {
579
+ host = new URL(u).host;
580
+ } catch {
581
+ continue;
582
+ }
583
+ if (!hostMatchesAllowlist(host, target.scopeAllowlist)) {
584
+ return {
585
+ stepId: step.id,
586
+ kind: "errored",
587
+ durationMs: Date.now() - start,
588
+ error: `out-of-scope url in shell cmd: ${u} (allowlist: ${target.scopeAllowlist.join(", ")})`
589
+ };
590
+ }
591
+ }
592
+ }
593
+ const env = { ...process.env, ...target.env ?? {} };
594
+ const cwd = action.cwd ?? target.cwd;
595
+ const timeoutMs = target.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
596
+ const spawned = await spawnAndCapture("/bin/sh", ["-c", action.cmd], { env, cwd, timeoutMs });
597
+ return finaliseProcessStep(step, spawned, start);
598
+ }
599
+ async function runDockerStep(step, action, target, start) {
600
+ const scopeError = scopeRequiredResult(step, target, start);
601
+ if (scopeError)
602
+ return scopeError;
603
+ const registryHost = dockerRegistryHost(action.image);
604
+ if (registryHost && !hostMatchesAllowlist(registryHost, target.scopeAllowlist)) {
605
+ return {
606
+ stepId: step.id,
607
+ kind: "errored",
608
+ durationMs: Date.now() - start,
609
+ error: `out-of-scope docker registry: ${registryHost} (allowlist: ${target.scopeAllowlist.join(", ")})`
610
+ };
611
+ }
612
+ if (target.scopeAllowlist && target.scopeAllowlist.length > 0) {
613
+ const dockerTokens = [action.image, ...action.args].join(" ");
614
+ for (const u of extractUrlsFromShellCommand(dockerTokens)) {
615
+ let host;
616
+ try {
617
+ host = new URL(u).host;
618
+ } catch {
619
+ continue;
620
+ }
621
+ if (!hostMatchesAllowlist(host, target.scopeAllowlist)) {
622
+ return {
623
+ stepId: step.id,
624
+ kind: "errored",
625
+ durationMs: Date.now() - start,
626
+ error: `out-of-scope url in docker action: ${u} (allowlist: ${target.scopeAllowlist.join(", ")})`
627
+ };
628
+ }
629
+ }
630
+ }
631
+ const env = { ...process.env, ...target.env ?? {} };
632
+ const cwd = target.cwd;
633
+ const timeoutMs = target.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
634
+ const spawned = await spawnAndCapture("docker", ["run", "--rm", ...action.args, action.image], { env, cwd, timeoutMs });
635
+ return finaliseProcessStep(step, spawned, start);
636
+ }
637
+ async function runHttpStep(step, action, target, start) {
638
+ const scopeError = scopeRequiredResult(step, target, start);
639
+ if (scopeError)
640
+ return scopeError;
641
+ const url = resolveUrl(action.url, target.baseUrl);
642
+ if (!url) {
643
+ return {
644
+ stepId: step.id,
645
+ kind: "errored",
646
+ durationMs: Date.now() - start,
647
+ error: `http step has a relative url (${action.url}) but no target.baseUrl was provided`
648
+ };
649
+ }
650
+ let host;
651
+ try {
652
+ host = new URL(url).host;
653
+ } catch {
654
+ return {
655
+ stepId: step.id,
656
+ kind: "errored",
657
+ durationMs: Date.now() - start,
658
+ error: `http step url is not a valid URL: ${url}`
659
+ };
660
+ }
661
+ if (!hostMatchesAllowlist(host, target.scopeAllowlist)) {
662
+ return {
663
+ stepId: step.id,
664
+ kind: "errored",
665
+ durationMs: Date.now() - start,
666
+ error: `out-of-scope host: ${host} (allowlist: ${target.scopeAllowlist.join(", ")})`
667
+ };
668
+ }
669
+ const rps = target.rpsPerHost ?? DEFAULT_RPS_PER_HOST;
670
+ await acquireHostToken(host, rps);
671
+ const headers = mergePersonaHeaders(action.headers ?? {}, target);
672
+ const timeoutMs = target.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
673
+ const ac = new AbortController();
674
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
675
+ let finalResponse;
676
+ let body = "";
677
+ let currentUrl = url;
678
+ let redirectCount = 0;
679
+ try {
680
+ while (true) {
681
+ finalResponse = await deps.fetch(currentUrl, {
682
+ method: action.method,
683
+ headers,
684
+ body: action.body,
685
+ signal: ac.signal,
686
+ redirect: "manual"
687
+ });
688
+ const status = finalResponse.status;
689
+ const isRedirect = [301, 302, 303, 307, 308].includes(status);
690
+ if (!isRedirect) {
691
+ body = await readBodyCapped(finalResponse);
692
+ if (status === 429) {
693
+ const retryAfter = finalResponse.headers.get("retry-after");
694
+ markHostRateLimited(host, retryAfter);
695
+ }
696
+ break;
697
+ }
698
+ redirectCount++;
699
+ if (redirectCount > MAX_REDIRECT_HOPS) {
700
+ try {
701
+ finalResponse.body?.cancel();
702
+ } catch {
703
+ }
704
+ clearTimeout(timer);
705
+ return {
706
+ stepId: step.id,
707
+ kind: "errored",
708
+ durationMs: Date.now() - start,
709
+ error: `too many redirects (exceeded ${MAX_REDIRECT_HOPS} hops)`
710
+ };
711
+ }
712
+ const location = finalResponse.headers.get("location");
713
+ if (!location) {
714
+ body = await readBodyCapped(finalResponse);
715
+ break;
716
+ }
717
+ const nextUrlStr = new URL(location, currentUrl).toString();
718
+ const nextHost = new URL(nextUrlStr).host;
719
+ if (!hostMatchesAllowlist(nextHost, target.scopeAllowlist)) {
720
+ try {
721
+ finalResponse.body?.cancel();
722
+ } catch {
723
+ }
724
+ clearTimeout(timer);
725
+ return {
726
+ stepId: step.id,
727
+ kind: "errored",
728
+ durationMs: Date.now() - start,
729
+ error: `redirect to out-of-scope host: ${nextHost} (allowlist: ${target.scopeAllowlist.join(", ")})`
730
+ };
731
+ }
732
+ try {
733
+ finalResponse.body?.cancel();
734
+ } catch {
735
+ }
736
+ currentUrl = nextUrlStr;
737
+ }
738
+ } catch (err) {
739
+ clearTimeout(timer);
740
+ const aborted = ac.signal.aborted;
741
+ return {
742
+ stepId: step.id,
743
+ kind: "errored",
744
+ durationMs: Date.now() - start,
745
+ error: aborted ? `timeout after ${timeoutMs}ms` : errMessage(err)
746
+ };
747
+ } finally {
748
+ clearTimeout(timer);
749
+ }
750
+ const verdict = evaluateExpect(step.expect, {
751
+ status: finalResponse.status,
752
+ body
753
+ });
754
+ return {
755
+ stepId: step.id,
756
+ kind: verdict.kind,
757
+ observedStatus: finalResponse.status,
758
+ observedResponseBody: body,
759
+ durationMs: Date.now() - start,
760
+ ...verdict.error ? { error: verdict.error } : {}
761
+ };
762
+ }
763
+ function resolveUrl(stepUrl, baseUrl) {
764
+ try {
765
+ new URL(stepUrl);
766
+ return stepUrl;
767
+ } catch {
768
+ }
769
+ if (!baseUrl)
770
+ return null;
771
+ try {
772
+ return new URL(stepUrl, baseUrl).toString();
773
+ } catch {
774
+ return null;
775
+ }
776
+ }
777
+ function mergePersonaHeaders(stepHeaders, target) {
778
+ const merged = {};
779
+ let personaName;
780
+ for (const [k, v] of Object.entries(stepHeaders)) {
781
+ if (k.toLowerCase() === PERSONA_HEADER.toLowerCase()) {
782
+ personaName = v;
783
+ continue;
784
+ }
785
+ merged[k] = v;
786
+ }
787
+ if (!personaName)
788
+ return merged;
789
+ const persona = target.personas?.[personaName];
790
+ if (!persona)
791
+ return merged;
792
+ if (persona.headers) {
793
+ for (const [k, v] of Object.entries(persona.headers)) {
794
+ if (!(k in merged))
795
+ merged[k] = v;
796
+ }
797
+ }
798
+ if (persona.cookies && !("cookie" in lowerKeys(merged))) {
799
+ merged["Cookie"] = persona.cookies;
800
+ }
801
+ return merged;
802
+ }
803
+ function lowerKeys(obj) {
804
+ const out = {};
805
+ for (const [k, v] of Object.entries(obj))
806
+ out[k.toLowerCase()] = v;
807
+ return out;
808
+ }
809
+ async function readBodyCapped(response) {
810
+ if (!response.body)
811
+ return "";
812
+ const reader = response.body.getReader();
813
+ const decoder = new TextDecoder("utf-8", { fatal: false });
814
+ let captured = "";
815
+ let truncated = false;
816
+ while (true) {
817
+ const { value, done } = await reader.read();
818
+ if (done)
819
+ break;
820
+ if (value) {
821
+ const chunk = decoder.decode(value, { stream: true });
822
+ if (captured.length + chunk.length > MAX_CAPTURE_BYTES) {
823
+ captured += chunk.slice(0, MAX_CAPTURE_BYTES - captured.length);
824
+ truncated = true;
825
+ try {
826
+ await reader.cancel();
827
+ } catch {
828
+ }
829
+ break;
830
+ }
831
+ captured += chunk;
832
+ }
833
+ }
834
+ return truncated ? captured + TRUNCATION_MARKER : captured;
835
+ }
836
+ function spawnAndCapture(cmd, args, opts) {
837
+ return new Promise((resolveP) => {
838
+ let child;
839
+ try {
840
+ child = deps.spawn(cmd, args, {
841
+ env: opts.env,
842
+ cwd: opts.cwd,
843
+ stdio: ["ignore", "pipe", "pipe"]
844
+ });
845
+ } catch (err) {
846
+ resolveP({
847
+ exitCode: null,
848
+ stdout: "",
849
+ stderr: "",
850
+ timedOut: false,
851
+ error: errMessage(err)
852
+ });
853
+ return;
854
+ }
855
+ let stdout = "";
856
+ let stderr = "";
857
+ let stdoutTruncated = false;
858
+ let stderrTruncated = false;
859
+ let timedOut = false;
860
+ let settled = false;
861
+ const timer = setTimeout(() => {
862
+ timedOut = true;
863
+ try {
864
+ child.kill("SIGKILL");
865
+ } catch {
866
+ }
867
+ }, opts.timeoutMs);
868
+ child.stdout?.on("data", (chunk) => {
869
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
870
+ if (stdout.length + s.length > MAX_CAPTURE_BYTES) {
871
+ if (!stdoutTruncated) {
872
+ stdout += s.slice(0, MAX_CAPTURE_BYTES - stdout.length);
873
+ stdoutTruncated = true;
874
+ }
875
+ return;
876
+ }
877
+ stdout += s;
878
+ });
879
+ child.stderr?.on("data", (chunk) => {
880
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
881
+ if (stderr.length + s.length > MAX_CAPTURE_BYTES) {
882
+ if (!stderrTruncated) {
883
+ stderr += s.slice(0, MAX_CAPTURE_BYTES - stderr.length);
884
+ stderrTruncated = true;
885
+ }
886
+ return;
887
+ }
888
+ stderr += s;
889
+ });
890
+ const settle = (exit, errorMsg) => {
891
+ if (settled)
892
+ return;
893
+ settled = true;
894
+ clearTimeout(timer);
895
+ resolveP({
896
+ exitCode: exit,
897
+ stdout: stdoutTruncated ? stdout + TRUNCATION_MARKER : stdout,
898
+ stderr: stderrTruncated ? stderr + TRUNCATION_MARKER : stderr,
899
+ timedOut,
900
+ error: errorMsg
901
+ });
902
+ };
903
+ child.on("error", (err) => {
904
+ settle(null, errMessage(err));
905
+ });
906
+ child.on("close", (code) => {
907
+ settle(code);
908
+ });
909
+ });
910
+ }
911
+ function finaliseProcessStep(step, spawned, start) {
912
+ const durationMs = Date.now() - start;
913
+ if (spawned.timedOut) {
914
+ return {
915
+ stepId: step.id,
916
+ kind: "errored",
917
+ observedStdout: spawned.stdout,
918
+ observedStderr: spawned.stderr,
919
+ durationMs,
920
+ error: `timeout after ${durationMs}ms`
921
+ };
922
+ }
923
+ if (spawned.error && spawned.exitCode === null) {
924
+ return {
925
+ stepId: step.id,
926
+ kind: "errored",
927
+ observedStdout: spawned.stdout,
928
+ observedStderr: spawned.stderr,
929
+ durationMs,
930
+ error: spawned.error
931
+ };
932
+ }
933
+ const verdict = evaluateExpect(step.expect, {
934
+ exitCode: spawned.exitCode,
935
+ stdout: spawned.stdout
936
+ });
937
+ const result = {
938
+ stepId: step.id,
939
+ kind: verdict.kind,
940
+ observedStdout: spawned.stdout,
941
+ observedStderr: spawned.stderr,
942
+ durationMs
943
+ };
944
+ if (spawned.exitCode !== null)
945
+ result.observedExit = spawned.exitCode;
946
+ if (verdict.error)
947
+ result.error = verdict.error;
948
+ return result;
949
+ }
950
+ function evaluateExpect(expect, obs) {
951
+ if (!expect) {
952
+ return { kind: "passed" };
953
+ }
954
+ switch (expect.type) {
955
+ case "exit-zero": {
956
+ if (obs.exitCode === 0)
957
+ return { kind: "passed" };
958
+ return {
959
+ kind: "failed",
960
+ error: `expected exit-zero, got exitCode=${String(obs.exitCode ?? "unknown")}`
961
+ };
962
+ }
963
+ case "http-status": {
964
+ const wanted = Array.isArray(expect.status) ? expect.status : [expect.status];
965
+ if (typeof obs.status === "number" && wanted.includes(obs.status)) {
966
+ return { kind: "passed" };
967
+ }
968
+ return {
969
+ kind: "failed",
970
+ error: `expected http-status in [${wanted.join(",")}], got ${String(obs.status ?? "no-response")}`
971
+ };
972
+ }
973
+ case "body-contains": {
974
+ const haystack = obs.body ?? obs.stdout ?? "";
975
+ if (haystack.includes(expect.text))
976
+ return { kind: "passed" };
977
+ return {
978
+ kind: "failed",
979
+ error: `expected body-contains '${expect.text}', not found in observed output`
980
+ };
981
+ }
982
+ case "body-matches": {
983
+ const haystack = obs.body ?? obs.stdout ?? "";
984
+ let re;
985
+ try {
986
+ re = new RegExp(expect.pattern);
987
+ } catch (err) {
988
+ return { kind: "errored", error: `invalid regex: ${errMessage(err)}` };
989
+ }
990
+ if (re.test(haystack))
991
+ return { kind: "passed" };
992
+ return {
993
+ kind: "failed",
994
+ error: `expected body-matches /${expect.pattern}/, no match in observed output`
995
+ };
996
+ }
997
+ case "file-exists": {
998
+ return {
999
+ kind: "errored",
1000
+ error: "file-exists predicate cannot be evaluated against a network target; rewrite as a shell action with `[ -e <path> ]`"
1001
+ };
1002
+ }
1003
+ default: {
1004
+ const _exhaustive = expect;
1005
+ return {
1006
+ kind: "errored",
1007
+ error: `unknown expect type: ${JSON.stringify(_exhaustive)}`
1008
+ };
1009
+ }
1010
+ }
1011
+ }
1012
+ function aggregateVerdict(graph, steps) {
1013
+ if (graph.length === 0) {
1014
+ return "could_not_run";
1015
+ }
1016
+ const byId = new Map(steps.map((s) => [s.stepId, s]));
1017
+ let reachedExploitOrVerify = false;
1018
+ for (const step of graph) {
1019
+ const result = byId.get(step.id);
1020
+ if (!result)
1021
+ continue;
1022
+ if (step.kind === "exploit" || step.kind === "verify") {
1023
+ reachedExploitOrVerify = true;
1024
+ }
1025
+ if (result.kind === "errored" && (step.kind === "setup" || step.kind === "auth" || step.kind === "prerequisite")) {
1026
+ return "could_not_run";
1027
+ }
1028
+ }
1029
+ const verifySteps = graph.filter((s) => s.kind === "verify");
1030
+ if (verifySteps.length > 0) {
1031
+ let anyFailed = false;
1032
+ let anyErrored = false;
1033
+ let allPassed = true;
1034
+ for (const v of verifySteps) {
1035
+ const result = byId.get(v.id);
1036
+ if (!result || result.kind === "skipped") {
1037
+ allPassed = false;
1038
+ anyErrored = true;
1039
+ continue;
1040
+ }
1041
+ if (result.kind === "failed") {
1042
+ anyFailed = true;
1043
+ allPassed = false;
1044
+ } else if (result.kind === "errored") {
1045
+ anyErrored = true;
1046
+ allPassed = false;
1047
+ }
1048
+ }
1049
+ if (allPassed)
1050
+ return "exploit_still_works";
1051
+ if (anyFailed)
1052
+ return "exploit_broken";
1053
+ if (anyErrored)
1054
+ return "could_not_run";
1055
+ }
1056
+ const exploitSteps = graph.filter((s) => s.kind === "exploit");
1057
+ if (exploitSteps.length > 0) {
1058
+ let anyFailed = false;
1059
+ let anyErrored = false;
1060
+ let allPassed = true;
1061
+ for (const e of exploitSteps) {
1062
+ const result = byId.get(e.id);
1063
+ if (!result || result.kind === "skipped") {
1064
+ allPassed = false;
1065
+ anyErrored = true;
1066
+ continue;
1067
+ }
1068
+ if (result.kind === "failed") {
1069
+ anyFailed = true;
1070
+ allPassed = false;
1071
+ } else if (result.kind === "errored") {
1072
+ anyErrored = true;
1073
+ allPassed = false;
1074
+ }
1075
+ }
1076
+ if (allPassed)
1077
+ return "exploit_still_works";
1078
+ if (anyFailed)
1079
+ return "exploit_broken";
1080
+ if (anyErrored)
1081
+ return "could_not_run";
1082
+ }
1083
+ return reachedExploitOrVerify ? "exploit_still_works" : "could_not_run";
1084
+ }
1085
+ function errMessage(err) {
1086
+ if (err instanceof Error)
1087
+ return err.message;
1088
+ if (typeof err === "string")
1089
+ return err;
1090
+ try {
1091
+ return JSON.stringify(err);
1092
+ } catch {
1093
+ return String(err);
1094
+ }
1095
+ }
1096
+
1097
+ // packages/core/dist/disclose/bundle.js
1098
+ function decideFilingState(inputs) {
1099
+ const { patchStatus, behaviouralReport, dropFixed, keepUnrun, emptyPoc } = inputs;
1100
+ if (emptyPoc) {
1101
+ return { filingState: "drop", dropReason: "unverified-poc: empty PoC" };
1102
+ }
1103
+ if (patchStatus && dropFixed && (patchStatus.status === "fixed" || patchStatus.status === "file-removed")) {
1104
+ return { filingState: "drop", dropReason: `canary status=${patchStatus.status}` };
1105
+ }
1106
+ if (behaviouralReport?.overallVerdict === "exploit_broken") {
1107
+ return { filingState: "drop", dropReason: "behavioural reverify: exploit_broken" };
1108
+ }
1109
+ if (behaviouralReport?.overallVerdict === "could_not_run") {
1110
+ if (keepUnrun)
1111
+ return { filingState: "needs-review" };
1112
+ return { filingState: "drop", dropReason: "unverified-poc: behavioural reverify could_not_run" };
1113
+ }
1114
+ return { filingState: "keep" };
1115
+ }
1116
+ function dropSlug(entry) {
1117
+ return entry.patchStatus ?? entry.behaviouralVerdict ?? "dropped";
1118
+ }
1119
+ function droppedFilename(entry) {
1120
+ return `${entry.finding.id.slice(0, 8)}-${entry.finding.severity}-${dropSlug(entry)}.md`;
1121
+ }
1122
+ function formatDroppedReason(args) {
1123
+ const { finding, scanId, patchStatus, behaviouralReport, reason } = args;
1124
+ const lines = [];
1125
+ lines.push(`# Dropped: ${finding.title}`);
1126
+ lines.push("");
1127
+ lines.push(`- **Reason:** ${reason}`);
1128
+ lines.push(`- **Scan:** \`${scanId}\``);
1129
+ lines.push(`- **Finding id:** \`${finding.id}\``);
1130
+ if (patchStatus) {
1131
+ lines.push(`- **Canary status:** ${patchStatus.status}`);
1132
+ lines.push(`- **Canary ref:** \`${patchStatus.ref}\``);
1133
+ }
1134
+ if (behaviouralReport) {
1135
+ lines.push(`- **Behavioural verdict:** ${behaviouralReport.overallVerdict}`);
1136
+ lines.push(`- **Last-known-good execution:** see \`${finding.id.slice(0, 8)}.execution.json\``);
1137
+ }
1138
+ lines.push("");
1139
+ if (patchStatus) {
1140
+ lines.push("## Canary notes", "");
1141
+ for (const n of patchStatus.notes)
1142
+ lines.push(`- ${n}`);
1143
+ lines.push("", "## Refs checked", "");
1144
+ for (const r of patchStatus.refsChecked) {
1145
+ lines.push(`- \`${r.file}${r.line ? `:${r.line}` : ""}\``);
1146
+ }
1147
+ lines.push("");
1148
+ }
1149
+ if (behaviouralReport) {
1150
+ lines.push("## Behavioural step verdicts", "");
1151
+ for (const s of behaviouralReport.steps) {
1152
+ lines.push(`- \`${s.stepId}\` \u2192 ${s.kind}${s.error ? ` (${s.error})` : ""}`);
1153
+ }
1154
+ lines.push("");
1155
+ }
1156
+ return lines.join("\n");
1157
+ }
1158
+ function findingProvenanceHashes(finding) {
1159
+ const HEX64 = /^[0-9a-f]{64}$/i;
1160
+ const seen = /* @__PURE__ */ new Set();
1161
+ for (const art of finding.verification_result?.evidence_artifacts ?? []) {
1162
+ if (art.sha256 && HEX64.test(art.sha256))
1163
+ seen.add(art.sha256.toLowerCase());
1164
+ }
1165
+ for (const envelope of finding.researchEvidence ?? []) {
1166
+ for (const art of envelope.artifacts ?? []) {
1167
+ if (art.sha256 && HEX64.test(art.sha256))
1168
+ seen.add(art.sha256.toLowerCase());
1169
+ }
1170
+ }
1171
+ return [...seen].sort();
1172
+ }
1173
+ function assembleBundleIndex(entries, options) {
1174
+ const { scanIds } = options;
1175
+ const generatedAt = options.generatedAt ?? /* @__PURE__ */ new Date();
1176
+ const drafts = entries.filter((e) => e.filingState !== "drop");
1177
+ const dropped = entries.filter((e) => e.filingState === "drop");
1178
+ const scanLabel = scanIds.length === 1 ? `\`${scanIds[0]}\`` : `\`${scanIds.join("`, `")}\` (${scanIds.length} scans)`;
1179
+ const truncateTitle = (t, max = 64) => t.length > max ? t.slice(0, max - 1) + "\u2026" : t;
1180
+ const truncateReason = (r, max = 48) => !r ? "\u2014" : r.length > max ? r.slice(0, max - 1) + "\u2026" : r;
1181
+ const escapePipe = (s) => s.replace(/\|/g, "\\|");
1182
+ const formatProvenance = (e) => {
1183
+ const hashes = findingProvenanceHashes(e.finding);
1184
+ if (hashes.length === 0)
1185
+ return "\u2014";
1186
+ const first = `\`${hashes[0].slice(0, 12)}\u2026\``;
1187
+ return hashes.length === 1 ? first : `${first} +${hashes.length - 1}`;
1188
+ };
1189
+ const redactTitle = (e) => escapePipe(truncateTitle(redactSensitiveHeaders(e.finding.title)));
1190
+ const redactReason = (e) => escapePipe(truncateReason(e.dropReason ? redactSensitiveHeaders(e.dropReason) : void 0));
1191
+ if (entries.length === 0) {
1192
+ return [
1193
+ "# Disclosure batch",
1194
+ "",
1195
+ `- Scan: ${scanLabel}`,
1196
+ "- Drafts: 0",
1197
+ `- Generated: ${generatedAt.toISOString()}`,
1198
+ "",
1199
+ "_No findings matched the current filters. Pass `--severity-floor low` or `--scan <id>` to widen the selection._",
1200
+ ""
1201
+ ].join("\n");
1202
+ }
1203
+ const lines = [];
1204
+ lines.push("# Disclosure batch", "");
1205
+ lines.push(`- Scan: ${scanLabel}`);
1206
+ lines.push(`- Drafts: ${drafts.length}`);
1207
+ if (dropped.length > 0)
1208
+ lines.push(`- Dropped: ${dropped.length} (see \`_dropped/\`)`);
1209
+ lines.push(`- Generated: ${generatedAt.toISOString()}`, "");
1210
+ lines.push("## Filing order", "", "| finding-id | severity | title | gate-status | behavioural | filing-state | provenance |", "|---|---|---|---|---|---|---|");
1211
+ for (const e of drafts) {
1212
+ const id = e.finding.id.slice(0, 8);
1213
+ const titleCell = `[${redactTitle(e)}](./${e.filename})`;
1214
+ lines.push(`| \`${id}\` | ${e.finding.severity} | ${titleCell} | ${e.patchStatus ?? "\u2014"} | ${e.behaviouralVerdict ?? "\u2014"} | ${e.filingState} | ${formatProvenance(e)} |`);
1215
+ }
1216
+ if (dropped.length > 0) {
1217
+ lines.push("", "## Dropped", "", "| finding-id | severity | title | gate-status | behavioural | reason | provenance |", "|---|---|---|---|---|---|---|");
1218
+ for (const e of dropped) {
1219
+ const id = e.finding.id.slice(0, 8);
1220
+ const titleCell = `[${redactTitle(e)}](./_dropped/${droppedFilename(e)})`;
1221
+ lines.push(`| \`${id}\` | ${e.finding.severity} | ${titleCell} | ${e.patchStatus ?? "\u2014"} | ${e.behaviouralVerdict ?? "\u2014"} | ${redactReason(e)} | ${formatProvenance(e)} |`);
1222
+ }
1223
+ }
1224
+ lines.push("", "## Before filing each advisory", "", "1. Re-read the draft \u2014 the PoC and Patch Status sections are auto-populated from the scan but you should sanity-check against the current upstream HEAD.", "2. Verify the CVSS vector suggested by 0sec is still appropriate for your deployment model.", "3. Attach or replace screenshots in the PoC section as needed.", "4. File at https://github.com/<owner>/<repo>/security/advisories/new", "");
1225
+ return lines.join("\n");
1226
+ }
1227
+
1228
+ // packages/core/dist/disclose/writeup.js
1229
+ var import_yaml = __toESM(require_dist(), 1);
1230
+ var PUBLISHABLE_STATUSES = /* @__PURE__ */ new Set([
1231
+ "published-cve",
1232
+ "patched-no-cve",
1233
+ "wontfix",
1234
+ "maintainer-rejected"
1235
+ ]);
1236
+ var EmbargoedFindingError = class extends Error {
1237
+ status;
1238
+ constructor(status) {
1239
+ super(`Finding status "${status}" is not operator-cleared for a writeup. Pass allowEmbargoed:true to stage an internal draft, or wait for a terminal status (${[...PUBLISHABLE_STATUSES].join(", ")}).`);
1240
+ this.name = "EmbargoedFindingError";
1241
+ this.status = status;
1242
+ }
1243
+ };
1244
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
1245
+ function parseDisclosure(raw) {
1246
+ const m = FRONTMATTER_RE.exec(raw);
1247
+ if (!m) {
1248
+ return { frontmatter: {}, body: raw.trim() };
1249
+ }
1250
+ let frontmatter = {};
1251
+ try {
1252
+ const parsed = (0, import_yaml.parse)(m[1]);
1253
+ if (parsed && typeof parsed === "object") {
1254
+ frontmatter = parsed;
1255
+ }
1256
+ } catch {
1257
+ frontmatter = {};
1258
+ }
1259
+ const body = raw.slice(m[0].length).trim();
1260
+ return { frontmatter, body };
1261
+ }
1262
+ var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
1263
+ var PUBLIC_SECURITY_LOCALPARTS = /* @__PURE__ */ new Set([
1264
+ "security",
1265
+ "secalert",
1266
+ "psirt",
1267
+ "abuse",
1268
+ "cve",
1269
+ "oss-security",
1270
+ "support",
1271
+ "info"
1272
+ ]);
1273
+ function redactPii(text) {
1274
+ if (!text)
1275
+ return text;
1276
+ return text.replace(EMAIL_RE, (addr) => {
1277
+ const localPart = addr.slice(0, addr.indexOf("@")).toLowerCase();
1278
+ if (PUBLIC_SECURITY_LOCALPARTS.has(localPart))
1279
+ return addr;
1280
+ return "<REDACTED-EMAIL>";
1281
+ });
1282
+ }
1283
+ function sanitizeWriteup(text) {
1284
+ return redactPii(redactSensitiveHeaders(text));
1285
+ }
1286
+ var HEADING_ALIASES = {
1287
+ summary: "Summary",
1288
+ analysis: "Summary",
1289
+ "attack vector": "How we found it",
1290
+ reproduction: "How we found it",
1291
+ poc: "How we found it",
1292
+ "proof of concept": "How we found it",
1293
+ "end-to-end chain": "How we found it",
1294
+ timeline: "Timeline",
1295
+ "recommended fix": "Remediation",
1296
+ "suggested fix": "Remediation",
1297
+ "recommended remediation": "Remediation",
1298
+ remediation: "Remediation"
1299
+ };
1300
+ var SECTION_ORDER = [
1301
+ "Summary",
1302
+ "How we found it",
1303
+ "Timeline",
1304
+ "Remediation"
1305
+ ];
1306
+ function extractSections(body) {
1307
+ const sections = {
1308
+ Summary: [],
1309
+ "How we found it": [],
1310
+ Timeline: [],
1311
+ Remediation: []
1312
+ };
1313
+ const headingRe = /^#{2,}\s+(.+?)\s*$/gm;
1314
+ const matches = [...body.matchAll(headingRe)];
1315
+ for (let i = 0; i < matches.length; i++) {
1316
+ const headingText = matches[i][1].replace(/`[^`]*`/g, "").replace(/[*_]/g, "").trim().toLowerCase();
1317
+ const canonical = HEADING_ALIASES[headingText];
1318
+ if (!canonical)
1319
+ continue;
1320
+ const start = matches[i].index + matches[i][0].length;
1321
+ const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
1322
+ const content = body.slice(start, end).trim();
1323
+ if (content)
1324
+ sections[canonical].push(content);
1325
+ }
1326
+ return sections;
1327
+ }
1328
+ function slugify2(value, max = 80) {
1329
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max);
1330
+ }
1331
+ function titleFor(fm, body) {
1332
+ const h1 = /^#\s+(.+?)\s*$/m.exec(body);
1333
+ if (h1)
1334
+ return h1[1].trim();
1335
+ const pkg = fm.package ?? "the target";
1336
+ const cls = fm.vuln_class ? ` ${fm.vuln_class.replace(/-/g, " ")}` : "";
1337
+ return `${pkg}${cls}`;
1338
+ }
1339
+ function generateWriteup(parsed, opts = {}) {
1340
+ const { frontmatter: fm, body } = parsed;
1341
+ const status = (fm.status ?? "unknown").trim();
1342
+ const cleared = PUBLISHABLE_STATUSES.has(status);
1343
+ if (!cleared && !opts.allowEmbargoed) {
1344
+ throw new EmbargoedFindingError(status);
1345
+ }
1346
+ const title = titleFor(fm, body);
1347
+ const sections = extractSections(body);
1348
+ const generatedAt = opts.generatedAt ?? /* @__PURE__ */ new Date();
1349
+ const dateStamp = generatedAt.toISOString().slice(0, 10);
1350
+ const sectionsPresent = SECTION_ORDER.filter((s) => sections[s].length > 0);
1351
+ const out = [];
1352
+ out.push(`# How we hacked ${title}`, "");
1353
+ out.push("> DRAFT \u2014 sanitised writeup generated from a disclosure record. Operator review required before publication; embargo rules in `disclosure/AGENTS.md` still apply.", "");
1354
+ const meta = [];
1355
+ if (fm.package)
1356
+ meta.push(`**Target:** \`${fm.package}\``);
1357
+ if (fm.vuln_class)
1358
+ meta.push(`**Class:** ${fm.vuln_class}`);
1359
+ if (fm.severity_estimate)
1360
+ meta.push(`**Severity:** ${fm.severity_estimate}`);
1361
+ if (fm.cve_id)
1362
+ meta.push(`**CVE:** ${fm.cve_id}`);
1363
+ else if (fm.ghsa_id)
1364
+ meta.push(`**Advisory:** ${fm.ghsa_id}`);
1365
+ meta.push(`**Status:** ${status}`);
1366
+ if (meta.length)
1367
+ out.push(meta.join(" \xB7 "), "");
1368
+ if (!cleared) {
1369
+ out.push(`> \u26A0\uFE0F This finding is **not yet in a public/terminal status** (\`${status}\`). Emitted under the embargo override as an internal draft only \u2014 do not publish.`, "");
1370
+ }
1371
+ for (const section of SECTION_ORDER) {
1372
+ const parts = sections[section];
1373
+ if (parts.length === 0)
1374
+ continue;
1375
+ out.push(`## ${section}`, "");
1376
+ out.push(sanitizeWriteup(parts.join("\n\n")), "");
1377
+ }
1378
+ out.push("## Credits", "");
1379
+ out.push("Found by 0sec's automated security-research tooling ([0sec.ai](https://0sec.ai)).", "");
1380
+ const slug = slugify2(fm.short_name ? `${fm.package ?? ""}-${fm.short_name}` : title);
1381
+ const filename = `${slug || "writeup"}-${dateStamp}.md`;
1382
+ return {
1383
+ filename,
1384
+ markdown: out.join("\n").replace(/\n{3,}/g, "\n\n"),
1385
+ sectionsPresent,
1386
+ embargoed: !cleared
1387
+ };
1388
+ }
1389
+
1390
+ // packages/core/dist/disclose/tracking.js
1391
+ var DISCLOSURE_STATUSES = [
1392
+ "draft",
1393
+ "sent",
1394
+ "acknowledged",
1395
+ "accepted",
1396
+ "cve_assigned",
1397
+ "published",
1398
+ "rejected",
1399
+ "not_applicable",
1400
+ "duplicate",
1401
+ "withdrawn"
1402
+ ];
1403
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1404
+ "published",
1405
+ "rejected",
1406
+ "not_applicable",
1407
+ "duplicate",
1408
+ "withdrawn"
1409
+ ]);
1410
+ var PUBLIC_STATUSES = /* @__PURE__ */ new Set([
1411
+ "published"
1412
+ ]);
1413
+ var FORWARD_EDGES = {
1414
+ draft: ["sent"],
1415
+ sent: ["acknowledged", "accepted", "cve_assigned"],
1416
+ acknowledged: ["accepted", "cve_assigned"],
1417
+ accepted: ["cve_assigned", "published"],
1418
+ cve_assigned: ["published"],
1419
+ published: [],
1420
+ rejected: [],
1421
+ not_applicable: [],
1422
+ duplicate: [],
1423
+ withdrawn: []
1424
+ };
1425
+ var OFFRAMPS = [
1426
+ "rejected",
1427
+ "not_applicable",
1428
+ "duplicate",
1429
+ "withdrawn"
1430
+ ];
1431
+ function allowedNextStatuses(from) {
1432
+ if (TERMINAL_STATUSES.has(from))
1433
+ return [];
1434
+ const forward = FORWARD_EDGES[from] ?? [];
1435
+ const next = new Set(forward);
1436
+ for (const off of OFFRAMPS)
1437
+ next.add(off);
1438
+ next.delete(from);
1439
+ return DISCLOSURE_STATUSES.filter((s) => next.has(s));
1440
+ }
1441
+ function canTransition(from, to) {
1442
+ return allowedNextStatuses(from).includes(to);
1443
+ }
1444
+ var IllegalTransitionError = class extends Error {
1445
+ from;
1446
+ to;
1447
+ constructor(from, to) {
1448
+ const allowed = allowedNextStatuses(from);
1449
+ super(`Illegal disclosure transition ${from} \u2192 ${to}. ` + (allowed.length ? `Allowed from "${from}": ${allowed.join(", ")}.` : `"${from}" is terminal \u2014 no further transitions.`));
1450
+ this.name = "IllegalTransitionError";
1451
+ this.from = from;
1452
+ this.to = to;
1453
+ }
1454
+ };
1455
+ function createDisclosureRecord(findingId, opts = {}) {
1456
+ const at = opts.at ?? (/* @__PURE__ */ new Date()).toISOString();
1457
+ const actor = opts.actor ?? "operator";
1458
+ return {
1459
+ findingId,
1460
+ status: "draft",
1461
+ timeline: [
1462
+ {
1463
+ fromStatus: null,
1464
+ toStatus: "draft",
1465
+ actor,
1466
+ at,
1467
+ message: opts.message ?? "Disclosure record opened (draft)."
1468
+ }
1469
+ ]
1470
+ };
1471
+ }
1472
+ function transition(record, input) {
1473
+ const from = record.status;
1474
+ const to = input.to;
1475
+ if (!canTransition(from, to)) {
1476
+ throw new IllegalTransitionError(from, to);
1477
+ }
1478
+ const at = input.at ?? (/* @__PURE__ */ new Date()).toISOString();
1479
+ const actor = input.actor ?? "operator";
1480
+ const next = {
1481
+ ...record,
1482
+ status: to,
1483
+ timeline: [
1484
+ ...record.timeline,
1485
+ { fromStatus: from, toStatus: to, actor, at, message: input.message }
1486
+ ]
1487
+ };
1488
+ if (to === "sent") {
1489
+ if (input.disclosedTo)
1490
+ next.disclosedTo = input.disclosedTo;
1491
+ next.disclosedAt = record.disclosedAt ?? at;
1492
+ }
1493
+ if (to === "cve_assigned" && input.cveId) {
1494
+ next.cveId = input.cveId;
1495
+ }
1496
+ return next;
1497
+ }
1498
+ function isPubliclyDisclosed(record) {
1499
+ return PUBLIC_STATUSES.has(record.status);
1500
+ }
1501
+
1502
+ // packages/core/dist/triage/can-auto-suppress.js
1503
+ var PROTECTED_SEVERITIES = /* @__PURE__ */ new Set([
1504
+ "critical",
1505
+ "high"
1506
+ ]);
1507
+ var HIGH_IMPACT_CATEGORIES = /* @__PURE__ */ new Set([
1508
+ // Remote code / command / code execution
1509
+ "command-injection",
1510
+ "code-injection",
1511
+ // Injection into the data store
1512
+ "sql-injection",
1513
+ // Unsafe deserialization is a classic RCE primitive
1514
+ "unsafe-deserialization",
1515
+ // Server-side request forgery (pivot / internal network reach)
1516
+ "ssrf",
1517
+ // Arbitrary file read/write
1518
+ "path-traversal",
1519
+ // Memory corruption / binary / kernel classes
1520
+ "heap-overflow",
1521
+ "out-of-bounds-read",
1522
+ "out-of-bounds-write",
1523
+ "use-after-free",
1524
+ "stack-buffer-overflow",
1525
+ "integer-overflow",
1526
+ "type-confusion",
1527
+ "double-free",
1528
+ "format-string",
1529
+ "uninitialized-memory",
1530
+ "race-condition",
1531
+ "toctou",
1532
+ // Supply-chain compromise (known-vulnerable / malicious package)
1533
+ "known-vulnerable-package",
1534
+ "supply-chain",
1535
+ // Prototype-pollution gadget → auth bypass / RCE when chained. We actively
1536
+ // disclose these (e.g. the jsonwebtoken verify() gadget), so they must never
1537
+ // be auto-dropped on a score alone.
1538
+ "prototype-pollution",
1539
+ // Cryptographic misuse — hardcoded keys/IVs, JWT alg-confusion (alg=none /
1540
+ // HS-vs-RS), and predictable RNG for secrets are direct key-leak / auth-bypass
1541
+ // primitives, so the class is protected from score-only auto-drop.
1542
+ "crypto-misuse",
1543
+ // AI-system adversarial classes — the core 0sec "adversarial reliability"
1544
+ // thesis (authorization-boundary break, unsafe tool use, data exfiltration,
1545
+ // prompt hijack). A missed one of these is a disclosure-grade loss.
1546
+ "prompt-injection",
1547
+ "data-exfiltration",
1548
+ "tool-misuse"
1549
+ ]);
1550
+ function canAutoSuppressDetailed(finding) {
1551
+ if (PROTECTED_SEVERITIES.has(finding.severity)) {
1552
+ return {
1553
+ canSuppress: false,
1554
+ guard: "high_severity",
1555
+ reason: `severity=${finding.severity} is disclosure-grade; routed to verification instead of auto-drop`
1556
+ };
1557
+ }
1558
+ if (HIGH_IMPACT_CATEGORIES.has(finding.category)) {
1559
+ return {
1560
+ canSuppress: false,
1561
+ guard: "high_impact_class",
1562
+ reason: `category=${finding.category} is a high-impact class; routed to verification instead of auto-drop`
1563
+ };
1564
+ }
1565
+ return {
1566
+ canSuppress: true,
1567
+ reason: `severity=${finding.severity}, category=${finding.category} eligible for score/heuristic auto-suppression`
1568
+ };
1569
+ }
1570
+
1571
+ // packages/core/dist/triage/verify-verdict.js
1572
+ var REPRODUCING_LAYERS = /* @__PURE__ */ new Set(["poc_gen", "pov_gate", "oracle"]);
1573
+ function evidenceKindForFinding(finding) {
1574
+ const hasPocSteps = Array.isArray(finding.pocSteps) && finding.pocSteps.length > 0;
1575
+ const reproduced = hasPocSteps && (finding.layerVerdicts ?? []).some((v) => v.verdict === "pass" && REPRODUCING_LAYERS.has(v.layer));
1576
+ return reproduced ? "reproduced-poc" : "source-only";
1577
+ }
1578
+ function outcomeOf(verdict) {
1579
+ return typeof verdict === "string" ? verdict : verdict.verdict;
1580
+ }
1581
+ function isDisclosureWorthy(finding, verdict) {
1582
+ if (outcomeOf(verdict) !== "rejected") {
1583
+ return {
1584
+ keep: true,
1585
+ reason: `verdict=${outcomeOf(verdict)}: not a rejection \u2014 finding kept`
1586
+ };
1587
+ }
1588
+ const guard = canAutoSuppressDetailed(finding);
1589
+ if (guard.canSuppress) {
1590
+ return { keep: false, reason: guard.reason };
1591
+ }
1592
+ return { keep: true, guard: guard.guard, reason: guard.reason };
1593
+ }
1594
+
1595
+ // packages/core/dist/disclose/evidence-pack.js
1596
+ var UnreproducedFindingError = class extends Error {
1597
+ findingId;
1598
+ constructor(findingId) {
1599
+ super(`Finding ${findingId} has no reproduced PoC (evidenceKind !== "reproduced-poc"). Refusing to assemble a vendor-notification draft. Pass allowUnreproduced:true to stage an internal draft for review anyway.`);
1600
+ this.name = "UnreproducedFindingError";
1601
+ this.findingId = findingId;
1602
+ }
1603
+ };
1604
+ function impactForSeverity(severity) {
1605
+ switch (severity) {
1606
+ case "critical":
1607
+ return "Critical \u2014 full compromise / unauthenticated remote impact is plausible.";
1608
+ case "high":
1609
+ return "High \u2014 significant confidentiality, integrity, or availability impact.";
1610
+ case "medium":
1611
+ return "Medium \u2014 meaningful impact, typically conditioned on access or configuration.";
1612
+ case "low":
1613
+ return "Low \u2014 limited impact or requires substantial preconditions.";
1614
+ default:
1615
+ return "Informational.";
1616
+ }
1617
+ }
1618
+ function impactLine(finding) {
1619
+ const base = impactForSeverity(finding.severity);
1620
+ const a = finding.impactAssessment;
1621
+ if (!a)
1622
+ return base;
1623
+ const gain = {
1624
+ rce: "remote code execution",
1625
+ "lpe-to-root": "local privilege escalation to root/SYSTEM",
1626
+ "info-leak": "information disclosure",
1627
+ "dos-crash": "denial of service (crash)"
1628
+ };
1629
+ const reach = {
1630
+ "remote-unauth": "remotely, unauthenticated",
1631
+ "proximity-rf": "from RF/physical proximity",
1632
+ "local-unpriv": "by an unprivileged local user",
1633
+ "local-priv": "by a privileged local user",
1634
+ "needs-hardware": "with specific/attacker-supplied hardware",
1635
+ "needs-host-migration": "if the victim mounts an attacker-supplied artifact"
1636
+ };
1637
+ const detail = `Attacker gains ${gain[a.weaponizability] ?? a.weaponizability}, exploitable ${reach[a.reachability_tier] ?? a.reachability_tier}.`;
1638
+ return `${base} ${detail}`;
1639
+ }
1640
+ function renderReproStep(step, index) {
1641
+ const n = index + 1;
1642
+ const a = step.action;
1643
+ let detail;
1644
+ switch (a.type) {
1645
+ case "shell":
1646
+ detail = "```sh\n" + a.cmd + "\n```";
1647
+ break;
1648
+ case "http": {
1649
+ const headerLines = a.headers ? Object.entries(a.headers).map(([k, v]) => `${k}: ${v}`) : [];
1650
+ const reqBlock = [
1651
+ `${a.method} ${a.url}`,
1652
+ ...headerLines,
1653
+ ...a.body ? ["", a.body] : []
1654
+ ].join("\n");
1655
+ detail = "```http\n" + reqBlock + "\n```";
1656
+ break;
1657
+ }
1658
+ case "docker":
1659
+ detail = `\`\`\`sh
1660
+ docker run ${a.image} ${a.args.join(" ")}
1661
+ \`\`\``;
1662
+ break;
1663
+ case "note":
1664
+ detail = a.text;
1665
+ break;
1666
+ default:
1667
+ detail = "";
1668
+ }
1669
+ const head = `${n}. **${step.summary}** _(${step.kind})_`;
1670
+ return detail ? `${head}
1671
+
1672
+ ${detail}` : head;
1673
+ }
1674
+ function assembleEvidencePack(finding, opts = {}) {
1675
+ const reproduced = evidenceKindForFinding(finding) === "reproduced-poc";
1676
+ if (!reproduced && !opts.allowUnreproduced) {
1677
+ throw new UnreproducedFindingError(finding.id);
1678
+ }
1679
+ const redact = (s) => redactSensitiveHeaders(s);
1680
+ const where = (() => {
1681
+ const parts = [];
1682
+ if (opts.target)
1683
+ parts.push(`\`${opts.target}\``);
1684
+ if (opts.affectedRef)
1685
+ parts.push(`(ref \`${opts.affectedRef}\`)`);
1686
+ if (parts.length === 0) {
1687
+ return "_Affected component/version: to be filled in by the operator before sending._";
1688
+ }
1689
+ return parts.join(" ");
1690
+ })();
1691
+ const reproSteps = (finding.pocSteps ?? []).map((step, i) => redact(renderReproStep(step, i)));
1692
+ const remediation = (() => {
1693
+ const r = finding.remediation;
1694
+ if (!r)
1695
+ return void 0;
1696
+ const parts = [];
1697
+ if (r.summary)
1698
+ parts.push(r.summary);
1699
+ if (r.steps?.length) {
1700
+ parts.push(r.steps.map((s, i) => `${i + 1}. ${s}`).join("\n"));
1701
+ }
1702
+ return parts.length ? redact(parts.join("\n\n")) : void 0;
1703
+ })();
1704
+ let markerWarnings;
1705
+ if (opts.sourceEvidence !== void 0) {
1706
+ markerWarnings = detectKnownMarkers(opts.sourceEvidence, opts.sourceEvidencePath);
1707
+ }
1708
+ const findingMarkers = analyzeFindingForKnownMarkers(finding);
1709
+ if (findingMarkers.hasKnownMarker) {
1710
+ markerWarnings = markerWarnings ? {
1711
+ hasKnownMarker: true,
1712
+ markers: [...markerWarnings.markers, ...findingMarkers.markers]
1713
+ } : findingMarkers;
1714
+ }
1715
+ return {
1716
+ findingId: finding.id,
1717
+ title: finding.title,
1718
+ severity: finding.severity,
1719
+ what: redact(finding.description.trim()),
1720
+ where,
1721
+ impact: impactLine(finding),
1722
+ reproSteps,
1723
+ remediation,
1724
+ reproduced,
1725
+ markerWarnings
1726
+ };
1727
+ }
1728
+ function renderVendorNotificationMarkdown(draft) {
1729
+ const out = [];
1730
+ out.push(`# Vulnerability report: ${draft.title}`, "");
1731
+ out.push("> **DRAFT \u2014 NOT SENT.** Auto-assembled vendor-notification draft. An operator must review, fill in any `to be filled in` fields, and send it manually. Nothing here is transmitted to a vendor by 0sec. Embargo rules in `disclosure/AGENTS.md` apply.", "");
1732
+ if (!draft.reproduced) {
1733
+ out.push("> \u26A0\uFE0F This finding's PoC did **not** reproduce in an isolated env (`evidenceKind !== reproduced-poc`). Staged as an internal draft only \u2014 do not send to a vendor without a working PoC.", "");
1734
+ }
1735
+ if (draft.markerWarnings?.hasKnownMarker) {
1736
+ out.push("> **[WARN] Known-marker signal (#674).** The supplied source evidence contains explicit TODO / FIXME / XXX markers or documented-limitation phrasing. This is a courtesy operator-review warning, not a verdict. Review the Known markers section before deciding whether to file.", "");
1737
+ }
1738
+ out.push(`**Severity (estimate):** ${draft.severity}`, "");
1739
+ out.push("## What", "", draft.what, "");
1740
+ out.push("## Where", "", draft.where, "");
1741
+ out.push("## Impact", "", draft.impact, "");
1742
+ out.push("## Reproduction", "");
1743
+ if (draft.reproSteps.length > 0) {
1744
+ out.push(draft.reproSteps.join("\n\n"), "");
1745
+ } else {
1746
+ out.push("_No structured PoC steps on this finding \u2014 attach a reproduction before sending._", "");
1747
+ }
1748
+ if (draft.remediation) {
1749
+ out.push("## Suggested remediation", "", draft.remediation, "");
1750
+ }
1751
+ if (draft.markerWarnings?.hasKnownMarker) {
1752
+ out.push("## Known markers (courtesy / operator review)", "");
1753
+ out.push("_The source evidence contains the following explicit markers. This is a conservative regex match: review each item before deciding whether the finding is worth filing._", "");
1754
+ for (const marker of draft.markerWarnings.markers) {
1755
+ out.push(`- **\`${marker.marker}\`**`);
1756
+ if (marker.sourcePath) {
1757
+ out.push(` - Source: \`${marker.sourcePath}\``);
1758
+ }
1759
+ if (marker.lineNumber) {
1760
+ out.push(` - Line ${marker.lineNumber}: \`${marker.line}\``);
1761
+ } else {
1762
+ out.push(` - \`${marker.line}\``);
1763
+ }
1764
+ if (marker.context) {
1765
+ out.push(` - Context:
1766
+ \`\`\`
1767
+ ${marker.context}
1768
+ \`\`\``);
1769
+ }
1770
+ out.push("");
1771
+ }
1772
+ }
1773
+ return out.join("\n").replace(/\n{3,}/g, "\n\n");
1774
+ }
1775
+
1776
+ // packages/core/dist/disclose/reproducibility-manifest.js
1777
+ import { createHash } from "node:crypto";
1778
+ var UnverifiedFindingError = class extends Error {
1779
+ constructor(message) {
1780
+ super(message);
1781
+ this.name = "UnverifiedFindingError";
1782
+ }
1783
+ };
1784
+ var IncompleteEvidenceError = class extends Error {
1785
+ constructor(message) {
1786
+ super(message);
1787
+ this.name = "IncompleteEvidenceError";
1788
+ }
1789
+ };
1790
+ var MANIFEST_VERSION = 1;
1791
+ var HASH_ALGORITHM = "sha256";
1792
+ var VERIFIED_STATUSES = {
1793
+ verified: true,
1794
+ confirmed: true
1795
+ };
1796
+ var SENSITIVE_USERINFO = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^@\n]+@/gi;
1797
+ var SENSITIVE_QUERY_VALUE = /([?&](?:access_token|api[_-]?key|authorization|auth|cookie|session|token|bearer|jwt|refresh_token|client_secret|client_id|password|passwd|secret)=)[^&#\s]*/gi;
1798
+ var STANDALONE_CREDENTIAL = /(^|[\s,;:.-])((?:access_token|api[_-]?key|authorization|auth|cookie|session|token|bearer|jwt|refresh_token|client_secret|client_id|password|passwd|secret)=)[^\s,;:)\]"'`]*(?![\w-])/gi;
1799
+ function redactManifestValue(value) {
1800
+ return redactPii(redactSensitiveHeaders(value).replace(SENSITIVE_USERINFO, "$1[REDACTED-USER]@").replace(SENSITIVE_QUERY_VALUE, "$1[REDACTED]").replace(STANDALONE_CREDENTIAL, "$1$2[REDACTED]"));
1801
+ }
1802
+ function redactAndHash(value) {
1803
+ if (!value)
1804
+ return null;
1805
+ return createHash(HASH_ALGORITHM).update(redactManifestValue(value), "utf8").digest("hex");
1806
+ }
1807
+ function canonicalJson(value) {
1808
+ if (value === null || typeof value !== "object")
1809
+ return JSON.stringify(value);
1810
+ if (Array.isArray(value))
1811
+ return `[${value.map(canonicalJson).join(",")}]`;
1812
+ const record = value;
1813
+ return `{${Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
1814
+ }
1815
+ function collectEnvironmentFingerprint() {
1816
+ return {
1817
+ platform: process.platform,
1818
+ arch: process.arch,
1819
+ nodeVersion: process.version
1820
+ };
1821
+ }
1822
+ function extractTarget(finding) {
1823
+ const url = finding.evidence?.request?.match(/^(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\S+)/)?.[1] ?? finding.evidence?.request?.match(/^\S+/)?.[0];
1824
+ if (url)
1825
+ return redactManifestValue(url);
1826
+ if (finding.title)
1827
+ return redactManifestValue(finding.title);
1828
+ return "unknown";
1829
+ }
1830
+ function computeHarnessRevision(finding) {
1831
+ if (!finding.pocSteps || finding.pocSteps.length === 0)
1832
+ return null;
1833
+ const redactedSteps = finding.pocSteps.map((step) => {
1834
+ const action = step.action ? { ...step.action } : step.action;
1835
+ if (action && "cmd" in action && typeof action.cmd === "string") {
1836
+ action.cmd = redactManifestValue(action.cmd);
1837
+ }
1838
+ if (action && "body" in action && typeof action.body === "string") {
1839
+ action.body = redactManifestValue(action.body);
1840
+ }
1841
+ return { ...step, action };
1842
+ });
1843
+ return createHash(HASH_ALGORITHM).update(canonicalJson(redactedSteps), "utf8").digest("hex");
1844
+ }
1845
+ function assembleReproducibilityManifest(finding, opts = {}) {
1846
+ const redactionsApplied = [];
1847
+ if (!VERIFIED_STATUSES[finding.status]) {
1848
+ throw new UnverifiedFindingError(`Cannot assemble reproducibility manifest for finding ${finding.id}: status is "${finding.status}", expected "verified" or "confirmed".`);
1849
+ }
1850
+ redactionsApplied.push("verified-status-gate");
1851
+ const pocExec = finding.pocExecution;
1852
+ const layerVerdicts = finding.layerVerdicts ?? [];
1853
+ const replayResult = finding.verification_result;
1854
+ const pocSuccessful = pocExec?.overallVerdict === "exploit_still_works";
1855
+ const replaySuccessful = replayResult?.status === "reproduced" || replayResult?.oast_confirmed === true;
1856
+ if (!pocSuccessful && !replaySuccessful) {
1857
+ throw new IncompleteEvidenceError(`Cannot assemble reproducibility manifest for finding ${finding.id}: no successful verified PoV. PoC verdict was ${pocExec?.overallVerdict ?? "absent"} and replay status was ${replayResult?.status ?? "absent"}. A reproduced PoC or verified replay is required.`);
1858
+ }
1859
+ const pocVerdict = pocExec?.overallVerdict ?? null;
1860
+ const passedLayerCount = layerVerdicts.filter((verdict) => verdict.verdict === "pass").length;
1861
+ const verifiedAt = pocExec?.startedAt ?? replayResult?.completed_at ?? null;
1862
+ const replayVerdict = replayResult?.status ?? null;
1863
+ if (pocSuccessful)
1864
+ redactionsApplied.push("poc-verified-impact");
1865
+ if (replaySuccessful)
1866
+ redactionsApplied.push("replay-verified-impact");
1867
+ const target = opts.targetIdentifier ? redactManifestValue(opts.targetIdentifier) : extractTarget(finding);
1868
+ const modelConfig = opts.modelConfig ? redactManifestValue(opts.modelConfig) : null;
1869
+ const toolVersion = opts.toolVersion ?? process.env["0SEC_VERSION"] ?? "unknown";
1870
+ const timestamp = opts.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
1871
+ const sanitizedTitle = redactManifestValue(finding.title);
1872
+ const evidenceReq = finding.evidence?.request ?? null;
1873
+ const evidenceRes = finding.evidence?.response ?? null;
1874
+ const evidenceHashes = {
1875
+ requestHash: redactAndHash(evidenceReq),
1876
+ responseHash: redactAndHash(evidenceRes)
1877
+ };
1878
+ const harnessRevision = computeHarnessRevision(finding);
1879
+ const sanitizedGeneratedAt = redactManifestValue(timestamp);
1880
+ const sanitizedFindingId = redactManifestValue(finding.id);
1881
+ const sanitizedSeverity = redactManifestValue(finding.severity);
1882
+ const sanitizedCategory = redactManifestValue(finding.category);
1883
+ const sanitizedStatus = redactManifestValue(finding.status);
1884
+ const sanitizedToolVersion = redactManifestValue(toolVersion);
1885
+ const sanitizedPocVerdict = pocVerdict ? redactManifestValue(pocVerdict) : null;
1886
+ const sanitizedReplayVerdict = replayVerdict ? redactManifestValue(replayVerdict) : null;
1887
+ const sanitizedVerifiedAt = verifiedAt ? redactManifestValue(verifiedAt) : null;
1888
+ redactionsApplied.push("redact-sensitive-headers");
1889
+ redactionsApplied.push("redact-pii");
1890
+ redactionsApplied.push("redact-all-text-fields");
1891
+ redactionsApplied.push("strip-url-userinfo");
1892
+ return {
1893
+ manifestVersion: MANIFEST_VERSION,
1894
+ generatedAt: sanitizedGeneratedAt,
1895
+ findingId: sanitizedFindingId,
1896
+ findingTitle: sanitizedTitle,
1897
+ findingSeverity: sanitizedSeverity,
1898
+ findingCategory: sanitizedCategory,
1899
+ findingStatus: sanitizedStatus,
1900
+ targetIdentifier: target,
1901
+ toolVersion: sanitizedToolVersion,
1902
+ harnessRevision,
1903
+ modelConfig,
1904
+ environmentFingerprint: collectEnvironmentFingerprint(),
1905
+ evidenceHashes,
1906
+ verification: {
1907
+ pocVerdict: sanitizedPocVerdict,
1908
+ layerVerdictCount: layerVerdicts.length,
1909
+ passedLayerCount,
1910
+ replayVerdict: sanitizedReplayVerdict,
1911
+ verifiedAt: sanitizedVerifiedAt
1912
+ },
1913
+ redactionsApplied
1914
+ };
1915
+ }
1916
+ function renderReproducibilityManifest(manifest) {
1917
+ const lines = [];
1918
+ const add = (key, val) => {
1919
+ lines.push(` ${key.padEnd(28)} ${val ?? "\u2014"}`);
1920
+ };
1921
+ lines.push("\u2500".repeat(72));
1922
+ lines.push(" REPRODUCIBILITY MANIFEST (v" + manifest.manifestVersion + ")");
1923
+ lines.push("\u2500".repeat(72));
1924
+ lines.push("");
1925
+ add("Generated", manifest.generatedAt);
1926
+ add("Finding ID", manifest.findingId);
1927
+ add("Title", manifest.findingTitle);
1928
+ add("Severity", manifest.findingSeverity);
1929
+ add("Category", manifest.findingCategory);
1930
+ add("Status", manifest.findingStatus);
1931
+ lines.push("");
1932
+ lines.push(" Target");
1933
+ lines.push(" " + "\u2500".repeat(66));
1934
+ add("Identifier", manifest.targetIdentifier);
1935
+ lines.push("");
1936
+ lines.push(" Environment");
1937
+ lines.push(" " + "\u2500".repeat(66));
1938
+ add("Tool version", manifest.toolVersion);
1939
+ add("Platform", manifest.environmentFingerprint.platform);
1940
+ add("Architecture", manifest.environmentFingerprint.arch);
1941
+ add("Node.js", manifest.environmentFingerprint.nodeVersion);
1942
+ add("Model config", manifest.modelConfig ?? "\u2014");
1943
+ lines.push("");
1944
+ lines.push(" Evidence");
1945
+ lines.push(" " + "\u2500".repeat(66));
1946
+ add("Request (SHA-256)", manifest.evidenceHashes.requestHash ?? "\u2014");
1947
+ add("Response (SHA-256)", manifest.evidenceHashes.responseHash ?? "\u2014");
1948
+ add("Harness revision", manifest.harnessRevision ?? "\u2014");
1949
+ lines.push("");
1950
+ lines.push(" Verification");
1951
+ lines.push(" " + "\u2500".repeat(66));
1952
+ add("PoC verdict", manifest.verification.pocVerdict ?? "\u2014");
1953
+ add("Layer verdicts", `${manifest.verification.passedLayerCount}/${manifest.verification.layerVerdictCount} pass`);
1954
+ add("Replay verdict", manifest.verification.replayVerdict ?? "\u2014");
1955
+ add("Verified at", manifest.verification.verifiedAt ?? "\u2014");
1956
+ lines.push("");
1957
+ lines.push(" Redactions applied");
1958
+ lines.push(" " + "\u2500".repeat(66));
1959
+ for (const r of manifest.redactionsApplied) {
1960
+ lines.push(` \u2022 ${r}`);
1961
+ }
1962
+ lines.push("");
1963
+ lines.push("\u2500".repeat(72));
1964
+ lines.push(" MANIFEST \u2014 FOR HUMAN REVIEW ONLY. NOT A DISCLOSURE ARTIFACT.");
1965
+ lines.push("\u2500".repeat(72));
1966
+ return lines.join("\n");
1967
+ }
1968
+
1969
+ export {
1970
+ isFreezeAvailable,
1971
+ composeExploitSession,
1972
+ composeStepSession,
1973
+ renderExploitScreenshot,
1974
+ extractSiblingFix,
1975
+ MAX_CAPTURE_BYTES,
1976
+ DEFAULT_STEP_TIMEOUT_MS,
1977
+ setRuntimeDeps,
1978
+ executePocSteps,
1979
+ decideFilingState,
1980
+ dropSlug,
1981
+ droppedFilename,
1982
+ formatDroppedReason,
1983
+ assembleBundleIndex,
1984
+ PUBLISHABLE_STATUSES,
1985
+ EmbargoedFindingError,
1986
+ parseDisclosure,
1987
+ redactPii,
1988
+ sanitizeWriteup,
1989
+ extractSections,
1990
+ generateWriteup,
1991
+ DISCLOSURE_STATUSES,
1992
+ TERMINAL_STATUSES,
1993
+ PUBLIC_STATUSES,
1994
+ allowedNextStatuses,
1995
+ canTransition,
1996
+ IllegalTransitionError,
1997
+ createDisclosureRecord,
1998
+ transition,
1999
+ isPubliclyDisclosed,
2000
+ canAutoSuppressDetailed,
2001
+ evidenceKindForFinding,
2002
+ isDisclosureWorthy,
2003
+ UnreproducedFindingError,
2004
+ assembleEvidencePack,
2005
+ renderVendorNotificationMarkdown,
2006
+ UnverifiedFindingError,
2007
+ IncompleteEvidenceError,
2008
+ assembleReproducibilityManifest,
2009
+ renderReproducibilityManifest
2010
+ };