@atlascrew/apparatus 0.9.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 (291) hide show
  1. package/bin/apparatus.mjs +2 -0
  2. package/certs/server.crt +17 -0
  3. package/certs/server.key +28 -0
  4. package/dist/ai/client.js +104 -0
  5. package/dist/ai/client.js.map +1 -0
  6. package/dist/ai/personas.js +104 -0
  7. package/dist/ai/personas.js.map +1 -0
  8. package/dist/ai/redteam.js +1404 -0
  9. package/dist/ai/redteam.js.map +1 -0
  10. package/dist/ai/report-store.js +309 -0
  11. package/dist/ai/report-store.js.map +1 -0
  12. package/dist/app.js +525 -0
  13. package/dist/app.js.map +1 -0
  14. package/dist/attack-sim.js +69 -0
  15. package/dist/attack-sim.js.map +1 -0
  16. package/dist/attacker-tracker.js +276 -0
  17. package/dist/attacker-tracker.js.map +1 -0
  18. package/dist/blackhole.js +95 -0
  19. package/dist/blackhole.js.map +1 -0
  20. package/dist/chaos.js +88 -0
  21. package/dist/chaos.js.map +1 -0
  22. package/dist/cluster.js +462 -0
  23. package/dist/cluster.js.map +1 -0
  24. package/dist/config.js +61 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/deception.js +205 -0
  27. package/dist/deception.js.map +1 -0
  28. package/dist/demo-mode.js +109 -0
  29. package/dist/demo-mode.js.map +1 -0
  30. package/dist/dist-dashboard/assets/index-BsMhEnGu.js +648 -0
  31. package/dist/dist-dashboard/assets/index-CNOkYC_Q.css +10 -0
  32. package/dist/dist-dashboard/assets/index-CW2grvPC.js +648 -0
  33. package/dist/dist-dashboard/assets/logo/apparatus-favicon.svg +15 -0
  34. package/dist/dist-dashboard/assets/logo/apparatus-icon-dark.svg +24 -0
  35. package/dist/dist-dashboard/assets/logo/apparatus-icon-light.svg +24 -0
  36. package/dist/dist-dashboard/assets/logo/apparatus-logo-512.png +0 -0
  37. package/dist/dist-dashboard/assets/logo/apparatus-logo-dark.svg +18 -0
  38. package/dist/dist-dashboard/assets/logo/apparatus-logo.svg +17 -0
  39. package/dist/dist-dashboard/assets/logo/apple-touch-icon.png +0 -0
  40. package/dist/dist-dashboard/assets/logo/favicon-192.png +0 -0
  41. package/dist/dist-dashboard/assets/logo/favicon-32.png +0 -0
  42. package/dist/dist-dashboard/assets/logo/favicon.ico +0 -0
  43. package/dist/dist-dashboard/assets/logo/icon-192.png +0 -0
  44. package/dist/dist-dashboard/assets/logo/icon-512.png +0 -0
  45. package/dist/dist-dashboard/assets/logo/icon-light-512.png +0 -0
  46. package/dist/dist-dashboard/assets/react-vendor-DpRMSntD.js +1 -0
  47. package/dist/dist-dashboard/assets/router-DSc5pRwN.js +59 -0
  48. package/dist/dist-dashboard/docs-index.json +1577 -0
  49. package/dist/dist-dashboard/index.html +21 -0
  50. package/dist/dlp.js +40 -0
  51. package/dist/dlp.js.map +1 -0
  52. package/dist/drills.js +770 -0
  53. package/dist/drills.js.map +1 -0
  54. package/dist/echoHandler.js +113 -0
  55. package/dist/echoHandler.js.map +1 -0
  56. package/dist/escape/index.js +225 -0
  57. package/dist/escape/index.js.map +1 -0
  58. package/dist/escape/methods/dns.js +74 -0
  59. package/dist/escape/methods/dns.js.map +1 -0
  60. package/dist/escape/methods/http.js +81 -0
  61. package/dist/escape/methods/http.js.map +1 -0
  62. package/dist/escape/methods/icmp.js +36 -0
  63. package/dist/escape/methods/icmp.js.map +1 -0
  64. package/dist/escape/methods/tcp.js +38 -0
  65. package/dist/escape/methods/tcp.js.map +1 -0
  66. package/dist/escape/methods/udp.js +27 -0
  67. package/dist/escape/methods/udp.js.map +1 -0
  68. package/dist/escape/methods/websocket.js +37 -0
  69. package/dist/escape/methods/websocket.js.map +1 -0
  70. package/dist/forensics.js +111 -0
  71. package/dist/forensics.js.map +1 -0
  72. package/dist/generator.js +67 -0
  73. package/dist/generator.js.map +1 -0
  74. package/dist/ghosting.js +414 -0
  75. package/dist/ghosting.js.map +1 -0
  76. package/dist/graphql.js +44 -0
  77. package/dist/graphql.js.map +1 -0
  78. package/dist/history.js +40 -0
  79. package/dist/history.js.map +1 -0
  80. package/dist/imposter/creds.js +16 -0
  81. package/dist/imposter/creds.js.map +1 -0
  82. package/dist/imposter/index.js +44 -0
  83. package/dist/imposter/index.js.map +1 -0
  84. package/dist/imposter/providers/aws.js +103 -0
  85. package/dist/imposter/providers/aws.js.map +1 -0
  86. package/dist/imposter/providers/gcp.js +26 -0
  87. package/dist/imposter/providers/gcp.js.map +1 -0
  88. package/dist/index.js +53 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/infra-debug.js +68 -0
  91. package/dist/infra-debug.js.map +1 -0
  92. package/dist/jwt-debug.js +272 -0
  93. package/dist/jwt-debug.js.map +1 -0
  94. package/dist/kv.js +22 -0
  95. package/dist/kv.js.map +1 -0
  96. package/dist/lib/generators.js +43 -0
  97. package/dist/lib/generators.js.map +1 -0
  98. package/dist/lib/json.js +26 -0
  99. package/dist/lib/json.js.map +1 -0
  100. package/dist/logger.js +9 -0
  101. package/dist/logger.js.map +1 -0
  102. package/dist/metrics.js +20 -0
  103. package/dist/metrics.js.map +1 -0
  104. package/dist/mtd.js +30 -0
  105. package/dist/mtd.js.map +1 -0
  106. package/dist/oidc.js +69 -0
  107. package/dist/oidc.js.map +1 -0
  108. package/dist/persistence/cluster-state.js +47 -0
  109. package/dist/persistence/cluster-state.js.map +1 -0
  110. package/dist/persistence/deception-history.js +65 -0
  111. package/dist/persistence/deception-history.js.map +1 -0
  112. package/dist/persistence/drill-runs.js +138 -0
  113. package/dist/persistence/drill-runs.js.map +1 -0
  114. package/dist/persistence/request-history.js +41 -0
  115. package/dist/persistence/request-history.js.map +1 -0
  116. package/dist/persistence/scenario-catalog.js +73 -0
  117. package/dist/persistence/scenario-catalog.js.map +1 -0
  118. package/dist/persistence/status.js +51 -0
  119. package/dist/persistence/status.js.map +1 -0
  120. package/dist/persistence/tarpit-state.js +47 -0
  121. package/dist/persistence/tarpit-state.js.map +1 -0
  122. package/dist/persistence/webhook-store.js +69 -0
  123. package/dist/persistence/webhook-store.js.map +1 -0
  124. package/dist/proxy.js +28 -0
  125. package/dist/proxy.js.map +1 -0
  126. package/dist/ratelimit.js +32 -0
  127. package/dist/ratelimit.js.map +1 -0
  128. package/dist/redteam.js +442 -0
  129. package/dist/redteam.js.map +1 -0
  130. package/dist/scenarios.js +229 -0
  131. package/dist/scenarios.js.map +1 -0
  132. package/dist/scripting.js +30 -0
  133. package/dist/scripting.js.map +1 -0
  134. package/dist/self-healing.js +42 -0
  135. package/dist/self-healing.js.map +1 -0
  136. package/dist/sentinel.js +50 -0
  137. package/dist/sentinel.js.map +1 -0
  138. package/dist/server-bad-ssl.js +47 -0
  139. package/dist/server-bad-ssl.js.map +1 -0
  140. package/dist/server-grpc.js +66 -0
  141. package/dist/server-grpc.js.map +1 -0
  142. package/dist/server-http1.js +5 -0
  143. package/dist/server-http1.js.map +1 -0
  144. package/dist/server-http2.js +27 -0
  145. package/dist/server-http2.js.map +1 -0
  146. package/dist/server-icap.js +46 -0
  147. package/dist/server-icap.js.map +1 -0
  148. package/dist/server-l4.js +30 -0
  149. package/dist/server-l4.js.map +1 -0
  150. package/dist/server-mqtt.js +29 -0
  151. package/dist/server-mqtt.js.map +1 -0
  152. package/dist/server-protocols.js +18 -0
  153. package/dist/server-protocols.js.map +1 -0
  154. package/dist/server-redis.js +112 -0
  155. package/dist/server-redis.js.map +1 -0
  156. package/dist/server-smtp.js +66 -0
  157. package/dist/server-smtp.js.map +1 -0
  158. package/dist/server-syslog.js +23 -0
  159. package/dist/server-syslog.js.map +1 -0
  160. package/dist/server-ws.js +18 -0
  161. package/dist/server-ws.js.map +1 -0
  162. package/dist/sidecar/chaos/engine.js +41 -0
  163. package/dist/sidecar/chaos/engine.js.map +1 -0
  164. package/dist/sidecar/index.js +98 -0
  165. package/dist/sidecar/index.js.map +1 -0
  166. package/dist/simulator/dependency-graph.js +102 -0
  167. package/dist/simulator/dependency-graph.js.map +1 -0
  168. package/dist/simulator/supply-chain.js +67 -0
  169. package/dist/simulator/supply-chain.js.map +1 -0
  170. package/dist/sink.js +24 -0
  171. package/dist/sink.js.map +1 -0
  172. package/dist/sse-broadcast.js +105 -0
  173. package/dist/sse-broadcast.js.map +1 -0
  174. package/dist/swagger.js +309 -0
  175. package/dist/swagger.js.map +1 -0
  176. package/dist/sysinfo.js +36 -0
  177. package/dist/sysinfo.js.map +1 -0
  178. package/dist/tarpit.js +126 -0
  179. package/dist/tarpit.js.map +1 -0
  180. package/dist/tool-executor.js +315 -0
  181. package/dist/tool-executor.js.map +1 -0
  182. package/dist/tui/api-client.js +341 -0
  183. package/dist/tui/api-client.js.map +1 -0
  184. package/dist/tui/core/action-handler.js +302 -0
  185. package/dist/tui/core/action-handler.js.map +1 -0
  186. package/dist/tui/core/index.js +18 -0
  187. package/dist/tui/core/index.js.map +1 -0
  188. package/dist/tui/core/keyboard.js +329 -0
  189. package/dist/tui/core/keyboard.js.map +1 -0
  190. package/dist/tui/core/modal.js +397 -0
  191. package/dist/tui/core/modal.js.map +1 -0
  192. package/dist/tui/core/screen-manager.js +262 -0
  193. package/dist/tui/core/screen-manager.js.map +1 -0
  194. package/dist/tui/core/store.js +254 -0
  195. package/dist/tui/core/store.js.map +1 -0
  196. package/dist/tui/core/widget.js +167 -0
  197. package/dist/tui/core/widget.js.map +1 -0
  198. package/dist/tui/dashboard.js +649 -0
  199. package/dist/tui/dashboard.js.map +1 -0
  200. package/dist/tui/index.js +118 -0
  201. package/dist/tui/index.js.map +1 -0
  202. package/dist/tui/modals/add-rule-modal.js +190 -0
  203. package/dist/tui/modals/add-rule-modal.js.map +1 -0
  204. package/dist/tui/modals/dlp-output-modal.js +102 -0
  205. package/dist/tui/modals/dlp-output-modal.js.map +1 -0
  206. package/dist/tui/modals/dns-form-modal.js +26 -0
  207. package/dist/tui/modals/dns-form-modal.js.map +1 -0
  208. package/dist/tui/modals/ghost-config-modal.js +35 -0
  209. package/dist/tui/modals/ghost-config-modal.js.map +1 -0
  210. package/dist/tui/modals/har-results-modal.js +41 -0
  211. package/dist/tui/modals/har-results-modal.js.map +1 -0
  212. package/dist/tui/modals/index.js +15 -0
  213. package/dist/tui/modals/index.js.map +1 -0
  214. package/dist/tui/modals/jwt-decode-modal.js +45 -0
  215. package/dist/tui/modals/jwt-decode-modal.js.map +1 -0
  216. package/dist/tui/modals/jwt-mint-modal.js +70 -0
  217. package/dist/tui/modals/jwt-mint-modal.js.map +1 -0
  218. package/dist/tui/modals/ping-form-modal.js +19 -0
  219. package/dist/tui/modals/ping-form-modal.js.map +1 -0
  220. package/dist/tui/modals/redteam-results-modal.js +43 -0
  221. package/dist/tui/modals/redteam-results-modal.js.map +1 -0
  222. package/dist/tui/modals/scan-form-modal.js +26 -0
  223. package/dist/tui/modals/scan-form-modal.js.map +1 -0
  224. package/dist/tui/screens/defense-screen.js +281 -0
  225. package/dist/tui/screens/defense-screen.js.map +1 -0
  226. package/dist/tui/screens/forensics-screen.js +81 -0
  227. package/dist/tui/screens/forensics-screen.js.map +1 -0
  228. package/dist/tui/screens/index.js +140 -0
  229. package/dist/tui/screens/index.js.map +1 -0
  230. package/dist/tui/screens/system-screen.js +81 -0
  231. package/dist/tui/screens/system-screen.js.map +1 -0
  232. package/dist/tui/screens/testing-screen.js +429 -0
  233. package/dist/tui/screens/testing-screen.js.map +1 -0
  234. package/dist/tui/screens/traffic-screen.js +76 -0
  235. package/dist/tui/screens/traffic-screen.js.map +1 -0
  236. package/dist/tui/sse-client.js +130 -0
  237. package/dist/tui/sse-client.js.map +1 -0
  238. package/dist/tui/state/metrics-buffer.js +195 -0
  239. package/dist/tui/state/metrics-buffer.js.map +1 -0
  240. package/dist/tui/state/metrics-buffer.test.js +102 -0
  241. package/dist/tui/state/metrics-buffer.test.js.map +1 -0
  242. package/dist/tui/theme.js +136 -0
  243. package/dist/tui/theme.js.map +1 -0
  244. package/dist/tui/types.js +6 -0
  245. package/dist/tui/types.js.map +1 -0
  246. package/dist/tui/widgets/chaos-widget.js +152 -0
  247. package/dist/tui/widgets/chaos-widget.js.map +1 -0
  248. package/dist/tui/widgets/cluster-widget.js +156 -0
  249. package/dist/tui/widgets/cluster-widget.js.map +1 -0
  250. package/dist/tui/widgets/dlp-widget.js +161 -0
  251. package/dist/tui/widgets/dlp-widget.js.map +1 -0
  252. package/dist/tui/widgets/ghost-widget.js +169 -0
  253. package/dist/tui/widgets/ghost-widget.js.map +1 -0
  254. package/dist/tui/widgets/har-widget.js +173 -0
  255. package/dist/tui/widgets/har-widget.js.map +1 -0
  256. package/dist/tui/widgets/index.js +122 -0
  257. package/dist/tui/widgets/index.js.map +1 -0
  258. package/dist/tui/widgets/jwt-widget.js +177 -0
  259. package/dist/tui/widgets/jwt-widget.js.map +1 -0
  260. package/dist/tui/widgets/kv-widget.js +261 -0
  261. package/dist/tui/widgets/kv-widget.js.map +1 -0
  262. package/dist/tui/widgets/mtd-widget.js +181 -0
  263. package/dist/tui/widgets/mtd-widget.js.map +1 -0
  264. package/dist/tui/widgets/netdiag-widget.js +155 -0
  265. package/dist/tui/widgets/netdiag-widget.js.map +1 -0
  266. package/dist/tui/widgets/oidc-widget.js +162 -0
  267. package/dist/tui/widgets/oidc-widget.js.map +1 -0
  268. package/dist/tui/widgets/pcap-widget.js +239 -0
  269. package/dist/tui/widgets/pcap-widget.js.map +1 -0
  270. package/dist/tui/widgets/redteam-widget.js +155 -0
  271. package/dist/tui/widgets/redteam-widget.js.map +1 -0
  272. package/dist/tui/widgets/rps-gauge-widget.js +124 -0
  273. package/dist/tui/widgets/rps-gauge-widget.js.map +1 -0
  274. package/dist/tui/widgets/sentinel-widget.js +171 -0
  275. package/dist/tui/widgets/sentinel-widget.js.map +1 -0
  276. package/dist/tui/widgets/sparklines-widget.js +127 -0
  277. package/dist/tui/widgets/sparklines-widget.js.map +1 -0
  278. package/dist/tui/widgets/sysinfo-widget.js +197 -0
  279. package/dist/tui/widgets/sysinfo-widget.js.map +1 -0
  280. package/dist/tui/widgets/traffic-chart-widget.js +170 -0
  281. package/dist/tui/widgets/traffic-chart-widget.js.map +1 -0
  282. package/dist/tui/widgets/webhook-widget.js +259 -0
  283. package/dist/tui/widgets/webhook-widget.js.map +1 -0
  284. package/dist/utils/ip.js +18 -0
  285. package/dist/utils/ip.js.map +1 -0
  286. package/dist/victim/index.js +71 -0
  287. package/dist/victim/index.js.map +1 -0
  288. package/dist/webhook.js +88 -0
  289. package/dist/webhook.js.map +1 -0
  290. package/package.json +90 -0
  291. package/proto/echo.proto +19 -0
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { logger } from "../logger.js";
5
+ function isPersistedEntry(value) {
6
+ if (!value || typeof value !== "object" || Array.isArray(value))
7
+ return false;
8
+ const candidate = value;
9
+ return typeof candidate.ip === "string" && Number.isFinite(candidate.trappedAt);
10
+ }
11
+ export function loadTarpitStateSync(statePath) {
12
+ if (!statePath) {
13
+ return [];
14
+ }
15
+ try {
16
+ const raw = readFileSync(statePath, "utf8");
17
+ const parsed = JSON.parse(raw);
18
+ if (Array.isArray(parsed)) {
19
+ return parsed.filter(isPersistedEntry);
20
+ }
21
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.entries)) {
22
+ return parsed.entries.filter(isPersistedEntry);
23
+ }
24
+ return [];
25
+ }
26
+ catch (error) {
27
+ if (error?.code !== "ENOENT") {
28
+ logger.warn({ statePath, error: error?.message }, "Tarpit state load failed; continuing with in-memory store");
29
+ }
30
+ return [];
31
+ }
32
+ }
33
+ export async function writeTarpitState(statePath, entries) {
34
+ if (!statePath) {
35
+ return true;
36
+ }
37
+ try {
38
+ await mkdir(path.dirname(statePath), { recursive: true });
39
+ await writeFile(statePath, `${JSON.stringify({ entries }, null, 2)}\n`, "utf8");
40
+ return true;
41
+ }
42
+ catch (error) {
43
+ logger.warn({ statePath, error: error?.message }, "Tarpit state persist failed; keeping data in memory");
44
+ return false;
45
+ }
46
+ }
47
+ //# sourceMappingURL=tarpit-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tarpit-state.js","sourceRoot":"","sources":["../../src/persistence/tarpit-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAOtC,SAAS,gBAAgB,CAAC,KAAc;IACpC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,MAAM,SAAS,GAAG,KAA6B,CAAC;IAChD,OAAO,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,SAAiB;IACjD,IAAI,CAAC,SAAS,EAAE,CAAC;QACb,OAAO,EAAE,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAE1C,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACxB,OAAO,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAC3C,CAAC;QAED,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAE,MAAkC,CAAC,OAAO,CAAC,EAAE,CAAC;YACrG,OAAQ,MAAiC,CAAC,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAC/E,CAAC;QAED,OAAO,EAAE,CAAC;IACd,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,IAAI,KAAK,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,2DAA2D,CAAC,CAAC;QACnH,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,SAAiB,EAAE,OAA+B;IACrF,IAAI,CAAC,SAAS,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,IAAI,CAAC;QACD,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,MAAM,SAAS,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAChF,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,qDAAqD,CAAC,CAAC;QACzG,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC"}
@@ -0,0 +1,69 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { logger } from "../logger.js";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function isPersistedWebhook(value) {
8
+ if (!isRecord(value))
9
+ return false;
10
+ if (typeof value.id !== "string")
11
+ return false;
12
+ if (typeof value.timestamp !== "string")
13
+ return false;
14
+ if (typeof value.method !== "string")
15
+ return false;
16
+ if (!isRecord(value.headers))
17
+ return false;
18
+ if (!isRecord(value.query))
19
+ return false;
20
+ if (typeof value.ip !== "string")
21
+ return false;
22
+ return true;
23
+ }
24
+ function parseWebhookStore(payload) {
25
+ const hooksObject = isRecord(payload) && isRecord(payload.hooks)
26
+ ? payload.hooks
27
+ : isRecord(payload)
28
+ ? payload
29
+ : {};
30
+ const parsed = {};
31
+ for (const [hookId, entries] of Object.entries(hooksObject)) {
32
+ if (!Array.isArray(entries))
33
+ continue;
34
+ parsed[hookId] = entries.filter(isPersistedWebhook);
35
+ }
36
+ return parsed;
37
+ }
38
+ export async function loadWebhookStore(storePath) {
39
+ if (!storePath) {
40
+ return {};
41
+ }
42
+ try {
43
+ const raw = await readFile(storePath, "utf8");
44
+ return parseWebhookStore(JSON.parse(raw));
45
+ }
46
+ catch (error) {
47
+ if (error?.code === "ENOENT") {
48
+ return {};
49
+ }
50
+ logger.warn({ storePath, error: error?.message }, "Webhook store load failed; continuing with in-memory store");
51
+ return {};
52
+ }
53
+ }
54
+ export async function writeWebhookStore(storePath, store) {
55
+ if (!storePath) {
56
+ return true;
57
+ }
58
+ try {
59
+ await mkdir(path.dirname(storePath), { recursive: true });
60
+ const payload = JSON.stringify({ hooks: store }, null, 2);
61
+ await writeFile(storePath, `${payload}\n`, "utf8");
62
+ return true;
63
+ }
64
+ catch (error) {
65
+ logger.warn({ storePath, error: error?.message }, "Webhook store persist failed; keeping data in memory");
66
+ return false;
67
+ }
68
+ }
69
+ //# sourceMappingURL=webhook-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-store.js","sourceRoot":"","sources":["../../src/persistence/webhook-store.ts"],"names":[],"mappings":"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"}
package/dist/proxy.js ADDED
@@ -0,0 +1,28 @@
1
+ import axios from "axios";
2
+ import { logger } from "./logger.js";
3
+ export async function proxyHandler(req, res) {
4
+ const targetUrl = req.query.url;
5
+ if (!targetUrl) {
6
+ return res.status(400).json({ error: "Missing 'url' query parameter" });
7
+ }
8
+ try {
9
+ const response = await axios({
10
+ method: req.method,
11
+ url: targetUrl,
12
+ headers: req.headers,
13
+ data: req.body,
14
+ validateStatus: () => true, // Accept all status codes
15
+ timeout: 5000 // 5s timeout to prevent hanging
16
+ });
17
+ res.status(response.status).set(response.headers).send(response.data);
18
+ }
19
+ catch (error) {
20
+ logger.error({ err: error.message, url: targetUrl }, "Proxy request failed");
21
+ res.status(502).json({
22
+ error: "Proxy request failed",
23
+ message: error.message,
24
+ code: error.code
25
+ });
26
+ }
27
+ }
28
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAY,EAAE,GAAa;IAC1D,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,GAAa,CAAC;IAE1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACb,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,IAAI,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC;YACzB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,GAAG,EAAE,SAAS;YACd,OAAO,EAAE,GAAG,CAAC,OAAc;YAC3B,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,0BAA0B;YACtD,OAAO,EAAE,IAAI,CAAC,gCAAgC;SACjD,CAAC,CAAC;QAEH,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC1E,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,sBAAsB,CAAC,CAAC;QAC7E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACjB,KAAK,EAAE,sBAAsB;YAC7B,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,IAAI,EAAE,KAAK,CAAC,IAAI;SACnB,CAAC,CAAC;IACP,CAAC;AACL,CAAC"}
@@ -0,0 +1,32 @@
1
+ const WINDOW_MS = 60 * 1000; // 1 minute
2
+ const MAX_REQUESTS = 10;
3
+ const ipHits = new Map();
4
+ export function rateLimitHandler(req, res) {
5
+ const ip = req.ip || "unknown";
6
+ const now = Date.now();
7
+ let record = ipHits.get(ip);
8
+ // Clean up expired or create new
9
+ if (!record || now > record.resetAt) {
10
+ record = { count: 0, resetAt: now + WINDOW_MS };
11
+ ipHits.set(ip, record);
12
+ }
13
+ record.count++;
14
+ const remaining = Math.max(0, MAX_REQUESTS - record.count);
15
+ const resetInSeconds = Math.ceil((record.resetAt - now) / 1000);
16
+ res.setHeader("X-RateLimit-Limit", MAX_REQUESTS);
17
+ res.setHeader("X-RateLimit-Remaining", remaining);
18
+ res.setHeader("X-RateLimit-Reset", resetInSeconds);
19
+ if (record.count > MAX_REQUESTS) {
20
+ return res.status(429).json({
21
+ error: "Too Many Requests",
22
+ message: `Rate limit of ${MAX_REQUESTS} requests per minute exceeded. Try again in ${resetInSeconds} seconds.`
23
+ });
24
+ }
25
+ res.json({
26
+ message: "Request accepted",
27
+ limit: MAX_REQUESTS,
28
+ remaining,
29
+ resetInSeconds
30
+ });
31
+ }
32
+ //# sourceMappingURL=ratelimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ratelimit.js","sourceRoot":"","sources":["../src/ratelimit.ts"],"names":[],"mappings":"AAEA,MAAM,SAAS,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,WAAW;AACxC,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,MAAM,GAAG,IAAI,GAAG,EAA8C,CAAC;AAErE,MAAM,UAAU,gBAAgB,CAAC,GAAY,EAAE,GAAa;IACxD,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEvB,IAAI,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAE5B,iCAAiC;IACjC,IAAI,CAAC,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QAClC,MAAM,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,SAAS,EAAE,CAAC;QAChD,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,KAAK,EAAE,CAAC;IAEf,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3D,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAEhE,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,YAAY,CAAC,CAAC;IACjD,GAAG,CAAC,SAAS,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;IAClD,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC;IAEnD,IAAI,MAAM,CAAC,KAAK,GAAG,YAAY,EAAE,CAAC;QAC9B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACxB,KAAK,EAAE,mBAAmB;YAC1B,OAAO,EAAE,iBAAiB,YAAY,+CAA+C,cAAc,WAAW;SACjH,CAAC,CAAC;IACP,CAAC;IAED,GAAG,CAAC,IAAI,CAAC;QACL,OAAO,EAAE,kBAAkB;QAC3B,KAAK,EAAE,YAAY;QACnB,SAAS;QACT,cAAc;KACjB,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,442 @@
1
+ import { request } from "undici";
2
+ const PAYLOADS = {
3
+ xss: [
4
+ "<script>alert(1)</script>",
5
+ "javascript:alert(1)",
6
+ "<img src=x onerror=alert(1)>",
7
+ "<svg onload=alert(1)>",
8
+ "<body onload=alert('XSS')>",
9
+ "<iframe src=javascript:alert('XSS')>",
10
+ "<noscript><p title=\"</noscript><img src=x onerror=alert(1)>\">",
11
+ ],
12
+ sqli: [
13
+ "' OR '1'='1",
14
+ "UNION SELECT 1,2,3--",
15
+ "admin' --",
16
+ "1' OR '1'='1",
17
+ "1 UNION SELECT NULL,username,password FROM users--",
18
+ "1' AND SLEEP(5)--",
19
+ "1;DROP TABLE users--",
20
+ "1' AND 1=CONVERT(int,@@version)--",
21
+ "'/**/OR/**/'1'='1",
22
+ "' oR '1'='1",
23
+ "admin'--",
24
+ ],
25
+ pathtraversal: [
26
+ "../../etc/passwd",
27
+ "..\\windows\\win.ini",
28
+ "%2e%2e%2f%2e%2e%2fetc%2fpasswd",
29
+ "....//....//....//etc/passwd",
30
+ "../../etc/passwd%00.txt",
31
+ ],
32
+ cmdinjection: [
33
+ "; cat /etc/passwd",
34
+ "| whoami",
35
+ "$(whoami)",
36
+ "; whoami",
37
+ "; ls -la",
38
+ "| id",
39
+ "`whoami`",
40
+ "$(cat /etc/passwd)",
41
+ "; sleep 5",
42
+ "& whoami",
43
+ "&& ls",
44
+ "|| cat /etc/passwd",
45
+ ";WhOaMi",
46
+ ";w''hoami",
47
+ ";${IFS}whoami",
48
+ ],
49
+ nosqli: [
50
+ '{"$$gt": ""}',
51
+ '{"$$ne": null}',
52
+ '{"username":{"$$ne":null},"password":{"$$ne":null}}'
53
+ ],
54
+ business_logic: [
55
+ '{"codes":["SAVE10","SAVE20","SAVE30"]}',
56
+ '{"item_id":123,"quantity":-5}',
57
+ '{"amount":999999}',
58
+ '{"from":"victim","to":"attacker","points":999999}'
59
+ ],
60
+ api_abuse: [
61
+ '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><user><name>&xxe;</name></user>',
62
+ "http://169.254.169.254/latest/meta-data/",
63
+ '{"username":"test","password":"test","isAdmin":true}',
64
+ "query IntrospectionQuery { __schema { types { name } } }"
65
+ ]
66
+ };
67
+ const FUZZER_BLOCKED_STATUS_CODES = new Set([403, 406, 429, 500, 502, 503]);
68
+ const VALIDATE_BLOCKED_STATUS_CODES = new Set([403, 406, 500]); // Preserve legacy validate endpoint behavior.
69
+ const SUPPORTED_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
70
+ const BODY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
71
+ const MAX_BODY_PREVIEW_CHARS = 8_000;
72
+ const MAX_RESPONSE_CAPTURE_BYTES = 64 * 1024;
73
+ const MAX_OUTBOUND_BODY_BYTES = 256 * 1024;
74
+ const DEFAULT_TIMEOUT_MS = 5_000;
75
+ const MIN_TIMEOUT_MS = 250;
76
+ const MAX_TIMEOUT_MS = 20_000;
77
+ const DEFAULT_ALLOWED_FUZZER_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
78
+ function isPlainObject(value) {
79
+ return typeof value === "object" && value !== null && !Array.isArray(value);
80
+ }
81
+ function isStringRecord(value) {
82
+ if (!isPlainObject(value))
83
+ return false;
84
+ return Object.values(value).every((entry) => typeof entry === "string");
85
+ }
86
+ function isPrimitiveRecord(value) {
87
+ if (!isPlainObject(value))
88
+ return false;
89
+ return Object.values(value).every((entry) => {
90
+ return typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean";
91
+ });
92
+ }
93
+ function normalizeMethod(method) {
94
+ if (typeof method !== "string" || method.trim().length === 0) {
95
+ return "GET";
96
+ }
97
+ const normalized = method.trim().toUpperCase();
98
+ return SUPPORTED_METHODS.has(normalized) ? normalized : null;
99
+ }
100
+ function normalizeTimeout(timeoutMs) {
101
+ if (typeof timeoutMs !== "number" || Number.isNaN(timeoutMs)) {
102
+ return DEFAULT_TIMEOUT_MS;
103
+ }
104
+ return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.floor(timeoutMs)));
105
+ }
106
+ function classifyFuzzerBlockedStatus(status) {
107
+ return FUZZER_BLOCKED_STATUS_CODES.has(status);
108
+ }
109
+ function classifyValidateBlockedStatus(status) {
110
+ return VALIDATE_BLOCKED_STATUS_CODES.has(status);
111
+ }
112
+ function buildPreview(bodyText) {
113
+ if (bodyText.length <= MAX_BODY_PREVIEW_CHARS)
114
+ return bodyText;
115
+ return `${bodyText.slice(0, MAX_BODY_PREVIEW_CHARS)}\n...[truncated]`;
116
+ }
117
+ function normalizeResponseHeaders(headers) {
118
+ const normalized = {};
119
+ for (const [key, value] of Object.entries(headers)) {
120
+ if (value === undefined)
121
+ continue;
122
+ normalized[key] = Array.isArray(value) ? value.join(", ") : value;
123
+ }
124
+ return normalized;
125
+ }
126
+ function normalizeHost(hostname) {
127
+ return hostname.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
128
+ }
129
+ function isLoopbackIpv4Host(hostname) {
130
+ const match = hostname.match(/^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
131
+ if (!match)
132
+ return false;
133
+ return match.slice(1).every((octet) => {
134
+ const value = Number(octet);
135
+ return Number.isInteger(value) && value >= 0 && value <= 255;
136
+ });
137
+ }
138
+ function configuredAllowedFuzzerHosts() {
139
+ const allowed = new Set(DEFAULT_ALLOWED_FUZZER_HOSTS);
140
+ const raw = process.env.APPARATUS_FUZZER_ALLOWED_TARGETS;
141
+ if (!raw)
142
+ return allowed;
143
+ for (const candidate of raw.split(",")) {
144
+ const normalized = normalizeHost(candidate);
145
+ if (normalized.length > 0) {
146
+ allowed.add(normalized);
147
+ }
148
+ }
149
+ return allowed;
150
+ }
151
+ function isAllowedFuzzerTargetHost(hostname) {
152
+ const normalized = normalizeHost(hostname);
153
+ if (normalized.startsWith("::ffff:"))
154
+ return false;
155
+ if (normalized.includes(":") && normalized !== "::1")
156
+ return false;
157
+ if (isLoopbackIpv4Host(normalized))
158
+ return true;
159
+ return configuredAllowedFuzzerHosts().has(normalized);
160
+ }
161
+ function normalizePath(pathValue) {
162
+ const trimmed = pathValue.trim();
163
+ if (trimmed.length === 0)
164
+ return "/echo";
165
+ if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("//")) {
166
+ return null;
167
+ }
168
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
169
+ }
170
+ function buildDefaultTarget(req) {
171
+ const port = req.socket.localPort;
172
+ if (typeof port === "number" && port > 0) {
173
+ return `${req.protocol}://127.0.0.1:${port}`;
174
+ }
175
+ return `${req.protocol}://127.0.0.1`;
176
+ }
177
+ function hasContentTypeHeader(headers) {
178
+ return Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
179
+ }
180
+ async function readResponsePreview(body) {
181
+ if (!body) {
182
+ return { bodyPreview: "", bodyBytes: 0, truncated: false };
183
+ }
184
+ const chunks = [];
185
+ let totalBytes = 0;
186
+ let truncated = false;
187
+ for await (const chunk of body) {
188
+ const buffer = Buffer.from(chunk);
189
+ const remaining = MAX_RESPONSE_CAPTURE_BYTES - Math.min(totalBytes, MAX_RESPONSE_CAPTURE_BYTES);
190
+ if (remaining > 0) {
191
+ chunks.push(buffer.length <= remaining ? buffer : buffer.subarray(0, remaining));
192
+ }
193
+ totalBytes += buffer.length;
194
+ if (totalBytes >= MAX_RESPONSE_CAPTURE_BYTES) {
195
+ truncated = true;
196
+ break;
197
+ }
198
+ }
199
+ const previewText = Buffer.concat(chunks).toString("utf8");
200
+ const bodyPreview = buildPreview(previewText);
201
+ const readableBody = body;
202
+ if (truncated && typeof readableBody?.destroy === "function") {
203
+ readableBody.destroy();
204
+ }
205
+ return {
206
+ bodyPreview,
207
+ bodyBytes: totalBytes,
208
+ truncated,
209
+ };
210
+ }
211
+ function normalizeUpstreamError(error) {
212
+ const rawCode = isPlainObject(error) && typeof error.code === "string" ? error.code : "";
213
+ if (rawCode === "UND_ERR_HEADERS_TIMEOUT" || rawCode === "UND_ERR_BODY_TIMEOUT") {
214
+ return { code: "upstream_timeout", message: "Upstream request timed out." };
215
+ }
216
+ if (rawCode === "ENOTFOUND") {
217
+ return { code: "dns_resolution_failed", message: "Target hostname could not be resolved." };
218
+ }
219
+ if (rawCode === "ECONNREFUSED") {
220
+ return { code: "connection_refused", message: "Target refused the connection." };
221
+ }
222
+ return { code: "upstream_request_failed", message: "Upstream request failed." };
223
+ }
224
+ export async function redTeamFuzzerRunHandler(req, res) {
225
+ if (!isPlainObject(req.body)) {
226
+ return res.status(400).json({ error: "Invalid payload. Expected JSON object body." });
227
+ }
228
+ const payload = req.body;
229
+ const rawTarget = payload.target;
230
+ if (rawTarget !== undefined && (typeof rawTarget !== "string" || rawTarget.trim().length === 0)) {
231
+ return res.status(400).json({ error: "target must be a non-empty string when provided." });
232
+ }
233
+ const target = rawTarget?.trim() || buildDefaultTarget(req);
234
+ let targetUrl;
235
+ try {
236
+ targetUrl = new URL(target);
237
+ }
238
+ catch {
239
+ return res.status(400).json({ error: "target must be a valid URL." });
240
+ }
241
+ if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") {
242
+ return res.status(400).json({ error: "target protocol must be http or https." });
243
+ }
244
+ if (!isAllowedFuzzerTargetHost(targetUrl.hostname)) {
245
+ return res.status(400).json({
246
+ error: "target hostname is not allowed. Use localhost/127.0.0.1 or configure APPARATUS_FUZZER_ALLOWED_TARGETS.",
247
+ });
248
+ }
249
+ if (payload.path !== undefined && typeof payload.path !== "string") {
250
+ return res.status(400).json({ error: "path must be a string when provided." });
251
+ }
252
+ const path = normalizePath(payload.path ?? "/echo");
253
+ if (!path) {
254
+ return res.status(400).json({ error: "path must be a relative path and cannot include a scheme or host." });
255
+ }
256
+ const method = normalizeMethod(payload.method);
257
+ if (!method) {
258
+ return res.status(400).json({ error: `Unsupported method. Supported: ${Array.from(SUPPORTED_METHODS).join(", ")}` });
259
+ }
260
+ if (payload.headers !== undefined && !isStringRecord(payload.headers)) {
261
+ return res.status(400).json({ error: "headers must be an object of string values." });
262
+ }
263
+ if (payload.query !== undefined && !isPrimitiveRecord(payload.query)) {
264
+ return res.status(400).json({ error: "query must be an object with string/number/boolean values." });
265
+ }
266
+ const timeoutMs = normalizeTimeout(payload.timeoutMs);
267
+ const headers = { ...(payload.headers ?? {}) };
268
+ const runUrl = new URL(path, targetUrl);
269
+ if (payload.query) {
270
+ for (const [key, value] of Object.entries(payload.query)) {
271
+ runUrl.searchParams.set(key, String(value));
272
+ }
273
+ }
274
+ let outboundBody;
275
+ if (BODY_METHODS.has(method) && payload.body !== undefined) {
276
+ if (typeof payload.body === "string") {
277
+ outboundBody = payload.body;
278
+ }
279
+ else {
280
+ outboundBody = JSON.stringify(payload.body);
281
+ if (!hasContentTypeHeader(headers)) {
282
+ headers["Content-Type"] = "application/json";
283
+ }
284
+ }
285
+ }
286
+ if (outboundBody && Buffer.byteLength(outboundBody, "utf8") > MAX_OUTBOUND_BODY_BYTES) {
287
+ return res.status(400).json({ error: `body exceeds maximum size of ${MAX_OUTBOUND_BODY_BYTES} bytes.` });
288
+ }
289
+ const startedAt = Date.now();
290
+ try {
291
+ const upstream = await request(runUrl.toString(), {
292
+ method,
293
+ headers,
294
+ body: outboundBody,
295
+ headersTimeout: timeoutMs,
296
+ bodyTimeout: timeoutMs,
297
+ });
298
+ let preview = {
299
+ bodyPreview: "",
300
+ bodyBytes: 0,
301
+ truncated: false,
302
+ };
303
+ try {
304
+ preview = await readResponsePreview(upstream.body);
305
+ }
306
+ catch {
307
+ // Preserve upstream status/headers even if preview capture fails.
308
+ preview = {
309
+ bodyPreview: "",
310
+ bodyBytes: 0,
311
+ truncated: false,
312
+ };
313
+ }
314
+ const durationMs = Date.now() - startedAt;
315
+ const status = upstream.statusCode;
316
+ return res.json({
317
+ request: {
318
+ method,
319
+ url: runUrl.toString(),
320
+ timeoutMs,
321
+ hasBody: outboundBody !== undefined,
322
+ },
323
+ response: {
324
+ status,
325
+ blocked: classifyFuzzerBlockedStatus(status),
326
+ durationMs,
327
+ headers: normalizeResponseHeaders(upstream.headers),
328
+ // When truncated, this is bytes observed before capture stopped (lower bound of full body size).
329
+ bodyBytes: preview.bodyBytes,
330
+ bodyPreview: preview.bodyPreview,
331
+ bodyTruncated: preview.truncated,
332
+ },
333
+ });
334
+ }
335
+ catch (error) {
336
+ const durationMs = Date.now() - startedAt;
337
+ const normalizedError = normalizeUpstreamError(error);
338
+ return res.json({
339
+ request: {
340
+ method,
341
+ url: runUrl.toString(),
342
+ timeoutMs,
343
+ hasBody: outboundBody !== undefined,
344
+ },
345
+ response: {
346
+ status: null,
347
+ blocked: true,
348
+ durationMs,
349
+ error: normalizedError.message,
350
+ errorCode: normalizedError.code,
351
+ headers: {},
352
+ bodyBytes: 0,
353
+ bodyPreview: "",
354
+ bodyTruncated: false,
355
+ },
356
+ });
357
+ }
358
+ }
359
+ export async function redTeamValidateHandler(req, res) {
360
+ const queryTarget = req.query.target;
361
+ if (queryTarget !== undefined && typeof queryTarget !== "string") {
362
+ return res.status(400).json({ error: "target query parameter must be a string when provided." });
363
+ }
364
+ const targetBase = queryTarget?.trim() || buildDefaultTarget(req);
365
+ let targetBaseUrl;
366
+ try {
367
+ targetBaseUrl = new URL(targetBase);
368
+ }
369
+ catch {
370
+ return res.status(400).json({ error: "target must be a valid URL." });
371
+ }
372
+ if (targetBaseUrl.protocol !== "http:" && targetBaseUrl.protocol !== "https:") {
373
+ return res.status(400).json({ error: "target protocol must be http or https." });
374
+ }
375
+ if (!isAllowedFuzzerTargetHost(targetBaseUrl.hostname)) {
376
+ return res.status(400).json({
377
+ error: "target hostname is not allowed. Use localhost/127.0.0.1 or configure APPARATUS_FUZZER_ALLOWED_TARGETS.",
378
+ });
379
+ }
380
+ const queryPath = req.query.path;
381
+ if (queryPath !== undefined && typeof queryPath !== "string") {
382
+ return res.status(400).json({ error: "path query parameter must be a string when provided." });
383
+ }
384
+ const targetPath = normalizePath(queryPath ?? "/echo");
385
+ if (!targetPath) {
386
+ return res.status(400).json({ error: "path must be a relative path and cannot include a scheme or host." });
387
+ }
388
+ const method = normalizeMethod(req.query.method);
389
+ if (!method) {
390
+ return res.status(400).json({ error: `Unsupported method. Supported: ${Array.from(SUPPORTED_METHODS).join(", ")}` });
391
+ }
392
+ const results = [];
393
+ // Helper to test a payload
394
+ const testPayload = async (category, payload) => {
395
+ const url = new URL(targetPath, targetBaseUrl);
396
+ // Inject into Query Param "q"
397
+ url.searchParams.set("q", payload);
398
+ try {
399
+ const start = Date.now();
400
+ const { statusCode } = await request(url.toString(), {
401
+ method,
402
+ // Also inject into headers for good measure
403
+ headers: {
404
+ "X-Payload": payload,
405
+ "User-Agent": `RedTeam/1.0 (${category})`
406
+ }
407
+ });
408
+ const duration = Date.now() - start;
409
+ return {
410
+ category,
411
+ payload,
412
+ status: statusCode,
413
+ blocked: classifyValidateBlockedStatus(statusCode), // Preserve legacy validate semantics.
414
+ duration
415
+ };
416
+ }
417
+ catch (e) {
418
+ return {
419
+ category,
420
+ payload,
421
+ error: e.message,
422
+ blocked: true // Connection reset/timeout usually counts as "blocked" by network/WAF
423
+ };
424
+ }
425
+ };
426
+ // Run tests
427
+ for (const [category, payloads] of Object.entries(PAYLOADS)) {
428
+ for (const payload of payloads) {
429
+ results.push(await testPayload(category, payload));
430
+ }
431
+ }
432
+ res.json({
433
+ target: targetBase + targetPath,
434
+ summary: {
435
+ total: results.length,
436
+ blocked: results.filter(r => r.blocked).length,
437
+ passed: results.filter(r => !r.blocked).length
438
+ },
439
+ details: results
440
+ });
441
+ }
442
+ //# sourceMappingURL=redteam.js.map