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,1148 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __0secCreateRequire } from "node:module";
3
+ const require = __0secCreateRequire(import.meta.url);
4
+ import {
5
+ enumerateJsChunkUrls,
6
+ fingerprintWebStack,
7
+ runBaselineWebChecks,
8
+ summarizeWebStackFingerprint
9
+ } from "./chunk-QJQKHO7G.js";
10
+ import {
11
+ enumerateSubdomains,
12
+ scanJsArtifacts,
13
+ searchAdvisories
14
+ } from "./chunk-LOHTE223.js";
15
+ import "./chunk-KKGE5RQE.js";
16
+ import "./chunk-YLMN3N25.js";
17
+ import "./chunk-IR537GON.js";
18
+
19
+ // packages/core/dist/stages/web-recon-prepass.js
20
+ import { randomUUID } from "node:crypto";
21
+
22
+ // packages/core/dist/intel/version-cve.js
23
+ async function lookupVersionCves(input, opts = {}) {
24
+ const { advisories } = await searchAdvisories({
25
+ ecosystem: input.ecosystem,
26
+ packageName: input.name,
27
+ version: input.version,
28
+ cacheDir: input.cacheDir,
29
+ offline: input.offline,
30
+ ttlMs: input.ttlMs
31
+ }, opts);
32
+ const byId = /* @__PURE__ */ new Map();
33
+ for (const advisory of advisories) {
34
+ if (byId.has(advisory.id))
35
+ continue;
36
+ byId.set(advisory.id, toVersionCve(advisory));
37
+ }
38
+ return [...byId.values()];
39
+ }
40
+ function toVersionCve(advisory) {
41
+ return {
42
+ id: advisory.id,
43
+ severity: advisory.severity === "info" ? void 0 : advisory.severity,
44
+ summary: advisory.summary ?? advisory.details ?? advisory.id,
45
+ affectedRange: advisory.affectedRanges[0],
46
+ fixedVersion: advisory.fixedVersions[0],
47
+ source: advisory.source
48
+ };
49
+ }
50
+
51
+ // packages/core/dist/recon/dns-email.js
52
+ var DEFAULT_DKIM_SELECTORS = [
53
+ "google",
54
+ "default",
55
+ "selector1",
56
+ "selector2",
57
+ "k1"
58
+ ];
59
+ function joinTxt(record) {
60
+ return record.join("");
61
+ }
62
+ async function resolveTxtStrings(resolveTxt, name) {
63
+ try {
64
+ const records = await resolveTxt(name);
65
+ return records.map(joinTxt);
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+ function parseSpfPolicy(record) {
71
+ const match = /(^|\s)([-~?+]?)all(\s|$)/i.exec(record);
72
+ if (!match)
73
+ return void 0;
74
+ const qualifier = match[2] || "+";
75
+ return `${qualifier}all`;
76
+ }
77
+ function parseDmarcPolicy(record) {
78
+ const match = /(^|;)\s*p\s*=\s*(none|quarantine|reject)\b/i.exec(record);
79
+ if (!match)
80
+ return void 0;
81
+ return match[2].toLowerCase();
82
+ }
83
+ function findSpfRecord(records) {
84
+ return records.find((r) => /^v=spf1\b/i.test(r.trim()));
85
+ }
86
+ function findDmarcRecord(records) {
87
+ return records.find((r) => /^v=DMARC1\b/i.test(r.trim()));
88
+ }
89
+ function looksLikeDkimKey(record) {
90
+ const r = record.trim();
91
+ return /(^|;)\s*v\s*=\s*DKIM1\b/i.test(r) || /(^|;)\s*p\s*=/i.test(r);
92
+ }
93
+ function gradeSpf(spf, findings) {
94
+ if (!spf.present) {
95
+ spf.issue = "no SPF record published";
96
+ findings.push({
97
+ severity: "medium",
98
+ title: "Missing SPF record",
99
+ detail: "No `v=spf1` TXT record was found for this domain. Without SPF, receivers cannot verify which hosts are authorized to send mail as the domain, making it trivial to spoof."
100
+ });
101
+ return;
102
+ }
103
+ switch (spf.policy) {
104
+ case "-all":
105
+ break;
106
+ case "~all":
107
+ spf.issue = "softfail (`~all`) is weaker than hardfail (`-all`)";
108
+ findings.push({
109
+ severity: "low",
110
+ title: "SPF uses softfail (~all)",
111
+ detail: "The SPF record ends in `~all` (softfail). Receivers are asked to accept-but-mark unlisted senders rather than reject them. Once the sending inventory is confirmed complete, tighten this to `-all` (hardfail) for stronger anti-spoofing."
112
+ });
113
+ break;
114
+ case "?all":
115
+ spf.issue = "neutral (`?all`) provides no enforcement";
116
+ findings.push({
117
+ severity: "low",
118
+ title: "SPF policy is neutral (?all)",
119
+ detail: "The SPF record ends in `?all` (neutral), which expresses no opinion on unlisted senders and provides no anti-spoofing value. Move to `-all` once the authorized-sender list is complete."
120
+ });
121
+ break;
122
+ case "+all":
123
+ spf.issue = "`+all` permits any host to send as the domain";
124
+ findings.push({
125
+ severity: "medium",
126
+ title: "SPF permits all senders (+all)",
127
+ detail: "The SPF record ends in `+all`, explicitly authorizing every host on the internet to send mail as this domain. This is equivalent to having no SPF protection and should be changed to `-all`."
128
+ });
129
+ break;
130
+ default:
131
+ spf.issue = "no explicit `all` mechanism \u2014 enforcement is ambiguous";
132
+ findings.push({
133
+ severity: "low",
134
+ title: "SPF has no explicit all mechanism",
135
+ detail: "An SPF record is published but lacks an explicit `all` mechanism (`-all`/`~all`). Add a terminal `-all` so receivers know how to treat senders not covered by the record."
136
+ });
137
+ }
138
+ }
139
+ function gradeDmarc(dmarc, findings) {
140
+ if (!dmarc.present) {
141
+ dmarc.issue = "no DMARC record published";
142
+ findings.push({
143
+ severity: "medium",
144
+ title: "Missing DMARC record",
145
+ detail: "No `v=DMARC1` TXT record was found at `_dmarc.<domain>`. Without DMARC, SPF/DKIM failures carry no receiver-side enforcement policy and spoofed mail is far more likely to be delivered."
146
+ });
147
+ return;
148
+ }
149
+ switch (dmarc.policy) {
150
+ case "reject":
151
+ break;
152
+ case "quarantine":
153
+ dmarc.issue = "`p=quarantine` is weaker than `p=reject`";
154
+ findings.push({
155
+ severity: "low",
156
+ title: "DMARC policy is p=quarantine",
157
+ detail: "The DMARC policy is `p=quarantine`, which sends failing mail to spam rather than rejecting it. After confirming legitimate senders pass alignment, move to `p=reject` for full anti-spoofing."
158
+ });
159
+ break;
160
+ case "none":
161
+ dmarc.issue = "`p=none` is monitor-only and does not block spoofing";
162
+ findings.push({
163
+ severity: "low",
164
+ title: "DMARC policy is p=none (monitor only)",
165
+ detail: "The DMARC policy is `p=none`, which only collects reports and applies no enforcement. Progress to `p=quarantine` and then `p=reject` once alignment is verified."
166
+ });
167
+ break;
168
+ default:
169
+ dmarc.issue = "DMARC record present but `p=` tag is missing or invalid";
170
+ findings.push({
171
+ severity: "low",
172
+ title: "DMARC record missing a valid policy",
173
+ detail: "A DMARC record is published but its `p=` tag is missing or unrecognized. Set an explicit `p=reject` (or staged `p=none` \u2192 `p=quarantine` \u2192 `p=reject`) policy."
174
+ });
175
+ }
176
+ }
177
+ async function checkEmailPosture(opts) {
178
+ const domain = opts.domain.trim().replace(/\.$/, "").toLowerCase();
179
+ if (!domain)
180
+ throw new Error("dns-email: empty domain");
181
+ const resolveTxt = opts.resolveTxt ?? await defaultResolveTxt();
182
+ const dkimSelectors = opts.dkimSelectors ?? DEFAULT_DKIM_SELECTORS;
183
+ const findings = [];
184
+ const apexTxt = await resolveTxtStrings(resolveTxt, domain);
185
+ const spfRecord = findSpfRecord(apexTxt);
186
+ const spf = spfRecord ? { present: true, record: spfRecord, policy: parseSpfPolicy(spfRecord) } : { present: false };
187
+ gradeSpf(spf, findings);
188
+ const dmarcTxt = await resolveTxtStrings(resolveTxt, `_dmarc.${domain}`);
189
+ const dmarcRecord = findDmarcRecord(dmarcTxt);
190
+ const dmarc = dmarcRecord ? { present: true, record: dmarcRecord, policy: parseDmarcPolicy(dmarcRecord) } : { present: false };
191
+ gradeDmarc(dmarc, findings);
192
+ const checkedSelectors = [...dkimSelectors];
193
+ const found = [];
194
+ for (const selector of checkedSelectors) {
195
+ const records = await resolveTxtStrings(resolveTxt, `${selector}._domainkey.${domain}`);
196
+ if (records.some(looksLikeDkimKey))
197
+ found.push(selector);
198
+ }
199
+ if (found.length === 0) {
200
+ findings.push({
201
+ severity: "low",
202
+ title: "No DKIM key found for common selectors",
203
+ detail: `None of the probed DKIM selectors (${checkedSelectors.join(", ")}) returned a signing key. DKIM may use a non-standard selector, or mail may be unsigned. Confirm DKIM is configured so DMARC alignment can rely on a cryptographic signature, not SPF alone.`
204
+ });
205
+ }
206
+ const dkim = { checkedSelectors, found };
207
+ return { domain, spf, dmarc, dkim, findings };
208
+ }
209
+ async function defaultResolveTxt() {
210
+ const dns = await import("node:dns/promises");
211
+ return (name) => dns.resolveTxt(name);
212
+ }
213
+
214
+ // packages/core/dist/recon/rate-limit.js
215
+ var DEFAULT_BURST = 15;
216
+ var MAX_BURST = 25;
217
+ function clampBurst(burst) {
218
+ const requested = Number.isFinite(burst) ? Math.floor(burst) : DEFAULT_BURST;
219
+ return Math.max(1, Math.min(requested, MAX_BURST));
220
+ }
221
+ function sawStatusChange(statuses) {
222
+ if (statuses.length < 2)
223
+ return false;
224
+ const first = statuses[0];
225
+ const changeAt = statuses.findIndex((s) => s !== first);
226
+ if (changeAt <= 0)
227
+ return false;
228
+ const after = statuses[changeAt];
229
+ return statuses.slice(changeAt).every((s) => s === after);
230
+ }
231
+ async function probeRateLimit(opts) {
232
+ const burst = clampBurst(opts.burst);
233
+ const statuses = [];
234
+ for (let i = 0; i < burst; i++) {
235
+ const { status } = await opts.request(i);
236
+ statuses.push(status);
237
+ }
238
+ const saw429 = statuses.includes(429);
239
+ const changed = sawStatusChange(statuses);
240
+ const throttled = saw429 || changed;
241
+ let note;
242
+ if (saw429) {
243
+ note = `throttled: saw HTTP 429 within ${statuses.length} requests`;
244
+ } else if (changed) {
245
+ note = `throttled: status changed mid-burst (${statuses[0]} -> ${statuses[statuses.length - 1]})`;
246
+ } else {
247
+ note = `no rate limiting observed: ${statuses.length} requests, no 429, no status change (all ${statuses[0] ?? "n/a"})`;
248
+ }
249
+ return { sent: statuses.length, statuses, saw429, throttled, note };
250
+ }
251
+
252
+ // packages/core/dist/agent/web-cve-codex.js
253
+ function parseSemver(version) {
254
+ const cleaned = version.trim().replace(/^v/i, "");
255
+ const core = cleaned.split(/[-+]/, 1)[0] ?? "";
256
+ const parts = core.split(".");
257
+ const num = (s) => {
258
+ const n = Number.parseInt(s ?? "", 10);
259
+ return Number.isFinite(n) ? n : 0;
260
+ };
261
+ return [num(parts[0]), num(parts[1]), num(parts[2])];
262
+ }
263
+ function semverLt(version, bound) {
264
+ const a = parseSemver(version);
265
+ const b = parseSemver(bound);
266
+ for (let i = 0; i < 3; i++) {
267
+ if (a[i] < b[i])
268
+ return true;
269
+ if (a[i] > b[i])
270
+ return false;
271
+ }
272
+ return false;
273
+ }
274
+ function semverInRange(version, lo, hi) {
275
+ return !semverLt(version, lo) && semverLt(version, hi);
276
+ }
277
+ function isNext(framework) {
278
+ return /^next(\.?js)?$/i.test(framework.trim());
279
+ }
280
+ function isDjango(framework) {
281
+ return /^django$/i.test(framework.trim());
282
+ }
283
+ function isRails(framework) {
284
+ return /^(ruby[\s-]?on[\s-]?)?rails$/i.test(framework.trim());
285
+ }
286
+ function isAffectedBy29927(version) {
287
+ const [major] = parseSemver(version);
288
+ if (major === 15)
289
+ return semverLt(version, "15.2.3");
290
+ if (major === 14)
291
+ return semverLt(version, "14.2.25");
292
+ if (major === 13)
293
+ return semverInRange(version, "13.0.0", "13.5.9");
294
+ if (major === 12)
295
+ return semverInRange(version, "12.0.0", "12.3.5");
296
+ return false;
297
+ }
298
+ function isAffectedByRscBypass(version) {
299
+ return semverInRange(version, "13.4.0", "15.3.0");
300
+ }
301
+ function isRedirect(status) {
302
+ return status >= 300 && status < 400;
303
+ }
304
+ function looksLikeLogin(res, loginPath) {
305
+ const loc = res.headers["location"] ?? res.headers["Location"] ?? "";
306
+ if (loc && loginPath && loc.includes(loginPath))
307
+ return true;
308
+ const body = res.body ?? "";
309
+ const lower = body.toLowerCase();
310
+ if (/<form[^>]+(login|signin|sign-in)|name=["']password["']|type=["']password["']/.test(lower)) {
311
+ return true;
312
+ }
313
+ const contentType = (res.headers["content-type"] ?? res.headers["Content-Type"] ?? "").toLowerCase();
314
+ const isRscFlight = contentType.includes("text/x-component") || /^\s*\d+:\s*("?\$|I\[)/.test(body);
315
+ const redirectsToLogin = /\b(replace|redirect|push|navigate)\b[\s;:"',]*\/?[a-z0-9/_-]*(sign-?in|log-?in|auth)/i.test(body);
316
+ if (isRscFlight && redirectsToLogin)
317
+ return true;
318
+ const normalizedLogin = loginPath.replace(/^\/+/, "").toLowerCase();
319
+ if (normalizedLogin && new RegExp(`\\b(replace|redirect|push|navigate)\\b[\\s;:"',]*\\/?${normalizedLogin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i").test(body)) {
320
+ return true;
321
+ }
322
+ return false;
323
+ }
324
+ async function fetchBaseline(opts) {
325
+ const res = await opts.httpGet(opts.protectedPath);
326
+ const gated = isRedirect(res.status) || looksLikeLogin(res, opts.loginPath);
327
+ return { res, gated };
328
+ }
329
+ function isBypass(baseline, crafted, loginPath) {
330
+ if (!baseline.gated)
331
+ return false;
332
+ if (crafted.status !== 200)
333
+ return false;
334
+ if (looksLikeLogin(crafted, loginPath))
335
+ return false;
336
+ return true;
337
+ }
338
+ var CVE_29927 = "CVE-2025-29927";
339
+ var CVE_29927_TITLE = "Next.js middleware auth bypass via x-middleware-subrequest header";
340
+ var CVE_29927_PAYLOADS = [
341
+ "middleware",
342
+ "src/middleware",
343
+ "middleware:middleware:middleware:middleware:middleware"
344
+ ];
345
+ async function checkCve29927(opts) {
346
+ const applicable = isAffectedBy29927(opts.version);
347
+ const base = {
348
+ cve: CVE_29927,
349
+ title: CVE_29927_TITLE,
350
+ applicableToVersion: applicable,
351
+ severityIfExploitable: "critical"
352
+ };
353
+ if (!applicable) {
354
+ return {
355
+ ...base,
356
+ tested: false,
357
+ exploitable: false,
358
+ evidence: `Next.js ${opts.version} is outside the affected range for ${CVE_29927} (fixed in 15.2.3 / 14.2.25 / 13.5.9 / 12.3.5). Version-gap note only; not tested.`
359
+ };
360
+ }
361
+ const baseline = await fetchBaseline(opts);
362
+ if (!baseline.gated) {
363
+ return {
364
+ ...base,
365
+ tested: true,
366
+ exploitable: false,
367
+ evidence: `Baseline ${opts.protectedPath} returned status ${baseline.res.status} without the crafted header and did not redirect to ${opts.loginPath} or render a login page \u2014 the route is not auth-gated, so a middleware-skip bypass cannot be proven here. Reporting not-exploitable (conservative).`
368
+ };
369
+ }
370
+ const observations = [];
371
+ observations.push(`baseline ${opts.protectedPath} => ${baseline.res.status} (gated; redirects/login).`);
372
+ for (const payload of CVE_29927_PAYLOADS) {
373
+ const crafted = await opts.httpGet(opts.protectedPath, {
374
+ "x-middleware-subrequest": payload
375
+ });
376
+ const bypassed = isBypass(baseline, crafted, opts.loginPath);
377
+ observations.push(`x-middleware-subrequest: "${payload}" => ${crafted.status}` + (bypassed ? " (PROTECTED CONTENT RETURNED \u2014 bypass)" : " (still gated)"));
378
+ if (bypassed) {
379
+ return {
380
+ ...base,
381
+ tested: true,
382
+ exploitable: true,
383
+ evidence: `${CVE_29927} confirmed: with header x-middleware-subrequest="${payload}", ${opts.protectedPath} returned 200 protected content where the baseline redirected to login. Target=${opts.baseUrl}.
384
+ ` + observations.join("\n")
385
+ };
386
+ }
387
+ }
388
+ return {
389
+ ...base,
390
+ tested: true,
391
+ exploitable: false,
392
+ evidence: `All ${CVE_29927_PAYLOADS.length} x-middleware-subrequest payloads still hit the login gate (baseline behavior preserved) \u2014 middleware not skipped, likely patched or mitigated (e.g. Vercel edge mitigation). Reporting not-exploitable.
393
+ ` + observations.join("\n")
394
+ };
395
+ }
396
+ var RSC_BYPASS_ID = "NEXT-RSC-PREFETCH-BYPASS-2026-05";
397
+ var RSC_BYPASS_TITLE = "Next.js middleware auth bypass via .rsc / RSC segment-prefetch requests";
398
+ function rscVariants(protectedPath) {
399
+ const trimmed = protectedPath.replace(/\/+$/, "") || "/";
400
+ const rscPath = trimmed === "/" ? "/index.rsc" : `${trimmed}.rsc`;
401
+ return [
402
+ { label: "<route>.rsc", path: rscPath },
403
+ { label: "RSC: 1 header", path: protectedPath, headers: { RSC: "1" } },
404
+ {
405
+ label: "Next-Router-Segment-Prefetch header",
406
+ path: protectedPath,
407
+ headers: { "Next-Router-Segment-Prefetch": "/_index" }
408
+ }
409
+ ];
410
+ }
411
+ async function checkRscBypass(opts) {
412
+ const applicable = isAffectedByRscBypass(opts.version);
413
+ const base = {
414
+ cve: RSC_BYPASS_ID,
415
+ title: RSC_BYPASS_TITLE,
416
+ applicableToVersion: applicable,
417
+ severityIfExploitable: "high"
418
+ };
419
+ if (!applicable) {
420
+ return {
421
+ ...base,
422
+ tested: false,
423
+ exploitable: false,
424
+ evidence: `Next.js ${opts.version} is outside the affected window for the RSC / segment-prefetch middleware bypass (13.4.0 <= affected < 15.3.0). Version-gap note only; not tested.`
425
+ };
426
+ }
427
+ const baseline = await fetchBaseline(opts);
428
+ if (!baseline.gated) {
429
+ return {
430
+ ...base,
431
+ tested: true,
432
+ exploitable: false,
433
+ evidence: `Baseline ${opts.protectedPath} returned status ${baseline.res.status} and is not auth-gated (no redirect to ${opts.loginPath}, no login page) \u2014 cannot prove an RSC bypass against an ungated route. Reporting not-exploitable (conservative).`
434
+ };
435
+ }
436
+ const observations = [];
437
+ observations.push(`baseline ${opts.protectedPath} => ${baseline.res.status} (gated; redirects/login).`);
438
+ for (const v of rscVariants(opts.protectedPath)) {
439
+ const crafted = await opts.httpGet(v.path, v.headers);
440
+ const bypassed = isBypass(baseline, crafted, opts.loginPath);
441
+ observations.push(`${v.label} (${v.path}) => ${crafted.status}` + (bypassed ? " (PROTECTED CONTENT RETURNED \u2014 bypass)" : " (still gated)"));
442
+ if (bypassed) {
443
+ return {
444
+ ...base,
445
+ tested: true,
446
+ exploitable: true,
447
+ evidence: `${RSC_BYPASS_ID} confirmed: ${v.label} returned 200 protected content where the baseline redirected to login. Target=${opts.baseUrl}.
448
+ ` + observations.join("\n")
449
+ };
450
+ }
451
+ }
452
+ return {
453
+ ...base,
454
+ tested: true,
455
+ exploitable: false,
456
+ evidence: `All RSC / segment-prefetch variants still hit the login gate (baseline behavior preserved) \u2014 middleware re-runs for RSC payloads here. Reporting not-exploitable.
457
+ ` + observations.join("\n")
458
+ };
459
+ }
460
+ var DJANGO_DEBUG_ID = "DJANGO-DEBUG-TRUE-INFO-DISCLOSURE";
461
+ var DJANGO_DEBUG_TITLE = "Django DEBUG=True exposes traceback / settings on error pages";
462
+ var DJANGO_DEBUG_PROBE_PATH = "/0sec-debug-probe-0nonexistent-9z/?__0sec_debug_check__=1";
463
+ var DJANGO_DEBUG_PROOF_RE = /You'?re seeing this error because you have\s+DEBUG\s*=\s*True/i;
464
+ var DJANGO_DEBUG_CORROBORATING_RES = [
465
+ /Django Version/i,
466
+ /\bTraceback\b/i,
467
+ /Request Method:/i,
468
+ /Exception Type:/i
469
+ ];
470
+ async function checkDjangoDebug(opts) {
471
+ const base = {
472
+ cve: DJANGO_DEBUG_ID,
473
+ title: DJANGO_DEBUG_TITLE,
474
+ // DEBUG is config, not a patched version range — applicable to any Django.
475
+ applicableToVersion: true,
476
+ severityIfExploitable: "medium"
477
+ };
478
+ const res = await opts.httpGet(DJANGO_DEBUG_PROBE_PATH);
479
+ const body = res.body ?? "";
480
+ const hasProof = DJANGO_DEBUG_PROOF_RE.test(body);
481
+ const corroborating = DJANGO_DEBUG_CORROBORATING_RES.filter((re) => re.test(body)).length;
482
+ if (hasProof) {
483
+ return {
484
+ ...base,
485
+ tested: true,
486
+ exploitable: true,
487
+ evidence: `${DJANGO_DEBUG_ID} confirmed: GET ${DJANGO_DEBUG_PROBE_PATH} returned status ${res.status} carrying Django's DEBUG=True technical error page ("You're seeing this error because you have DEBUG = True"), with ${corroborating}/${DJANGO_DEBUG_CORROBORATING_RES.length} corroborating signatures (traceback / Django version / request metadata). This leaks settings, installed apps and stack traces. Target=${opts.baseUrl}, Django ${opts.version || "version unknown"}.`
488
+ };
489
+ }
490
+ return {
491
+ ...base,
492
+ tested: true,
493
+ exploitable: false,
494
+ evidence: `GET ${DJANGO_DEBUG_PROBE_PATH} returned status ${res.status} but the body did not contain the "DEBUG = True" banner (${corroborating}/${DJANGO_DEBUG_CORROBORATING_RES.length} corroborating signatures, proof banner absent) \u2014 DEBUG is off or the response is a generic error page. Reporting not-exploitable (conservative).`
495
+ };
496
+ }
497
+ var RAILS_INFO_ID = "RAILS-DEV-INFO-ENDPOINTS-EXPOSED";
498
+ var RAILS_INFO_TITLE = "Rails dev-only /rails/info endpoints publicly reachable (config leak)";
499
+ var RAILS_INFO_PROBES = [
500
+ {
501
+ label: "/rails/info/properties",
502
+ path: "/rails/info/properties",
503
+ signatures: [/Rails version/i, /Ruby version/i, /Middleware/i]
504
+ },
505
+ {
506
+ label: "/rails/info/routes",
507
+ path: "/rails/info/routes",
508
+ signatures: [/Helper/i, /HTTP Verb/i, /Path\s*\/?\s*Url/i, /Controller#Action/i]
509
+ }
510
+ ];
511
+ async function checkRailsInfoEndpoints(opts) {
512
+ const base = {
513
+ cve: RAILS_INFO_ID,
514
+ title: RAILS_INFO_TITLE,
515
+ // Dev-mode-in-prod is config, not a patched version range — any Rails.
516
+ applicableToVersion: true,
517
+ severityIfExploitable: "medium"
518
+ };
519
+ const observations = [];
520
+ for (const probe of RAILS_INFO_PROBES) {
521
+ const res = await opts.httpGet(probe.path);
522
+ const body = res.body ?? "";
523
+ const matched = probe.signatures.filter((re) => re.test(body)).length;
524
+ const exposed = res.status === 200 && matched >= 2;
525
+ observations.push(`${probe.label} => ${res.status} (${matched}/${probe.signatures.length} Rails-internals signatures)` + (exposed ? " (RAILS INTERNALS EXPOSED)" : ""));
526
+ if (exposed) {
527
+ return {
528
+ ...base,
529
+ tested: true,
530
+ exploitable: true,
531
+ evidence: `${RAILS_INFO_ID} confirmed: GET ${probe.label} returned 200 with ${matched}/${probe.signatures.length} Rails-internals signatures \u2014 this dev-only endpoint is publicly reachable, leaking Rails/Ruby versions, middleware and routes (app is serving development mode). Target=${opts.baseUrl}, Rails ${opts.version || "version unknown"}.
532
+ ` + observations.join("\n")
533
+ };
534
+ }
535
+ }
536
+ return {
537
+ ...base,
538
+ tested: true,
539
+ exploitable: false,
540
+ evidence: `Neither Rails dev-info endpoint returned 200 with Rails internals \u2014 the dev-only routes are gated (production mode or 403/404). Reporting not-exploitable (conservative).
541
+ ` + observations.join("\n")
542
+ };
543
+ }
544
+ var FRAMEWORK_REGISTRY = [
545
+ {
546
+ name: "next",
547
+ matches: isNext,
548
+ checks: [checkCve29927, checkRscBypass]
549
+ },
550
+ {
551
+ name: "django",
552
+ matches: isDjango,
553
+ checks: [checkDjangoDebug]
554
+ },
555
+ {
556
+ name: "rails",
557
+ matches: isRails,
558
+ checks: [checkRailsInfoEndpoints]
559
+ }
560
+ ];
561
+ async function runFrameworkCveChecks(opts) {
562
+ const entry = FRAMEWORK_REGISTRY.find((e) => e.matches(opts.framework));
563
+ if (!entry) {
564
+ return [];
565
+ }
566
+ const results = [];
567
+ for (const check of entry.checks) {
568
+ results.push(await check(opts));
569
+ }
570
+ return results;
571
+ }
572
+
573
+ // packages/core/dist/stages/web-recon-prepass.js
574
+ var DEFAULT_TIMEOUT_MS = 15e3;
575
+ async function runWebReconPrePass(config, options = {}) {
576
+ const findings = [];
577
+ const leads = [];
578
+ let fingerprint;
579
+ let subdomains = [];
580
+ const target = config.target;
581
+ const timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
582
+ const base = target.replace(/\/+$/, "");
583
+ const targetHost = safeHost(target);
584
+ const domain = registrableDomain(target);
585
+ const limiter = options.rateLimiter;
586
+ const resetBurstProbeAllowed = options.posture?.resetBurstProbe ?? true;
587
+ const fetchResponse = async (url) => {
588
+ if (!isInScopeUrl(url, targetHost, domain)) {
589
+ return { status: 0, headers: {}, body: "" };
590
+ }
591
+ if (limiter)
592
+ await limiter.acquire(url);
593
+ const controller = new AbortController();
594
+ const timer = setTimeout(() => controller.abort(), timeout);
595
+ try {
596
+ const res = await fetch(url, {
597
+ method: "GET",
598
+ headers: { Accept: "*/*" },
599
+ redirect: "manual",
600
+ signal: controller.signal
601
+ });
602
+ limiter?.noteResponse(url, res);
603
+ const body = await res.text();
604
+ const headers = {};
605
+ res.headers.forEach((value, key) => {
606
+ headers[key.toLowerCase()] = value;
607
+ });
608
+ return { status: res.status, headers, body };
609
+ } finally {
610
+ clearTimeout(timer);
611
+ }
612
+ };
613
+ const fetchText = async (url) => {
614
+ const r = await fetchResponse(url);
615
+ return { status: r.status, headers: r.headers, body: r.body };
616
+ };
617
+ let baseline;
618
+ try {
619
+ const ctx = buildScanContext(config);
620
+ const result = await runBaselineWebChecks(ctx);
621
+ findings.push(...result.findings);
622
+ } catch {
623
+ }
624
+ try {
625
+ baseline = await fetchResponse(base);
626
+ } catch {
627
+ baseline = void 0;
628
+ }
629
+ try {
630
+ fingerprint = await fingerprintWebStack({ baseUrl: base, fetchText });
631
+ } catch {
632
+ fingerprint = void 0;
633
+ }
634
+ const chunkUrls = baseline?.body ? enumerateJsChunkUrls(baseline.body, base) : [];
635
+ const onVercel = detectVercelHosting(baseline?.headers);
636
+ const activeByCveId = /* @__PURE__ */ new Map();
637
+ if (fingerprint?.framework && isNextFramework(fingerprint.framework.name) && fingerprint.framework.version && baseline) {
638
+ const redirect = deriveProtectedAndLogin(base, baseline);
639
+ if (redirect) {
640
+ try {
641
+ const httpGet = async (path, headers) => {
642
+ const url = new URL(path, `${base}/`).toString();
643
+ if (!isInScopeUrl(url, targetHost, domain)) {
644
+ return { status: 0, headers: {}, body: "" };
645
+ }
646
+ if (limiter)
647
+ await limiter.acquire(url);
648
+ const controller = new AbortController();
649
+ const timer = setTimeout(() => controller.abort(), timeout);
650
+ try {
651
+ const res = await fetch(url, {
652
+ method: "GET",
653
+ headers: { Accept: "*/*", ...headers ?? {} },
654
+ redirect: "manual",
655
+ signal: controller.signal
656
+ });
657
+ limiter?.noteResponse(url, res);
658
+ const body = await res.text();
659
+ const h = {};
660
+ res.headers.forEach((value, key) => {
661
+ h[key.toLowerCase()] = value;
662
+ });
663
+ return { status: res.status, headers: h, body };
664
+ } finally {
665
+ clearTimeout(timer);
666
+ }
667
+ };
668
+ const results = await runFrameworkCveChecks({
669
+ framework: fingerprint.framework.name,
670
+ version: fingerprint.framework.version,
671
+ baseUrl: base,
672
+ protectedPath: redirect.protectedPath,
673
+ loginPath: redirect.loginPath,
674
+ httpGet
675
+ });
676
+ for (const r of results) {
677
+ activeByCveId.set(r.cve, r);
678
+ if (!r.exploitable)
679
+ continue;
680
+ findings.push(createFinding({
681
+ templateId: "web-recon-framework-cve",
682
+ title: `${r.title} (${r.cve})`,
683
+ description: `Active check confirmed ${r.cve} is exploitable on the target.`,
684
+ severity: r.severityIfExploitable,
685
+ category: "security-misconfiguration",
686
+ request: `GET ${redirect.protectedPath} (crafted ${r.cve} check)`,
687
+ response: r.evidence,
688
+ analysis: r.evidence
689
+ }));
690
+ }
691
+ } catch {
692
+ }
693
+ } else {
694
+ leads.push(`Framework is ${fingerprint.framework.name}${fingerprint.framework.version ? ` v${fingerprint.framework.version}` : ""}; could not auto-derive an auth-gated route \u2014 manually identify a protected path and test known middleware-bypass CVEs.`);
695
+ }
696
+ }
697
+ if (fingerprint) {
698
+ const versioned = [];
699
+ if (fingerprint.framework?.version) {
700
+ versioned.push({
701
+ name: npmPackageName(fingerprint.framework.name),
702
+ version: fingerprint.framework.version
703
+ });
704
+ }
705
+ for (const lib of fingerprint.libraries) {
706
+ if (lib.version)
707
+ versioned.push({ name: lib.name, version: lib.version });
708
+ }
709
+ for (const { name, version } of versioned) {
710
+ let cves = [];
711
+ try {
712
+ cves = await lookupVersionCves({ ecosystem: "npm", name, version });
713
+ } catch {
714
+ continue;
715
+ }
716
+ const consolidated = buildConsolidatedCveFinding({
717
+ name,
718
+ version,
719
+ base,
720
+ cves,
721
+ activeByCveId,
722
+ onVercel
723
+ });
724
+ if (consolidated)
725
+ findings.push(consolidated);
726
+ }
727
+ }
728
+ if (chunkUrls.length > 0) {
729
+ try {
730
+ const artifacts = await scanJsArtifacts({ baseUrl: base, chunkUrls, fetchText });
731
+ for (const sm of artifacts.sourceMaps) {
732
+ if (!sm.exposed)
733
+ continue;
734
+ findings.push(createFinding({
735
+ templateId: "web-recon-source-map",
736
+ title: "Exposed JavaScript source map",
737
+ description: "A production JavaScript bundle ships a source map that is served publicly, leaking original (pre-minification) source.",
738
+ severity: "medium",
739
+ category: "information-disclosure",
740
+ request: `GET ${sm.url}`,
741
+ response: "HTTP 200 (source map served)",
742
+ analysis: `The source map at ${sm.url} is publicly reachable and discloses original source code.`
743
+ }));
744
+ }
745
+ for (const secret of artifacts.secrets) {
746
+ if (secret.confidence !== "high")
747
+ continue;
748
+ findings.push(createFinding({
749
+ templateId: "web-recon-leaked-secret",
750
+ title: `Leaked credential in JS bundle (${secret.kind})`,
751
+ description: `A high-confidence ${secret.kind} credential was found in a client-side JavaScript bundle.`,
752
+ severity: "high",
753
+ category: "information-disclosure",
754
+ request: `GET ${secret.chunk}`,
755
+ response: `Matched ${secret.kind}: ${secret.match}`,
756
+ analysis: `A ${secret.kind} value (${secret.match}) was found in ${secret.chunk}. A genuine credential shipped to the browser should be treated as compromised and rotated.`
757
+ }));
758
+ }
759
+ } catch {
760
+ }
761
+ }
762
+ if (domain) {
763
+ try {
764
+ const posture = await checkEmailPosture({ domain });
765
+ for (const f of posture.findings) {
766
+ findings.push(createFinding({
767
+ templateId: "web-recon-email-posture",
768
+ title: f.title,
769
+ description: f.detail,
770
+ severity: f.severity,
771
+ category: "security-misconfiguration",
772
+ request: `DNS TXT lookup for ${domain}`,
773
+ response: f.detail,
774
+ analysis: `Email anti-spoofing posture for ${domain}: ${f.title}.`
775
+ }));
776
+ }
777
+ } catch {
778
+ }
779
+ }
780
+ if (domain) {
781
+ try {
782
+ subdomains = await enumerateSubdomains({ domain });
783
+ for (const host of subdomains) {
784
+ if (host.host === targetHost)
785
+ continue;
786
+ findings.push(createFinding({
787
+ templateId: "web-recon-subdomain",
788
+ title: `Additional exposed host: ${host.host}`,
789
+ description: `A subdomain of ${domain} resolves in DNS and may expand the attack surface.`,
790
+ severity: "info",
791
+ category: "information-disclosure",
792
+ request: `crt.sh + DNS resolution for ${host.host}`,
793
+ response: `host=${host.host}` + (host.addresses?.length ? `, addresses=${host.addresses.join(", ")}` : "") + (host.cname ? `, cname=${host.cname}` : ""),
794
+ analysis: `Discovered via ${host.source}. Additional exposed host worth including in the engagement scope.`
795
+ }));
796
+ }
797
+ } catch {
798
+ subdomains = [];
799
+ }
800
+ }
801
+ const resetEndpoint = baseline?.body ? findPasswordResetEndpoint(base, baseline.body) : void 0;
802
+ if (resetBurstProbeAllowed && resetEndpoint && isInScopeUrl(resetEndpoint, targetHost, domain)) {
803
+ try {
804
+ const invalidEmail = `0sec-noreply-${randomUUID().slice(0, 8)}@invalid.example`;
805
+ const probe = await probeRateLimit({
806
+ request: async () => {
807
+ if (limiter)
808
+ await limiter.acquire(resetEndpoint);
809
+ const controller = new AbortController();
810
+ const timer = setTimeout(() => controller.abort(), timeout);
811
+ try {
812
+ const res = await fetch(resetEndpoint, {
813
+ method: "POST",
814
+ headers: { "content-type": "application/json", Accept: "*/*" },
815
+ body: JSON.stringify({ email: invalidEmail }),
816
+ redirect: "manual",
817
+ signal: controller.signal
818
+ });
819
+ return { status: res.status };
820
+ } finally {
821
+ clearTimeout(timer);
822
+ }
823
+ }
824
+ });
825
+ if (!probe.throttled) {
826
+ findings.push(createFinding({
827
+ templateId: "web-recon-rate-limit",
828
+ title: "Password-reset endpoint lacks rate limiting",
829
+ description: "A small bounded burst of password-reset requests (with a clearly-invalid, non-existent email) saw no throttling \u2014 no HTTP 429 and no status change.",
830
+ severity: "medium",
831
+ category: "security-misconfiguration",
832
+ request: `POST ${resetEndpoint} x${probe.sent} (invalid email ${invalidEmail})`,
833
+ response: probe.note,
834
+ analysis: `${probe.note}. Missing anti-automation controls on a sensitive endpoint enable account enumeration and abuse.`
835
+ }));
836
+ }
837
+ } catch {
838
+ }
839
+ } else if (!resetBurstProbeAllowed) {
840
+ leads.push("Anti-automation testing on sensitive endpoints (password-reset / OTP) is DISABLED for this engagement by the hardening profile \u2014 do not send request bursts. Report missing rate limiting as a manual test item for the client instead.");
841
+ } else {
842
+ leads.push("If you find a password-reset or OTP endpoint, test rate limiting with a safe, clearly-invalid email (e.g. a non-existent @invalid.example address) \u2014 no real mail should be triggered.");
843
+ }
844
+ const promptBlock = buildPromptBlock(fingerprint, findings, subdomains, leads);
845
+ return { findings, promptBlock };
846
+ }
847
+ function buildPromptBlock(fingerprint, findings, subdomains, leads) {
848
+ const lines = [];
849
+ lines.push("## Deterministic web recon (pursue these leads)");
850
+ lines.push("");
851
+ lines.push("A deterministic pre-pass already ran cheap, non-destructive probes against this target. Use the results below as priority leads. Findings it could prove are already saved \u2014 do not re-discover them; build on them.");
852
+ lines.push("");
853
+ if (fingerprint) {
854
+ lines.push("### Detected stack");
855
+ lines.push(summarizeWebStackFingerprint(fingerprint));
856
+ lines.push("");
857
+ }
858
+ const cveFindings = findings.filter((f) => f.templateId === "web-recon-version-cve" || f.templateId === "web-recon-framework-cve");
859
+ if (cveFindings.length > 0) {
860
+ lines.push(`### CVEs already surfaced (${cveFindings.length})`);
861
+ for (const f of cveFindings) {
862
+ lines.push(`- ${f.title}`);
863
+ }
864
+ lines.push("");
865
+ }
866
+ if (subdomains.length > 0) {
867
+ lines.push(`### Additional exposed hosts (${subdomains.length})`);
868
+ for (const host of subdomains.slice(0, 20)) {
869
+ lines.push(`- ${host.host}${host.cname ? ` (cname ${host.cname})` : ""}`);
870
+ }
871
+ lines.push("");
872
+ }
873
+ if (leads.length > 0) {
874
+ lines.push("### Unconfirmed leads to chase");
875
+ for (const lead of leads) {
876
+ lines.push(`- ${lead}`);
877
+ }
878
+ lines.push("");
879
+ }
880
+ return lines.join("\n");
881
+ }
882
+ function buildScanContext(config) {
883
+ return {
884
+ config,
885
+ target: { url: config.target, type: "web-app" },
886
+ findings: [],
887
+ attacks: [],
888
+ warnings: [],
889
+ startedAt: Date.now()
890
+ };
891
+ }
892
+ function mapCveSeverity(severity) {
893
+ switch (severity) {
894
+ case "critical":
895
+ return "critical";
896
+ case "high":
897
+ return "high";
898
+ case "medium":
899
+ return "medium";
900
+ case "low":
901
+ return "low";
902
+ default:
903
+ return "info";
904
+ }
905
+ }
906
+ var SEVERITY_RANK = {
907
+ critical: 4,
908
+ high: 3,
909
+ medium: 2,
910
+ low: 1,
911
+ info: 0
912
+ };
913
+ var VERCEL_EDGE_MITIGATED_CVES = /* @__PURE__ */ new Set([
914
+ "CVE-2025-29927"
915
+ ]);
916
+ function reconcileActiveResult(cve, activeByCveId) {
917
+ const direct = activeByCveId.get(cve.id);
918
+ if (direct)
919
+ return direct;
920
+ if (/\brsc\b|segment[\s-]?prefetch/i.test(`${cve.id} ${cve.summary}`)) {
921
+ return activeByCveId.get("NEXT-RSC-PREFETCH-BYPASS-2026-05");
922
+ }
923
+ return void 0;
924
+ }
925
+ function highestFixedVersion(versions) {
926
+ const cmp = (a, b) => {
927
+ const pa = a.replace(/^v/i, "").split(/[-+]/, 1)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
928
+ const pb = b.replace(/^v/i, "").split(/[-+]/, 1)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
929
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
930
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
931
+ if (d !== 0)
932
+ return d;
933
+ }
934
+ return 0;
935
+ };
936
+ let best;
937
+ for (const v of versions) {
938
+ if (!v)
939
+ continue;
940
+ if (!best || cmp(v, best) > 0)
941
+ best = v;
942
+ }
943
+ return best;
944
+ }
945
+ function buildConsolidatedCveFinding(input) {
946
+ const { name, version, base, cves, activeByCveId, onVercel } = input;
947
+ const relevant = cves.filter((c) => {
948
+ const sev = mapCveSeverity(c.severity);
949
+ return sev === "high" || sev === "critical";
950
+ });
951
+ if (relevant.length === 0)
952
+ return void 0;
953
+ const annotated = relevant.map((cve) => {
954
+ const sev = mapCveSeverity(cve.severity);
955
+ const active = reconcileActiveResult(cve, activeByCveId);
956
+ const confirmed = active?.tested === true && active.exploitable === true;
957
+ const testedNotExploitable = active?.tested === true && active.exploitable === false;
958
+ const vercelMitigated = onVercel && VERCEL_EDGE_MITIGATED_CVES.has(cve.id);
959
+ const countsTowardSeverity = confirmed || !testedNotExploitable && !vercelMitigated;
960
+ let note;
961
+ if (confirmed)
962
+ note = "active check: CONFIRMED exploitable";
963
+ else if (testedNotExploitable)
964
+ note = "active check: NOT exploitable on this deployment";
965
+ else if (vercelMitigated)
966
+ note = "auto-mitigated at the Vercel edge";
967
+ return {
968
+ cve,
969
+ sev,
970
+ confirmed,
971
+ testedNotExploitable,
972
+ vercelMitigated,
973
+ countsTowardSeverity,
974
+ note
975
+ };
976
+ });
977
+ const counting = annotated.filter((a) => a.countsTowardSeverity);
978
+ const rollupSeverity = counting.length ? counting.reduce((max, a) => SEVERITY_RANK[a.sev] > SEVERITY_RANK[max] ? a.sev : max, "info") : "medium";
979
+ const fixedTarget = highestFixedVersion(relevant.map((c) => c.fixedVersion).filter((v) => !!v));
980
+ const confirmedCount = annotated.filter((a) => a.confirmed).length;
981
+ const mitigatedCount = annotated.filter((a) => a.testedNotExploitable || a.vercelMitigated).length;
982
+ const cveLines = annotated.map((a) => {
983
+ const fixed = a.cve.fixedVersion ? ` fixed in ${a.cve.fixedVersion}` : " no fixed version published";
984
+ const annotation = a.note ? ` \u2014 ${a.note}` : "";
985
+ return `- ${a.cve.id} (${a.sev}, ${a.cve.source}):${fixed}${annotation}`;
986
+ }).join("\n");
987
+ const remediation = fixedTarget ? `Upgrade ${name} to >= ${fixedTarget} to clear all listed advisories.` : `Upgrade ${name} to the latest patched release.`;
988
+ const vercelNote = onVercel ? `
989
+
990
+ Platform note: target appears to be hosted on Vercel. Several Next.js advisories \u2014 the x-middleware-subrequest middleware bypass (CVE-2025-29927) and the middleware-bypass / DoS classes \u2014 are auto-mitigated at the Vercel edge and are down-ranked here accordingly.` : "";
991
+ const activeNote = confirmedCount > 0 || mitigatedCount > 0 ? `
992
+
993
+ Active checks reconciled: ${confirmedCount} confirmed exploitable, ${mitigatedCount} tested/known-mitigated (excluded from the rollup severity).` : "";
994
+ const description = `${name}@${version} is VERSION-AFFECTED by ${relevant.length} published HIGH/CRITICAL advisor${relevant.length === 1 ? "y" : "ies"}. This is an SCA (software-composition) signal: the detected version falls in each advisory's affected range, but exploitability against this deployment is NOT confirmed unless an advisory is annotated CONFIRMED below.`;
995
+ const analysis = `Consolidated SCA finding for ${name}@${version} (${relevant.length} advisories).
996
+
997
+ ${cveLines}` + vercelNote + activeNote + `
998
+
999
+ Remediation: ${remediation}`;
1000
+ return createFinding({
1001
+ templateId: "web-recon-version-cve",
1002
+ title: `Known-vulnerable dependency: ${name}@${version} (${relevant.length} published advisor${relevant.length === 1 ? "y" : "ies"})`,
1003
+ description,
1004
+ severity: rollupSeverity,
1005
+ category: "known-vulnerable-package",
1006
+ request: `GET ${base} (stack fingerprint: ${name}@${version})`,
1007
+ response: `${relevant.length} advisories affect ${name}@${version}: ${relevant.map((c) => c.id).join(", ")}`,
1008
+ analysis
1009
+ });
1010
+ }
1011
+ function detectVercelHosting(headers) {
1012
+ if (!headers)
1013
+ return false;
1014
+ if (typeof headers["x-vercel-id"] === "string" && headers["x-vercel-id"])
1015
+ return true;
1016
+ const server = headers["server"];
1017
+ if (typeof server === "string" && /vercel/i.test(server))
1018
+ return true;
1019
+ return false;
1020
+ }
1021
+ function npmPackageName(frameworkName) {
1022
+ const map = {
1023
+ "next.js": "next",
1024
+ nuxt: "nuxt",
1025
+ remix: "@remix-run/server-runtime",
1026
+ sveltekit: "@sveltejs/kit",
1027
+ gatsby: "gatsby",
1028
+ astro: "astro",
1029
+ react: "react",
1030
+ vue: "vue",
1031
+ express: "express"
1032
+ };
1033
+ return map[frameworkName.toLowerCase()] ?? frameworkName.toLowerCase();
1034
+ }
1035
+ function isNextFramework(name) {
1036
+ return /^next(\.?js)?$/i.test(name.trim());
1037
+ }
1038
+ function safeHost(target) {
1039
+ try {
1040
+ return new URL(target).hostname.toLowerCase();
1041
+ } catch {
1042
+ return void 0;
1043
+ }
1044
+ }
1045
+ function registrableDomain(target) {
1046
+ const host = safeHost(target);
1047
+ if (!host)
1048
+ return void 0;
1049
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":"))
1050
+ return void 0;
1051
+ const stripped = host.replace(/^www\./, "");
1052
+ const labels = stripped.split(".");
1053
+ if (labels.length <= 2)
1054
+ return stripped;
1055
+ return labels.slice(-2).join(".");
1056
+ }
1057
+ function isPrivateHost(host) {
1058
+ const h = host.replace(/^\[|\]$/g, "").toLowerCase();
1059
+ if (h === "localhost" || h.endsWith(".localhost") || h === "0.0.0.0")
1060
+ return true;
1061
+ const v4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
1062
+ if (v4) {
1063
+ const a = Number(v4[1]);
1064
+ const b = Number(v4[2]);
1065
+ if (a === 0 || a === 10 || a === 127)
1066
+ return true;
1067
+ if (a === 169 && b === 254)
1068
+ return true;
1069
+ if (a === 172 && b >= 16 && b <= 31)
1070
+ return true;
1071
+ if (a === 192 && b === 168)
1072
+ return true;
1073
+ if (a === 100 && b >= 64 && b <= 127)
1074
+ return true;
1075
+ return false;
1076
+ }
1077
+ if (h.includes(":")) {
1078
+ if (h === "::1" || /^f[cd][0-9a-f]{2}:/.test(h) || /^fe80:/.test(h))
1079
+ return true;
1080
+ }
1081
+ return false;
1082
+ }
1083
+ function isInScopeUrl(rawUrl, targetHost, domain) {
1084
+ let u;
1085
+ try {
1086
+ u = new URL(rawUrl);
1087
+ } catch {
1088
+ return false;
1089
+ }
1090
+ if (u.protocol !== "https:" && u.protocol !== "http:")
1091
+ return false;
1092
+ const host = u.hostname.toLowerCase();
1093
+ if (isPrivateHost(host))
1094
+ return false;
1095
+ if (targetHost && host === targetHost.toLowerCase())
1096
+ return true;
1097
+ if (domain && (host === domain || host.endsWith("." + domain)))
1098
+ return true;
1099
+ return false;
1100
+ }
1101
+ function deriveProtectedAndLogin(base, baseline) {
1102
+ if (baseline.status < 300 || baseline.status >= 400)
1103
+ return void 0;
1104
+ const location = baseline.headers["location"];
1105
+ if (!location)
1106
+ return void 0;
1107
+ let loginPath;
1108
+ try {
1109
+ loginPath = new URL(location, `${base}/`).pathname;
1110
+ } catch {
1111
+ loginPath = location;
1112
+ }
1113
+ if (!/login|signin|sign-in|auth/i.test(loginPath))
1114
+ return void 0;
1115
+ return { protectedPath: "/", loginPath };
1116
+ }
1117
+ function findPasswordResetEndpoint(base, html) {
1118
+ const refs = [...html.matchAll(/(?:href|action)=["']([^"']+)["']/gi)].map((m) => m[1]);
1119
+ const match = refs.find((r) => /(forgot|reset).*(password|pwd)|password.*(reset|forgot)|reset-password|forgot-password/i.test(r));
1120
+ if (!match)
1121
+ return void 0;
1122
+ try {
1123
+ return new URL(match, `${base}/`).toString();
1124
+ } catch {
1125
+ return void 0;
1126
+ }
1127
+ }
1128
+ function createFinding(input) {
1129
+ return {
1130
+ id: randomUUID(),
1131
+ templateId: input.templateId,
1132
+ title: input.title,
1133
+ description: input.description,
1134
+ severity: input.severity,
1135
+ category: input.category,
1136
+ status: "discovered",
1137
+ evidence: {
1138
+ request: input.request,
1139
+ response: input.response,
1140
+ analysis: input.analysis
1141
+ },
1142
+ timestamp: Date.now()
1143
+ };
1144
+ }
1145
+ export {
1146
+ isInScopeUrl,
1147
+ runWebReconPrePass
1148
+ };