0sec-cli 0.15.0 → 0.18.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 +327 -0
  2. package/README.md +68 -48
  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-SMH6V4PD.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-HQ7HLP6V.js +34 -0
  59. package/chunks/assumption-mining-5SNIVVDN.js +79 -0
  60. package/chunks/chunk-242TMK6G.js +110 -0
  61. package/chunks/chunk-2JCCA2JL.js +381 -0
  62. package/chunks/chunk-2RMLOJVB.js +47 -0
  63. package/chunks/chunk-3KPWWJCI.js +1422 -0
  64. package/chunks/chunk-3MOLBTLS.js +953 -0
  65. package/chunks/chunk-53G27VPS.js +136 -0
  66. package/chunks/chunk-57ZENEX2.js +169 -0
  67. package/chunks/chunk-5G3ZXFBW.js +182 -0
  68. package/chunks/chunk-5JI7L7KV.js +74240 -0
  69. package/chunks/chunk-6L26OBWI.js +1094 -0
  70. package/chunks/chunk-6LKRLK2R.js +542 -0
  71. package/chunks/chunk-72CISA2D.js +2597 -0
  72. package/chunks/chunk-7DQEV5QI.js +287 -0
  73. package/chunks/chunk-AD7WXDH6.js +3250 -0
  74. package/chunks/chunk-ADOLI6DT.js +102 -0
  75. package/chunks/chunk-AHTBZC3E.js +596 -0
  76. package/chunks/chunk-ASUR522M.js +518 -0
  77. package/chunks/chunk-ATATQACO.js +471 -0
  78. package/chunks/chunk-BATRQBOR.js +1732 -0
  79. package/chunks/chunk-BFR2CDV5.js +5735 -0
  80. package/chunks/chunk-BFRB6HB2.js +235 -0
  81. package/chunks/chunk-BKZFDZ23.js +361 -0
  82. package/chunks/chunk-BVAZO4WA.js +193 -0
  83. package/chunks/chunk-BZNEY2ZS.js +33225 -0
  84. package/chunks/chunk-C2WXWFBB.js +1070 -0
  85. package/chunks/chunk-CMKXP5RR.js +917 -0
  86. package/chunks/chunk-DW5UWPFY.js +275 -0
  87. package/chunks/chunk-F3WBKITT.js +687 -0
  88. package/chunks/chunk-GMT2AZKM.js +2439 -0
  89. package/chunks/chunk-H2FFLZNK.js +3 -0
  90. package/chunks/chunk-IOL7D5YV.js +515 -0
  91. package/chunks/chunk-IR537GON.js +49 -0
  92. package/chunks/chunk-JQHOLLTD.js +3729 -0
  93. package/chunks/chunk-KLTNTE2Z.js +121 -0
  94. package/chunks/chunk-LP3HHYQU.js +245 -0
  95. package/chunks/chunk-MYVT64FN.js +16 -0
  96. package/chunks/chunk-N7BOUYEV.js +852 -0
  97. package/chunks/chunk-OEFNRYI2.js +150 -0
  98. package/chunks/chunk-P6WKNFWX.js +1968 -0
  99. package/chunks/chunk-QI233I24.js +333 -0
  100. package/chunks/chunk-RJWOYEOG.js +271 -0
  101. package/chunks/chunk-RRMJC3ZE.js +105 -0
  102. package/chunks/chunk-RWONANDA.js +973 -0
  103. package/chunks/chunk-SAFFWQW4.js +5921 -0
  104. package/chunks/chunk-SF4KZ4O3.js +502 -0
  105. package/chunks/chunk-SVJEDVYK.js +808 -0
  106. package/chunks/chunk-TSK2AG6J.js +5108 -0
  107. package/chunks/chunk-U5FLXGHT.js +2010 -0
  108. package/chunks/chunk-UCYRA73C.js +662 -0
  109. package/chunks/chunk-UR4ELX2Y.js +4784 -0
  110. package/chunks/chunk-VQG4FT5D.js +685 -0
  111. package/chunks/chunk-WEBMRWEG.js +9337 -0
  112. package/chunks/chunk-WU6AFRAZ.js +1167 -0
  113. package/chunks/chunk-WVTBZEQO.js +1531 -0
  114. package/chunks/chunk-WXQ6BJ6A.js +596 -0
  115. package/chunks/chunk-XM5NLHYU.js +600 -0
  116. package/chunks/chunk-YLMN3N25.js +4054 -0
  117. package/chunks/chunk-YYRBQVFE.js +2572 -0
  118. package/chunks/chunk-Z5FA2XOX.js +2798 -0
  119. package/chunks/commands-TBC2F5CE.js +16999 -0
  120. package/chunks/corpus-v1.json +403 -0
  121. package/chunks/cost-ledger-NU63MYZM.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-KFWOAUUB.js +16 -0
  127. package/chunks/disclose-EKTWSCGW.js +132 -0
  128. package/chunks/dist-4MAW5X6L.js +74 -0
  129. package/chunks/dist-4YICS2J2.js +2776 -0
  130. package/chunks/dist-FMGSR3DW.js +159 -0
  131. package/chunks/eval-runner-7ZF472KV.js +27 -0
  132. package/chunks/example-manifest.json +95 -0
  133. package/chunks/exploit-agent-45YQOHPJ.js +98 -0
  134. package/chunks/exploit-autoclimb-UZGEMGYT.js +81 -0
  135. package/chunks/exploit-climb-SG23KB2G.js +358 -0
  136. package/chunks/fix-IG7LYXH3.js +12 -0
  137. package/chunks/github-issues-MJ6OYOOU.js +157 -0
  138. package/chunks/harness-7ODOLOWU.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-MMKUOETU.js +38 -0
  142. package/chunks/identity-6ZAIIWOR.js +191 -0
  143. package/chunks/kernel-primitive-7U6CWHXD.js +44 -0
  144. package/chunks/kernel-vm-runner-3JAU4O6J.js +62 -0
  145. package/chunks/memsafety-scan-J7SECV5W.js +16 -0
  146. package/chunks/native-loop-RJCJPOUS.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-OC2VZN2X.js +62 -0
  150. package/chunks/pipeline-BZK5BLJR.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-LQRCS5OY.js +13 -0
  154. package/chunks/replay-runner-45G3VU7X.js +39 -0
  155. package/chunks/run-D56MPDKY.js +32716 -0
  156. package/chunks/runtime-G7YWIO3B.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-GZ4A4KYT.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-YERCYMPD.js +13 -0
  164. package/chunks/web-recon-prepass-JJGPEOLE.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,917 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __0secCreateRequire } from "node:module";
3
+ const require = __0secCreateRequire(import.meta.url);
4
+ import {
5
+ jitterFor
6
+ } from "./chunk-YYRBQVFE.js";
7
+
8
+ // packages/core/dist/scope/rate-limit.js
9
+ var TokenBucket = class {
10
+ state;
11
+ nowFn;
12
+ sleepFn;
13
+ jitter;
14
+ constructor(rps, capacity = rps, nowFn = () => Date.now(), sleepFn = defaultSleep, jitter) {
15
+ if (!Number.isFinite(rps) || rps <= 0) {
16
+ throw new Error(`TokenBucket: rps must be a positive finite number, got ${rps}`);
17
+ }
18
+ if (!Number.isFinite(capacity) || capacity <= 0) {
19
+ throw new Error(`TokenBucket: capacity must be a positive finite number, got ${capacity}`);
20
+ }
21
+ this.nowFn = nowFn;
22
+ this.sleepFn = sleepFn;
23
+ this.jitter = jitter && jitter.baseMs > 0 ? jitter : void 0;
24
+ this.state = {
25
+ tokens: capacity,
26
+ refillRatePerMs: rps / 1e3,
27
+ capacity,
28
+ lastRefill: nowFn(),
29
+ retryUntil: 0
30
+ };
31
+ }
32
+ refill() {
33
+ const now = this.nowFn();
34
+ const elapsed = now - this.state.lastRefill;
35
+ if (elapsed > 0) {
36
+ this.state.tokens = Math.min(this.state.capacity, this.state.tokens + elapsed * this.state.refillRatePerMs);
37
+ this.state.lastRefill = now;
38
+ }
39
+ }
40
+ /**
41
+ * Non-blocking. Returns true and consumes `n` tokens if available;
42
+ * returns false and consumes nothing otherwise. Blocked during 429
43
+ * cool-off (returns false until park expires).
44
+ */
45
+ tryConsume(n = 1) {
46
+ if (this.nowFn() < this.state.retryUntil)
47
+ return false;
48
+ this.refill();
49
+ if (this.state.tokens >= n) {
50
+ this.state.tokens -= n;
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ /**
56
+ * Block until `n` tokens are available, then consume them. Honours
57
+ * the 429 park deadline first, then refill-and-spin with sleeps
58
+ * sized to the exact next-token-available time.
59
+ */
60
+ async acquire(n = 1) {
61
+ if (n > this.state.capacity) {
62
+ n = this.state.capacity;
63
+ }
64
+ while (this.nowFn() < this.state.retryUntil) {
65
+ const wait = this.state.retryUntil - this.nowFn();
66
+ await this.sleepFn(Math.max(1, Math.min(wait, 1e3)));
67
+ }
68
+ while (true) {
69
+ this.refill();
70
+ if (this.state.tokens >= n) {
71
+ this.state.tokens -= n;
72
+ await this.pace();
73
+ return;
74
+ }
75
+ const tokensNeeded = n - this.state.tokens;
76
+ const waitMs = Math.max(1, Math.ceil(tokensNeeded / this.state.refillRatePerMs));
77
+ await this.sleepFn(waitMs);
78
+ }
79
+ }
80
+ /**
81
+ * Sleep the configured full-jitter delay. No-op (and no `await`-visible
82
+ * sleep) when no jitter is configured, so the default path is unchanged.
83
+ *
84
+ * Reuses `jitterFor` from `waf-detect.ts` — attempt 0 with an injected rng
85
+ * yields full jitter in `[0, baseMs]` — rather than growing a second
86
+ * jitter implementation.
87
+ */
88
+ async pace() {
89
+ if (!this.jitter)
90
+ return;
91
+ const ms = jitterFor(0, this.jitter.baseMs, this.jitter.rng ?? Math.random);
92
+ if (ms > 0)
93
+ await this.sleepFn(ms);
94
+ }
95
+ /**
96
+ * Park the bucket until `deadline` (ms since epoch). Subsequent
97
+ * `acquire`/`tryConsume` calls block until the deadline passes.
98
+ * Idempotent in the "later wins" sense: a longer park overrides a
99
+ * shorter one, but a shorter park does NOT shorten an existing one.
100
+ */
101
+ markRetryUntil(deadline) {
102
+ if (deadline > this.state.retryUntil) {
103
+ this.state.retryUntil = deadline;
104
+ }
105
+ }
106
+ /** Test introspection. */
107
+ _peek() {
108
+ this.refill();
109
+ return {
110
+ tokens: this.state.tokens,
111
+ retryUntil: this.state.retryUntil,
112
+ capacity: this.state.capacity
113
+ };
114
+ }
115
+ };
116
+ var RateLimiter = class _RateLimiter {
117
+ buckets = /* @__PURE__ */ new Map();
118
+ defaultCfg;
119
+ perHost;
120
+ jitter;
121
+ nowFn;
122
+ sleepFn;
123
+ /**
124
+ * Conservative cool-off floor when a 429 lands without a useful
125
+ * `Retry-After`, or with a tiny one. 60s matches the PoC-runtime
126
+ * precedent in #206 — a target that just told us to slow down has
127
+ * earned more than a one-second pause.
128
+ */
129
+ static RETRY_AFTER_FLOOR_MS = 6e4;
130
+ /**
131
+ * Optional observer invoked once whenever a fetch is actually throttled:
132
+ * either an `acquire` that had to block because the bucket was dry, or a
133
+ * 429 response parking the host. Used by the http_audit enforcement
134
+ * tracker to populate `rate_limited_count`. No-op when unset, so the
135
+ * normal scan path is unaffected.
136
+ */
137
+ onThrottle;
138
+ constructor(config, opts = {}) {
139
+ this.defaultCfg = config.default;
140
+ this.jitter = config.jitter;
141
+ this.perHost = {};
142
+ if (config.perHost) {
143
+ for (const [host, cfg] of Object.entries(config.perHost)) {
144
+ this.perHost[host.toLowerCase()] = cfg;
145
+ }
146
+ }
147
+ this.nowFn = opts.nowFn ?? (() => Date.now());
148
+ this.sleepFn = opts.sleepFn ?? defaultSleep;
149
+ this.onThrottle = opts.onThrottle;
150
+ }
151
+ /**
152
+ * Normalize a URL or host string to a bucket key. Lowercased,
153
+ * port-stripped, IPv6 brackets retained on the literal but dropped
154
+ * from the port suffix (e.g. `[::1]:443` → `[::1]`).
155
+ */
156
+ hostKey(urlOrHost) {
157
+ let host;
158
+ try {
159
+ host = new URL(urlOrHost).hostname;
160
+ } catch {
161
+ const trimmed = urlOrHost.trim();
162
+ if (!trimmed)
163
+ return null;
164
+ if (trimmed.startsWith("[")) {
165
+ const close = trimmed.indexOf("]");
166
+ if (close < 0)
167
+ return null;
168
+ host = trimmed.slice(0, close + 1);
169
+ } else {
170
+ host = trimmed.split(":")[0];
171
+ }
172
+ }
173
+ return host.toLowerCase();
174
+ }
175
+ getBucket(host) {
176
+ let bucket = this.buckets.get(host);
177
+ if (!bucket) {
178
+ const cfg = this.perHost[host] ?? this.defaultCfg;
179
+ bucket = new TokenBucket(cfg.rps, cfg.burst ?? cfg.rps, this.nowFn, this.sleepFn, this.jitter);
180
+ this.buckets.set(host, bucket);
181
+ }
182
+ return bucket;
183
+ }
184
+ /**
185
+ * Block until a token is available for `urlOrHost`'s host, then
186
+ * consume it. Unparseable input is treated as a no-op (returns
187
+ * immediately) — callers should not rely on rate-limiting unknowable
188
+ * hosts. The fetch sites that wire this up always pass full URLs.
189
+ */
190
+ async acquire(urlOrHost, n = 1) {
191
+ const host = this.hostKey(urlOrHost);
192
+ if (!host)
193
+ return;
194
+ const bucket = this.getBucket(host);
195
+ if (bucket.tryConsume(n)) {
196
+ await bucket.pace();
197
+ return;
198
+ }
199
+ this.onThrottle?.();
200
+ await bucket.acquire(n);
201
+ }
202
+ /**
203
+ * Non-blocking variant. Returns true and consumes if available,
204
+ * false otherwise. Used by callers that want to fall through to a
205
+ * fast-path / degrade rather than wait.
206
+ */
207
+ tryAcquire(urlOrHost, n = 1) {
208
+ const host = this.hostKey(urlOrHost);
209
+ if (!host)
210
+ return true;
211
+ return this.getBucket(host).tryConsume(n);
212
+ }
213
+ /**
214
+ * Inspect a fetch response and, if it's a 429, park the host's
215
+ * bucket until `Retry-After` (or 60s, whichever is longer). The
216
+ * parsed value supports both delta-seconds (RFC 7231 §7.1.3) and
217
+ * HTTP-date forms; anything else falls back to the floor.
218
+ *
219
+ * No-ops on non-429 responses, or on hosts with no bucket yet
220
+ * (which can't happen if the caller acquired before fetching, but
221
+ * we don't depend on that).
222
+ */
223
+ noteResponse(urlOrHost, res) {
224
+ if (res.status !== 429)
225
+ return;
226
+ const host = this.hostKey(urlOrHost);
227
+ if (!host)
228
+ return;
229
+ this.onThrottle?.();
230
+ const bucket = this.getBucket(host);
231
+ const retryAfter = readHeader(res.headers, "retry-after");
232
+ const delayMs = parseRetryAfter(retryAfter, this.nowFn());
233
+ const deadline = this.nowFn() + Math.max(delayMs, _RateLimiter.RETRY_AFTER_FLOOR_MS);
234
+ bucket.markRetryUntil(deadline);
235
+ }
236
+ /** Test-only. Reset all buckets. */
237
+ _reset() {
238
+ this.buckets.clear();
239
+ }
240
+ };
241
+ function defaultSleep(ms) {
242
+ return new Promise((r) => setTimeout(r, ms));
243
+ }
244
+ function readHeader(headers, name) {
245
+ if (typeof headers.get === "function") {
246
+ return headers.get(name);
247
+ }
248
+ return null;
249
+ }
250
+ function parseRetryAfter(value, now) {
251
+ if (!value)
252
+ return 0;
253
+ const trimmed = value.trim();
254
+ if (!trimmed)
255
+ return 0;
256
+ if (/^\d+$/.test(trimmed)) {
257
+ const seconds = parseInt(trimmed, 10);
258
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : 0;
259
+ }
260
+ const date = Date.parse(trimmed);
261
+ if (Number.isFinite(date)) {
262
+ return Math.max(0, date - now);
263
+ }
264
+ return 0;
265
+ }
266
+ function parseRateLimitFlag(spec, fallbackRps = 5) {
267
+ const trimmed = spec.trim();
268
+ if (!trimmed) {
269
+ return { default: { rps: fallbackRps } };
270
+ }
271
+ const parts = trimmed.split(",").map((p) => p.trim()).filter(Boolean);
272
+ let defaultCfg = null;
273
+ const perHost = {};
274
+ for (const part of parts) {
275
+ const eq = part.indexOf("=");
276
+ if (eq < 0) {
277
+ const cfg = parseRpsBurst(part);
278
+ defaultCfg = cfg;
279
+ } else {
280
+ const host = part.slice(0, eq).trim().toLowerCase();
281
+ const cfg = parseRpsBurst(part.slice(eq + 1));
282
+ if (!host) {
283
+ throw new Error(`--rate-limit: empty host in "${part}"`);
284
+ }
285
+ perHost[host] = cfg;
286
+ }
287
+ }
288
+ return {
289
+ default: defaultCfg ?? { rps: fallbackRps },
290
+ perHost: Object.keys(perHost).length > 0 ? perHost : void 0
291
+ };
292
+ }
293
+ function parseRpsBurst(s) {
294
+ const trimmed = s.trim();
295
+ if (!trimmed) {
296
+ throw new Error(`--rate-limit: empty rps value`);
297
+ }
298
+ const colon = trimmed.indexOf(":");
299
+ if (colon < 0) {
300
+ const rps2 = Number(trimmed);
301
+ if (!Number.isFinite(rps2) || rps2 <= 0) {
302
+ throw new Error(`--rate-limit: invalid rps "${trimmed}"`);
303
+ }
304
+ return { rps: rps2 };
305
+ }
306
+ const rps = Number(trimmed.slice(0, colon));
307
+ const burst = Number(trimmed.slice(colon + 1));
308
+ if (!Number.isFinite(rps) || rps <= 0) {
309
+ throw new Error(`--rate-limit: invalid rps "${trimmed.slice(0, colon)}"`);
310
+ }
311
+ if (!Number.isFinite(burst) || burst <= 0) {
312
+ throw new Error(`--rate-limit: invalid burst "${trimmed.slice(colon + 1)}"`);
313
+ }
314
+ return { rps, burst };
315
+ }
316
+
317
+ // packages/core/dist/stages/web.js
318
+ import { randomUUID } from "node:crypto";
319
+ var RATE_LIMITER_CACHE = /* @__PURE__ */ new WeakMap();
320
+ function getStageRateLimiter(config) {
321
+ let rl = RATE_LIMITER_CACHE.get(config);
322
+ if (!rl) {
323
+ rl = new RateLimiter(parseRateLimitFlag(config.rateLimit ?? "", 5));
324
+ RATE_LIMITER_CACHE.set(config, rl);
325
+ }
326
+ return rl;
327
+ }
328
+ var EVIL_ORIGIN = "https://evil.example";
329
+ async function runWebDiscoveryProbe(ctx) {
330
+ const response = await requestUrl(ctx.config.target, {
331
+ method: "GET",
332
+ headers: {
333
+ Accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8"
334
+ },
335
+ timeout: ctx.config.timeout,
336
+ rateLimiter: getStageRateLimiter(ctx.config)
337
+ });
338
+ return {
339
+ url: ctx.config.target,
340
+ type: "web-app",
341
+ endpoints: discoverInterestingEndpoints(response.body),
342
+ detectedFeatures: detectWebFeatures(response.body, response.headers)
343
+ };
344
+ }
345
+ async function runBaselineWebChecks(ctx) {
346
+ const findings = [];
347
+ const results = [];
348
+ const baseResponse = await requestUrl(ctx.config.target, {
349
+ method: "GET",
350
+ headers: {
351
+ Accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8"
352
+ },
353
+ timeout: ctx.config.timeout,
354
+ rateLimiter: getStageRateLimiter(ctx.config)
355
+ });
356
+ results.push(createAttackResult("web-root-fetch", "baseline-get", "GET /", summarizeResponse(baseResponse), "safe", baseResponse.latencyMs));
357
+ const headerFinding = buildSecurityHeadersFinding(ctx, baseResponse);
358
+ results.push(createAttackResult("web-security-headers", "missing-headers", "GET / (security header audit)", summarizeResponse(baseResponse), headerFinding ? "vulnerable" : "safe", baseResponse.latencyMs));
359
+ if (headerFinding)
360
+ findings.push(headerFinding);
361
+ const disclosureFinding = buildHeaderDisclosureFinding(baseResponse);
362
+ results.push(createAttackResult("web-header-disclosure", "server-fingerprint", "GET / (response header disclosure check)", summarizeResponse(baseResponse), disclosureFinding ? "vulnerable" : "safe", baseResponse.latencyMs));
363
+ if (disclosureFinding)
364
+ findings.push(disclosureFinding);
365
+ const corsCheck = await runCorsCheck(ctx);
366
+ results.push(corsCheck.result);
367
+ if (corsCheck.finding)
368
+ findings.push(corsCheck.finding);
369
+ const sensitivePathChecks = await runSensitivePathChecks(ctx);
370
+ results.push(...sensitivePathChecks.results);
371
+ findings.push(...sensitivePathChecks.findings);
372
+ return { findings, results };
373
+ }
374
+ async function runCorsCheck(ctx) {
375
+ const headers = {
376
+ Origin: EVIL_ORIGIN,
377
+ "Access-Control-Request-Method": "POST",
378
+ "Access-Control-Request-Headers": "content-type,authorization"
379
+ };
380
+ let response;
381
+ try {
382
+ response = await requestUrl(ctx.config.target, {
383
+ method: "OPTIONS",
384
+ headers,
385
+ timeout: ctx.config.timeout,
386
+ rateLimiter: getStageRateLimiter(ctx.config)
387
+ });
388
+ } catch {
389
+ response = await requestUrl(ctx.config.target, {
390
+ method: "GET",
391
+ headers: { Origin: EVIL_ORIGIN },
392
+ timeout: ctx.config.timeout,
393
+ rateLimiter: getStageRateLimiter(ctx.config)
394
+ });
395
+ }
396
+ const origin = response.headers["access-control-allow-origin"];
397
+ const credentials = response.headers["access-control-allow-credentials"]?.toLowerCase() === "true";
398
+ let severity = null;
399
+ let description = "";
400
+ if ((origin === "*" || origin === EVIL_ORIGIN) && credentials) {
401
+ severity = "high";
402
+ description = "The application allows a hostile origin and also permits credentialed cross-origin requests, enabling browser-based data theft.";
403
+ } else if (origin === EVIL_ORIGIN) {
404
+ severity = "medium";
405
+ description = "The application reflects an arbitrary Origin value, which weakens same-origin protections for browser clients.";
406
+ } else if (origin === "*") {
407
+ severity = "medium";
408
+ description = "The application allows any origin with Access-Control-Allow-Origin: *, expanding cross-origin data exposure.";
409
+ }
410
+ return {
411
+ finding: severity ? createFinding({
412
+ templateId: "web-cors",
413
+ title: "Permissive CORS policy",
414
+ description,
415
+ severity,
416
+ category: "cors",
417
+ request: `OPTIONS ${ctx.config.target}
418
+ Origin: ${EVIL_ORIGIN}
419
+ Access-Control-Request-Method: POST`,
420
+ response: summarizeResponse(response),
421
+ analysis: `Observed CORS headers: Access-Control-Allow-Origin=${origin ?? "<absent>"}, Access-Control-Allow-Credentials=${response.headers["access-control-allow-credentials"] ?? "<absent>"}.`
422
+ }) : void 0,
423
+ result: createAttackResult("web-cors", "arbitrary-origin", `OPTIONS / with Origin: ${EVIL_ORIGIN}`, summarizeResponse(response), severity ? "vulnerable" : "safe", response.latencyMs)
424
+ };
425
+ }
426
+ async function runSensitivePathChecks(ctx) {
427
+ const candidates = [
428
+ {
429
+ path: "/.git/config",
430
+ title: "Exposed Git metadata",
431
+ severity: "high",
432
+ analysis: "The repository's Git config is directly accessible over HTTP, which can disclose repository layout and deployment details.",
433
+ matcher: /\[core\]|\[remote/i
434
+ },
435
+ {
436
+ path: "/.env",
437
+ title: "Exposed environment file",
438
+ severity: "critical",
439
+ analysis: "The application exposes its .env file over HTTP, which commonly contains secrets and deployment configuration.",
440
+ // Require at least two KEY=value lines to avoid matching random HTML attributes
441
+ matcher: /^[A-Z][A-Z0-9_]+=\S+/m,
442
+ minMatches: 2
443
+ },
444
+ {
445
+ path: "/server-status",
446
+ title: "Exposed server status endpoint",
447
+ severity: "medium",
448
+ analysis: "A live server-status style endpoint is publicly reachable, leaking operational details about the web stack.",
449
+ matcher: /server version|total accesses|uptime|busyworkers/i
450
+ }
451
+ ];
452
+ const findings = [];
453
+ const results = [];
454
+ for (const candidate of candidates) {
455
+ try {
456
+ const url = new URL(candidate.path, ctx.config.target).toString();
457
+ const response = await requestUrl(url, {
458
+ method: "GET",
459
+ headers: { Accept: "*/*" },
460
+ timeout: ctx.config.timeout,
461
+ rateLimiter: getStageRateLimiter(ctx.config)
462
+ });
463
+ const contentType = response.headers["content-type"]?.toLowerCase() ?? "";
464
+ const isHtmlResponse = contentType.includes("text/html") || /^\s*<!doctype html/i.test(response.body) || /<html[\s>]/i.test(response.body);
465
+ const matchCount = (response.body.match(new RegExp(candidate.matcher, "gm")) ?? []).length;
466
+ const minRequired = "minMatches" in candidate ? candidate.minMatches : 1;
467
+ const vulnerable = response.status >= 200 && response.status < 300 && !isHtmlResponse && matchCount >= minRequired;
468
+ results.push(createAttackResult("web-sensitive-paths", candidate.path, `GET ${candidate.path}`, summarizeResponse(response), vulnerable ? "vulnerable" : "safe", response.latencyMs));
469
+ if (!vulnerable)
470
+ continue;
471
+ findings.push(createFinding({
472
+ templateId: "web-sensitive-paths",
473
+ title: candidate.title,
474
+ description: candidate.analysis,
475
+ severity: candidate.severity,
476
+ category: "security-misconfiguration",
477
+ request: `GET ${candidate.path}`,
478
+ response: summarizeResponse(response),
479
+ analysis: `The response from ${candidate.path} matched sensitive content signatures and should not be publicly accessible.`
480
+ }));
481
+ } catch {
482
+ results.push(createAttackResult("web-sensitive-paths", candidate.path, `GET ${candidate.path}`, "Request failed", "error", 0, "Request failed"));
483
+ }
484
+ }
485
+ return { findings, results };
486
+ }
487
+ function buildSecurityHeadersFinding(ctx, response) {
488
+ const requiredHeaders = [
489
+ {
490
+ name: "content-security-policy",
491
+ label: "Content-Security-Policy",
492
+ impact: "without CSP, browsers have fewer defenses against injected script execution"
493
+ },
494
+ {
495
+ name: "x-frame-options",
496
+ label: "X-Frame-Options",
497
+ impact: "without X-Frame-Options, the site is more exposed to clickjacking"
498
+ },
499
+ {
500
+ name: "x-content-type-options",
501
+ label: "X-Content-Type-Options",
502
+ impact: "without X-Content-Type-Options, some browsers may MIME-sniff responses unexpectedly"
503
+ }
504
+ ];
505
+ if (ctx.config.target.startsWith("https://")) {
506
+ requiredHeaders.push({
507
+ name: "strict-transport-security",
508
+ label: "Strict-Transport-Security",
509
+ impact: "without HSTS, browsers may downgrade future visits to plaintext HTTP"
510
+ });
511
+ }
512
+ const missing = requiredHeaders.filter((header) => !response.headers[header.name]);
513
+ if (missing.length === 0)
514
+ return null;
515
+ const severity = missing.some((header) => header.name === "content-security-policy" || header.name === "x-frame-options") ? "medium" : "low";
516
+ const summary = missing.map((header) => `${header.label} (${header.impact})`).join("; ");
517
+ return createFinding({
518
+ templateId: "web-security-headers",
519
+ title: "Missing recommended security headers",
520
+ description: `The application is missing ${missing.map((header) => header.label).join(", ")}.`,
521
+ severity,
522
+ category: "security-misconfiguration",
523
+ request: `GET ${ctx.config.target}`,
524
+ response: summarizeResponse(response),
525
+ analysis: summary
526
+ });
527
+ }
528
+ function buildHeaderDisclosureFinding(response) {
529
+ const exposed = ["server", "x-powered-by"].filter((header) => response.headers[header]).map((header) => `${header}: ${response.headers[header]}`);
530
+ if (exposed.length === 0)
531
+ return null;
532
+ return createFinding({
533
+ templateId: "web-header-disclosure",
534
+ title: "Technology fingerprint exposed in response headers",
535
+ description: "The application discloses server/framework details in response headers.",
536
+ severity: "low",
537
+ category: "information-disclosure",
538
+ request: `GET ${response.url}`,
539
+ response: summarizeResponse(response),
540
+ analysis: `Observed fingerprinting headers: ${exposed.join(", ")}.`
541
+ });
542
+ }
543
+ async function requestUrl(url, options) {
544
+ const start = Date.now();
545
+ const controller = new AbortController();
546
+ const timer = setTimeout(() => controller.abort(), options.timeout ?? 3e4);
547
+ try {
548
+ if (options.rateLimiter)
549
+ await options.rateLimiter.acquire(url);
550
+ const response = await fetch(url, {
551
+ method: options.method,
552
+ headers: options.headers,
553
+ redirect: "manual",
554
+ signal: controller.signal
555
+ });
556
+ if (options.rateLimiter)
557
+ options.rateLimiter.noteResponse(url, response);
558
+ const body = await response.text();
559
+ const headers = {};
560
+ response.headers.forEach((value, key) => {
561
+ headers[key] = value;
562
+ });
563
+ return {
564
+ url,
565
+ status: response.status,
566
+ body,
567
+ headers,
568
+ latencyMs: Date.now() - start
569
+ };
570
+ } finally {
571
+ clearTimeout(timer);
572
+ }
573
+ }
574
+ function createAttackResult(templateId, payloadId, request, response, outcome, latencyMs, error) {
575
+ return {
576
+ templateId,
577
+ payloadId,
578
+ outcome,
579
+ request,
580
+ response,
581
+ latencyMs,
582
+ timestamp: Date.now(),
583
+ error
584
+ };
585
+ }
586
+ function createFinding(input) {
587
+ return {
588
+ id: randomUUID(),
589
+ templateId: input.templateId,
590
+ title: input.title,
591
+ description: input.description,
592
+ severity: input.severity,
593
+ category: input.category,
594
+ status: "discovered",
595
+ evidence: {
596
+ request: input.request,
597
+ response: input.response,
598
+ analysis: input.analysis
599
+ },
600
+ timestamp: Date.now()
601
+ };
602
+ }
603
+ function summarizeResponse(response) {
604
+ const headers = Object.entries(response.headers).map(([key, value]) => `${key}: ${value}`).join("\n");
605
+ const bodyPreview = response.body.length > 800 ? `${response.body.slice(0, 800)}...` : response.body;
606
+ return `HTTP ${response.status}
607
+ ${headers}${headers ? "\n\n" : ""}${bodyPreview}`;
608
+ }
609
+ function discoverInterestingEndpoints(body) {
610
+ const matches = [...body.matchAll(/(?:href|action)=["']([^"']+)["']/gi)].map((match) => match[1]).filter((value) => value.startsWith("/"));
611
+ return [...new Set(matches)].slice(0, 20);
612
+ }
613
+ function detectWebFeatures(body, headers) {
614
+ const features = [];
615
+ const lower = body.toLowerCase();
616
+ if (lower.includes("<form"))
617
+ features.push("forms");
618
+ if (lower.includes("csrf"))
619
+ features.push("csrf");
620
+ if (lower.includes("graphql"))
621
+ features.push("graphql");
622
+ if (lower.includes("<script"))
623
+ features.push("javascript");
624
+ if (headers["set-cookie"])
625
+ features.push("session-cookies");
626
+ return features;
627
+ }
628
+
629
+ // packages/core/dist/recon/stack-fingerprint.js
630
+ var DEFAULT_MAX_CHUNKS = 12;
631
+ async function fingerprintWebStack(opts) {
632
+ const base = normalizeBase(opts.baseUrl);
633
+ const maxChunks = opts.maxChunks ?? DEFAULT_MAX_CHUNKS;
634
+ const rawSignals = [];
635
+ const libraries = /* @__PURE__ */ new Map();
636
+ let framework;
637
+ const root = await safeFetch(opts.fetchText, base);
638
+ const html = root?.body ?? "";
639
+ const headers = normalizeHeaders(root?.headers ?? {});
640
+ const headerFw = detectFrameworkFromHeaders(headers);
641
+ if (headerFw) {
642
+ framework = headerFw;
643
+ rawSignals.push(`header: ${headerFw.name}${headerFw.version ? ` v${headerFw.version}` : ""}`);
644
+ }
645
+ if (html) {
646
+ const htmlFw = detectFrameworkFromHtml(html);
647
+ if (htmlFw) {
648
+ rawSignals.push(`html: ${htmlFw.name}${htmlFw.version ? ` v${htmlFw.version}` : ""}`);
649
+ framework = mergeFramework(framework, htmlFw);
650
+ }
651
+ for (const lib of parseLibraryBanners(html)) {
652
+ addLibrary(libraries, lib);
653
+ rawSignals.push(`html banner: ${lib.name}${lib.version ? ` v${lib.version}` : ""}`);
654
+ }
655
+ }
656
+ const chunkUrls = enumerateJsChunkUrls(html, base).slice(0, maxChunks);
657
+ const chunkBodies = await Promise.all(chunkUrls.map(async (url) => ({ url, body: (await safeFetch(opts.fetchText, url))?.body ?? "" })));
658
+ for (const { url, body } of chunkBodies) {
659
+ if (!body)
660
+ continue;
661
+ const chunkFw = detectFrameworkFromBundle(body);
662
+ if (chunkFw) {
663
+ rawSignals.push(`bundle ${shortUrl(url)}: ${chunkFw.name}${chunkFw.version ? ` v${chunkFw.version}` : ""}`);
664
+ framework = mergeFramework(framework, chunkFw);
665
+ }
666
+ for (const lib of parseLibraryBanners(body)) {
667
+ addLibrary(libraries, lib);
668
+ rawSignals.push(`bundle ${shortUrl(url)} banner: ${lib.name}${lib.version ? ` v${lib.version}` : ""}`);
669
+ }
670
+ }
671
+ return {
672
+ framework,
673
+ libraries: [...libraries.values()],
674
+ rawSignals
675
+ };
676
+ }
677
+ var HEADER_FRAMEWORK_HINTS = [
678
+ { pattern: /Next\.js/i, name: "Next.js" },
679
+ { pattern: /Nuxt/i, name: "Nuxt" },
680
+ { pattern: /Express/i, name: "Express" },
681
+ { pattern: /SvelteKit/i, name: "SvelteKit" },
682
+ { pattern: /Remix/i, name: "Remix" },
683
+ { pattern: /Gatsby/i, name: "Gatsby" },
684
+ { pattern: /Astro/i, name: "Astro" },
685
+ { pattern: /Django/i, name: "Django" },
686
+ { pattern: /Laravel/i, name: "Laravel" },
687
+ { pattern: /Rails|Phusion Passenger/i, name: "Ruby on Rails" }
688
+ ];
689
+ function detectFrameworkFromHeaders(headers) {
690
+ const lower = normalizeHeaders(headers);
691
+ const haystack = [lower["x-powered-by"], lower["server"], lower["x-framework"]].filter((v) => typeof v === "string").join(" | ");
692
+ if (!haystack)
693
+ return void 0;
694
+ for (const hint of HEADER_FRAMEWORK_HINTS) {
695
+ if (hint.pattern.test(haystack)) {
696
+ const ver = haystack.match(new RegExp(`${escapeRegExp(hint.name)}[\\s/]v?(\\d+\\.\\d+(?:\\.\\d+)?)`, "i"));
697
+ return { name: hint.name, version: ver?.[1], source: "header" };
698
+ }
699
+ }
700
+ return void 0;
701
+ }
702
+ function detectFrameworkFromHtml(html) {
703
+ const gen = html.match(/<meta[^>]+name=["']generator["'][^>]+content=["']([^"']+)["']/i);
704
+ if (gen) {
705
+ const fw = parseGeneratorContent(gen[1]);
706
+ if (fw)
707
+ return fw;
708
+ }
709
+ if (/\/_next\/static\//.test(html) || /id=["']__NEXT_DATA__["']/.test(html)) {
710
+ const buildVer = html.match(/"buildId":"[^"]*"[\s\S]{0,200}?"nextExport"/);
711
+ void buildVer;
712
+ return { name: "Next.js", source: "html" };
713
+ }
714
+ if (/window\.__NUXT__/.test(html) || /\/_nuxt\//.test(html)) {
715
+ return { name: "Nuxt", source: "html" };
716
+ }
717
+ if (/id=["']___gatsby["']/.test(html)) {
718
+ return { name: "Gatsby", source: "html" };
719
+ }
720
+ if (/<div[^>]+id=["']root["'][^>]*>/.test(html) && /\.chunk\.js|static\/js\//.test(html)) {
721
+ return { name: "React", source: "html" };
722
+ }
723
+ return void 0;
724
+ }
725
+ function parseGeneratorContent(content) {
726
+ for (const hint of HEADER_FRAMEWORK_HINTS) {
727
+ if (hint.pattern.test(content)) {
728
+ const ver = content.match(/v?(\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?)/);
729
+ return { name: hint.name, version: ver?.[1], source: "meta_generator" };
730
+ }
731
+ }
732
+ return void 0;
733
+ }
734
+ function detectFrameworkFromBundle(body) {
735
+ const nextGlobal = body.match(/window\.next\s*=\s*\{[^}]*?version:\s*["']([^"']+)["']/);
736
+ if (nextGlobal) {
737
+ return { name: "Next.js", version: nextGlobal[1], source: "window_global" };
738
+ }
739
+ const reactVer = body.match(/reactVersion["':\s]+["']?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
740
+ if (reactVer) {
741
+ return { name: "React", version: reactVer[1], source: "js_bundle" };
742
+ }
743
+ if (/react|reactdom|__SECRET_INTERNALS/i.test(body)) {
744
+ const ver = body.match(/version\s*[:=]\s*["'](\d+\.\d+\.\d+(?:-(?:rc|canary|experimental|beta|alpha|next)-[0-9A-Za-z.-]+)?)["']/i);
745
+ if (ver)
746
+ return { name: "React", version: ver[1], source: "js_bundle" };
747
+ }
748
+ const vueVer = body.match(/Vue\.version\s*=\s*["'](\d+\.\d+\.\d+)["']/);
749
+ if (vueVer)
750
+ return { name: "Vue", version: vueVer[1], source: "js_bundle" };
751
+ return void 0;
752
+ }
753
+ function parseLibraryBanners(text) {
754
+ const out = /* @__PURE__ */ new Map();
755
+ const bannerRe = /(?:^|[\s/*])((?:@[a-z0-9][\w.-]*\/)?[a-z][a-z0-9][\w.-]*)\s+v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/gim;
756
+ for (const m of text.matchAll(bannerRe)) {
757
+ const name = m[1].toLowerCase();
758
+ if (isLikelyLibraryName(name)) {
759
+ addLibrary(out, { name, version: m[2], source: "js_bundle" });
760
+ }
761
+ }
762
+ const atRe = /(?:^|[\s"'(/])((?:@[a-z0-9][\w.-]*\/)?[a-z][a-z0-9-]*[a-z0-9])@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/gim;
763
+ for (const m of text.matchAll(atRe)) {
764
+ const name = m[1].toLowerCase();
765
+ if (isLikelyLibraryName(name)) {
766
+ addLibrary(out, { name, version: m[2], source: "js_bundle" });
767
+ }
768
+ }
769
+ return [...out.values()];
770
+ }
771
+ var LIBRARY_NAME_DENYLIST = /* @__PURE__ */ new Set([
772
+ "version",
773
+ "node",
774
+ "npm",
775
+ "http",
776
+ "https",
777
+ "true",
778
+ "false",
779
+ "null",
780
+ "function"
781
+ ]);
782
+ function isLikelyLibraryName(name) {
783
+ if (name.length < 2 || name.length > 64)
784
+ return false;
785
+ if (LIBRARY_NAME_DENYLIST.has(name))
786
+ return false;
787
+ if (/^\d/.test(name))
788
+ return false;
789
+ return true;
790
+ }
791
+ function enumerateJsChunkUrls(html, base) {
792
+ const urls = /* @__PURE__ */ new Set();
793
+ const scriptRe = /<script[^>]+src=["']([^"']+)["']/gi;
794
+ for (const m of html.matchAll(scriptRe)) {
795
+ pushIfJs(urls, m[1], base);
796
+ }
797
+ const linkRe = /<link[^>]+(?:href|src)=["']([^"']+\.(?:m?js)(?:\?[^"']*)?)["']/gi;
798
+ for (const m of html.matchAll(linkRe)) {
799
+ pushIfJs(urls, m[1], base);
800
+ }
801
+ return [...urls];
802
+ }
803
+ function pushIfJs(out, raw, base) {
804
+ const url = resolveUrl(raw, base);
805
+ if (!url)
806
+ return;
807
+ const path = url.split("?")[0];
808
+ if (/\.(m?js)$/i.test(path)) {
809
+ out.add(url);
810
+ }
811
+ }
812
+ function addLibrary(map, lib) {
813
+ const existing = map.get(lib.name);
814
+ if (!existing) {
815
+ map.set(lib.name, lib);
816
+ return;
817
+ }
818
+ if (!existing.version && lib.version) {
819
+ map.set(lib.name, lib);
820
+ }
821
+ }
822
+ function mergeFramework(current, next) {
823
+ if (!current)
824
+ return next;
825
+ if (current.name === next.name) {
826
+ return {
827
+ name: current.name,
828
+ version: current.version ?? next.version,
829
+ source: sourceRank(next.source) > sourceRank(current.source) ? next.source : current.source
830
+ };
831
+ }
832
+ if (sourceRank(next.source) > sourceRank(current.source)) {
833
+ return next;
834
+ }
835
+ return current;
836
+ }
837
+ function sourceRank(source) {
838
+ switch (source) {
839
+ case "window_global":
840
+ return 5;
841
+ case "js_bundle":
842
+ return 4;
843
+ case "header":
844
+ return 3;
845
+ case "meta_generator":
846
+ return 2;
847
+ case "html":
848
+ return 1;
849
+ default:
850
+ return 0;
851
+ }
852
+ }
853
+ function normalizeHeaders(headers) {
854
+ const out = {};
855
+ for (const [k, v] of Object.entries(headers)) {
856
+ out[k.toLowerCase()] = v;
857
+ }
858
+ return out;
859
+ }
860
+ function normalizeBase(target) {
861
+ return target.replace(/\/+$/, "");
862
+ }
863
+ function resolveUrl(raw, base) {
864
+ try {
865
+ return new URL(raw, base).toString();
866
+ } catch {
867
+ return void 0;
868
+ }
869
+ }
870
+ function shortUrl(url) {
871
+ try {
872
+ const u = new URL(url);
873
+ return u.pathname.split("/").slice(-1)[0] || u.pathname;
874
+ } catch {
875
+ return url;
876
+ }
877
+ }
878
+ function escapeRegExp(value) {
879
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
880
+ }
881
+ async function safeFetch(fetchText, url) {
882
+ try {
883
+ return await fetchText(url);
884
+ } catch {
885
+ return void 0;
886
+ }
887
+ }
888
+ function summarizeWebStackFingerprint(result) {
889
+ const lines = [];
890
+ if (result.framework) {
891
+ const { name, version, source } = result.framework;
892
+ lines.push(`Framework: ${name}${version ? ` v${version}` : ""} (via ${source})`);
893
+ } else {
894
+ lines.push("Framework: not identified");
895
+ }
896
+ if (result.libraries.length > 0) {
897
+ lines.push(`Libraries (${result.libraries.length}):`);
898
+ for (const lib of result.libraries) {
899
+ lines.push(` - ${lib.name}${lib.version ? `@${lib.version}` : ""} (${lib.source})`);
900
+ }
901
+ } else {
902
+ lines.push("Libraries: none detected");
903
+ }
904
+ return lines.join("\n");
905
+ }
906
+
907
+ export {
908
+ TokenBucket,
909
+ RateLimiter,
910
+ parseRetryAfter,
911
+ parseRateLimitFlag,
912
+ runWebDiscoveryProbe,
913
+ runBaselineWebChecks,
914
+ fingerprintWebStack,
915
+ enumerateJsChunkUrls,
916
+ summarizeWebStackFingerprint
917
+ };