@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,649 @@
1
+ /**
2
+ * Apparatus TUI Dashboard
3
+ * Security-focused terminal dashboard for Apparatus
4
+ */
5
+ import blessed from 'blessed';
6
+ import contrib from 'blessed-contrib';
7
+ import { SSEClient } from './sse-client.js';
8
+ import { ApiClient, ApiPoller } from './api-client.js';
9
+ import { THEME, formatRelativeTime, getStatusColor, getHealthColor, truncate, } from './theme.js';
10
+ export function createDashboard(options) {
11
+ const { target, refreshInterval = 5000, maxRequests = 100 } = options;
12
+ // Initialize state
13
+ const state = {
14
+ connected: false,
15
+ requests: [],
16
+ };
17
+ const logFilter = {};
18
+ // Create screen
19
+ const screen = blessed.screen({
20
+ smartCSR: true,
21
+ title: 'Apparatus TUI - Apparatus Dashboard',
22
+ });
23
+ // Create grid layout (12x12)
24
+ const grid = new contrib.grid({ rows: 12, cols: 12, screen });
25
+ // ========================================
26
+ // Header (Row 0-1)
27
+ // ========================================
28
+ const header = grid.set(0, 0, 1, 12, contrib.markdown, {
29
+ label: ' Apparatus TUI ',
30
+ tags: true,
31
+ padding: { left: 1, right: 1 },
32
+ style: { border: { fg: THEME.borders.header } },
33
+ });
34
+ function updateHeader() {
35
+ const sseStatus = state.connected
36
+ ? `{green-fg}● SSE{/green-fg}`
37
+ : `{red-fg}○ SSE{/red-fg}`;
38
+ const healthStatus = state.health
39
+ ? `{${getHealthColor(state.health.status)}-fg}${state.health.status}{/${getHealthColor(state.health.status)}-fg} (${(state.health.lag_ms ?? 0).toFixed(0)}ms)`
40
+ : '{gray-fg}unknown{/gray-fg}';
41
+ header.setMarkdown(`**Target:** ${target} | ${sseStatus} | **Health:** ${healthStatus} | ` +
42
+ `**q**:quit **?**:help **f**:filter **x**:release **R**:reconnect`);
43
+ }
44
+ // ========================================
45
+ // Health Status (Row 1-3)
46
+ // ========================================
47
+ const healthBox = grid.set(1, 0, 2, 6, blessed.box, {
48
+ label: ' Health ',
49
+ border: { type: 'line' },
50
+ style: { border: { fg: THEME.borders.health } },
51
+ tags: true,
52
+ padding: { left: 1, right: 1 },
53
+ });
54
+ function updateHealthBox() {
55
+ if (!state.health) {
56
+ healthBox.setContent('{gray-fg}Loading...{/gray-fg}');
57
+ return;
58
+ }
59
+ const h = state.health;
60
+ const statusColor = getHealthColor(h.status);
61
+ const lines = [
62
+ `Status: {${statusColor}-fg}${h.status.toUpperCase()}{/${statusColor}-fg}`,
63
+ `Lag: ${(h.lag_ms ?? 0).toFixed(1)}ms`,
64
+ state.lastUpdated ? `Updated: ${formatRelativeTime(state.lastUpdated)}` : '',
65
+ ];
66
+ healthBox.setContent(lines.filter(Boolean).join('\n'));
67
+ }
68
+ // ========================================
69
+ // Totals (Row 1-3)
70
+ // ========================================
71
+ const totalsBox = grid.set(1, 6, 2, 6, blessed.box, {
72
+ label: ' Totals ',
73
+ border: { type: 'line' },
74
+ style: { border: { fg: THEME.colors.muted } },
75
+ tags: true,
76
+ padding: { left: 1, right: 1 },
77
+ });
78
+ function updateTotalsBox() {
79
+ const reqCount = state.requests.length;
80
+ const tarpitCount = state.tarpit?.count ?? 0;
81
+ const deceptionCount = state.deception?.count ?? 0;
82
+ const lines = [
83
+ `Requests: ${reqCount}`,
84
+ `Tarpit: ${tarpitCount} IPs`,
85
+ `Deception: ${deceptionCount}`,
86
+ ];
87
+ totalsBox.setContent(lines.join('\n'));
88
+ }
89
+ // ========================================
90
+ // Deception Panel (Row 3-5)
91
+ // ========================================
92
+ const deceptionBox = grid.set(3, 0, 2, 6, blessed.box, {
93
+ label: ' Deception Activity ',
94
+ border: { type: 'line' },
95
+ style: { border: { fg: THEME.borders.deception } },
96
+ tags: true,
97
+ padding: { left: 1, right: 1 },
98
+ scrollable: true,
99
+ alwaysScroll: true,
100
+ keys: true,
101
+ vi: true,
102
+ });
103
+ function updateDeceptionBox() {
104
+ if (!state.deception || state.deception.events.length === 0) {
105
+ deceptionBox.setContent('{gray-fg}No deception events{/gray-fg}');
106
+ return;
107
+ }
108
+ const lines = state.deception.events.slice(0, 8).map((e) => {
109
+ const time = new Date(e.timestamp).toLocaleTimeString();
110
+ const typeColor = e.type === 'honeypot_hit' ? 'magenta' : e.type === 'shell_command' ? 'red' : 'yellow';
111
+ return `{${typeColor}-fg}${e.type}{/${typeColor}-fg} ${e.ip} ${truncate(e.route, 20)} ${time}`;
112
+ });
113
+ deceptionBox.setContent(lines.join('\n'));
114
+ }
115
+ // ========================================
116
+ // Tarpit Panel (Row 3-5)
117
+ // ========================================
118
+ const tarpitBox = grid.set(3, 6, 2, 6, blessed.box, {
119
+ label: ' Tarpit (x:release X:all) ',
120
+ border: { type: 'line' },
121
+ style: { border: { fg: 'red' } },
122
+ tags: true,
123
+ padding: { left: 1, right: 1 },
124
+ scrollable: true,
125
+ alwaysScroll: true,
126
+ keys: true,
127
+ vi: true,
128
+ });
129
+ let selectedTarpitIndex = 0;
130
+ function updateTarpitBox() {
131
+ if (!state.tarpit || state.tarpit.trapped.length === 0) {
132
+ tarpitBox.setContent('{gray-fg}No trapped IPs{/gray-fg}\nTraps: ' +
133
+ (state.tarpit?.trapPaths?.join(', ') || '/wp-admin, /.env, /.git'));
134
+ return;
135
+ }
136
+ const lines = state.tarpit.trapped.slice(0, 8).map((t, idx) => {
137
+ const duration = Math.floor((Date.now() - t.trappedAt) / 1000);
138
+ const durationStr = duration > 60 ? `${Math.floor(duration / 60)}m${duration % 60}s` : `${duration}s`;
139
+ const pointer = idx === selectedTarpitIndex ? '{yellow-fg}>{/yellow-fg}' : ' ';
140
+ return `${pointer}{red-fg}${t.ip}{/red-fg} trapped ${durationStr}`;
141
+ });
142
+ lines.push('');
143
+ lines.push(`{gray-fg}Traps: ${state.tarpit.trapPaths.slice(0, 3).join(', ')}{/gray-fg}`);
144
+ tarpitBox.setContent(lines.join('\n'));
145
+ }
146
+ // ========================================
147
+ // Recent Signals Panel (Row 5-7)
148
+ // ========================================
149
+ const signalsBox = grid.set(5, 0, 2, 6, blessed.box, {
150
+ label: ' Recent Threat Signals ',
151
+ border: { type: 'line' },
152
+ style: { border: { fg: 'yellow' } },
153
+ tags: true,
154
+ padding: { left: 1, right: 1 },
155
+ scrollable: true,
156
+ });
157
+ function updateSignalsBox() {
158
+ signalsBox.setContent('{gray-fg}No recent signals{/gray-fg}');
159
+ }
160
+ // ========================================
161
+ // System Info Panel (Row 5-7)
162
+ // ========================================
163
+ const infoBox = grid.set(5, 6, 2, 6, blessed.box, {
164
+ label: ' Connection Info ',
165
+ border: { type: 'line' },
166
+ style: { border: { fg: THEME.colors.muted } },
167
+ tags: true,
168
+ padding: { left: 1, right: 1 },
169
+ });
170
+ function updateInfoBox() {
171
+ const lines = [
172
+ `Target: ${truncate(target, 30)}`,
173
+ state.error ? `{red-fg}Error: ${truncate(state.error, 25)}{/red-fg}` : '',
174
+ ];
175
+ infoBox.setContent(lines.filter(Boolean).join('\n'));
176
+ }
177
+ // ========================================
178
+ // Request Log Table (Row 7-11)
179
+ // ========================================
180
+ const requestTable = grid.set(7, 0, 5, 12, contrib.table, {
181
+ label: ' Live Requests (↑/↓ navigate, Enter details, f filter) ',
182
+ columnWidth: [10, 7, 40, 6, 18],
183
+ columnSpacing: 2,
184
+ keys: true,
185
+ interactive: true,
186
+ fg: 'white',
187
+ style: {
188
+ header: { fg: 'white', bold: true },
189
+ cell: { fg: 'white' },
190
+ border: { fg: THEME.borders.requests },
191
+ },
192
+ });
193
+ function applyFilter(req) {
194
+ if (logFilter.method && req.method !== logFilter.method)
195
+ return false;
196
+ if (logFilter.pathContains && !req.path.includes(logFilter.pathContains))
197
+ return false;
198
+ if (logFilter.ipContains && !req.ip.includes(logFilter.ipContains))
199
+ return false;
200
+ return true;
201
+ }
202
+ function updateRequestTable() {
203
+ const filtered = state.requests.filter(applyFilter);
204
+ const rows = filtered.slice(0, 50).map((r) => {
205
+ const time = new Date(r.timestamp).toLocaleTimeString();
206
+ const statusColor = r.latencyMs !== undefined ? getStatusColor(200) : THEME.colors.muted;
207
+ return [
208
+ time,
209
+ r.method,
210
+ truncate(r.path, 38),
211
+ r.latencyMs?.toString() ?? '-',
212
+ r.ip,
213
+ ];
214
+ });
215
+ requestTable.setData({
216
+ headers: ['Time', 'Method', 'Path', 'ms', 'IP'],
217
+ data: rows.length > 0 ? rows : [['', '', 'No requests yet', '', '']],
218
+ });
219
+ }
220
+ // ========================================
221
+ // Help Modal
222
+ // ========================================
223
+ function showHelp() {
224
+ const helpBox = blessed.box({
225
+ parent: screen,
226
+ top: 'center',
227
+ left: 'center',
228
+ width: '70%',
229
+ height: '70%',
230
+ border: 'line',
231
+ label: ' Help — Press Esc to close ',
232
+ scrollable: true,
233
+ alwaysScroll: true,
234
+ keys: true,
235
+ vi: true,
236
+ tags: true,
237
+ style: { border: { fg: THEME.borders.modal } },
238
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
239
+ content: '{bold}Apparatus TUI Dashboard{/bold}\n\n' +
240
+ '{underline}Keyboard Shortcuts{/underline}\n' +
241
+ ' q, Ctrl+C Quit dashboard\n' +
242
+ ' ?, h Show this help\n' +
243
+ ' f Open filter dialog\n' +
244
+ ' c Clear all filters\n' +
245
+ ' x Release selected tarpit IP\n' +
246
+ ' X Release all tarpit IPs\n' +
247
+ ' R Reconnect SSE\n' +
248
+ ' r Force refresh all data\n' +
249
+ ' ↑/↓, j/k Navigate lists\n' +
250
+ ' Enter Show request details\n' +
251
+ ' Tab Switch focus between panels\n\n' +
252
+ '{underline}Panels{/underline}\n' +
253
+ ' Health System status and event loop lag\n' +
254
+ ' Deception Honeypot activity log\n' +
255
+ ' Tarpit Currently trapped IPs\n' +
256
+ ' Signals Recent threat signals sent\n' +
257
+ ' Requests Live request stream\n\n' +
258
+ '{underline}SSE Events{/underline}\n' +
259
+ ' request New HTTP request received\n' +
260
+ ' deception Honeypot activity detected\n' +
261
+ ' tarpit IP trapped or released\n' +
262
+ ' health System health update\n',
263
+ });
264
+ helpBox.key(['escape', 'q', 'enter'], () => {
265
+ helpBox.destroy();
266
+ screen.render();
267
+ });
268
+ helpBox.focus();
269
+ screen.render();
270
+ }
271
+ // ========================================
272
+ // Filter Modal
273
+ // ========================================
274
+ function showFilterDialog() {
275
+ const filterBox = blessed.form({
276
+ parent: screen,
277
+ top: 'center',
278
+ left: 'center',
279
+ width: '50%',
280
+ height: '50%',
281
+ border: 'line',
282
+ label: ' Filter Requests ',
283
+ keys: true,
284
+ style: { border: { fg: THEME.borders.modal } },
285
+ });
286
+ blessed.text({
287
+ parent: filterBox,
288
+ top: 1,
289
+ left: 2,
290
+ content: 'Method (GET, POST, etc.):',
291
+ });
292
+ const methodInput = blessed.textbox({
293
+ parent: filterBox,
294
+ top: 2,
295
+ left: 2,
296
+ width: 20,
297
+ height: 3,
298
+ border: 'line',
299
+ value: logFilter.method || '',
300
+ });
301
+ blessed.text({
302
+ parent: filterBox,
303
+ top: 5,
304
+ left: 2,
305
+ content: 'Path contains:',
306
+ });
307
+ const pathInput = blessed.textbox({
308
+ parent: filterBox,
309
+ top: 6,
310
+ left: 2,
311
+ width: 30,
312
+ height: 3,
313
+ border: 'line',
314
+ value: logFilter.pathContains || '',
315
+ });
316
+ blessed.text({
317
+ parent: filterBox,
318
+ top: 9,
319
+ left: 2,
320
+ content: 'IP contains:',
321
+ });
322
+ const ipInput = blessed.textbox({
323
+ parent: filterBox,
324
+ top: 10,
325
+ left: 2,
326
+ width: 20,
327
+ height: 3,
328
+ border: 'line',
329
+ value: logFilter.ipContains || '',
330
+ });
331
+ const applyBtn = blessed.button({
332
+ parent: filterBox,
333
+ top: 14,
334
+ left: 2,
335
+ width: 12,
336
+ height: 3,
337
+ border: 'line',
338
+ content: 'Apply',
339
+ align: 'center',
340
+ });
341
+ const cancelBtn = blessed.button({
342
+ parent: filterBox,
343
+ top: 14,
344
+ left: 16,
345
+ width: 12,
346
+ height: 3,
347
+ border: 'line',
348
+ content: 'Cancel',
349
+ align: 'center',
350
+ });
351
+ function apply() {
352
+ const method = methodInput.getValue();
353
+ const path = pathInput.getValue();
354
+ const ip = ipInput.getValue();
355
+ logFilter.method = method || undefined;
356
+ logFilter.pathContains = path || undefined;
357
+ logFilter.ipContains = ip || undefined;
358
+ filterBox.destroy();
359
+ updateRequestTable();
360
+ screen.render();
361
+ }
362
+ applyBtn.on('press', apply);
363
+ cancelBtn.on('press', () => {
364
+ filterBox.destroy();
365
+ screen.render();
366
+ });
367
+ filterBox.key(['escape'], () => {
368
+ filterBox.destroy();
369
+ screen.render();
370
+ });
371
+ filterBox.key(['enter'], apply);
372
+ methodInput.focus();
373
+ screen.render();
374
+ }
375
+ // ========================================
376
+ // Request Details Modal
377
+ // ========================================
378
+ function showRequestDetails() {
379
+ const selectedIndex = requestTable.rows?.selected ?? 0;
380
+ const filtered = state.requests.filter(applyFilter);
381
+ const req = filtered[selectedIndex];
382
+ if (!req)
383
+ return;
384
+ const detailBox = blessed.box({
385
+ parent: screen,
386
+ top: 'center',
387
+ left: 'center',
388
+ width: '80%',
389
+ height: '80%',
390
+ border: 'line',
391
+ label: ' Request Details — Esc to close ',
392
+ scrollable: true,
393
+ alwaysScroll: true,
394
+ keys: true,
395
+ vi: true,
396
+ tags: true,
397
+ style: { border: { fg: THEME.borders.modal } },
398
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
399
+ content: `{bold}Request Details{/bold}\n\n` +
400
+ `Method: ${req.method}\n` +
401
+ `Path: ${req.path}\n` +
402
+ `Full URL: ${req.originalUrl}\n` +
403
+ `HTTP Version: ${req.httpVersion}\n` +
404
+ `IP: ${req.ip}\n` +
405
+ `Timestamp: ${req.timestamp}\n` +
406
+ `Latency: ${req.latencyMs ?? 'N/A'}ms\n\n` +
407
+ `{underline}Query Parameters{/underline}\n` +
408
+ JSON.stringify(req.query, null, 2) + '\n\n' +
409
+ `{underline}Headers{/underline}\n` +
410
+ Object.entries(req.headers).map(([k, v]) => ` ${k}: ${v}`).join('\n') + '\n\n' +
411
+ (req.body ? `{underline}Body{/underline}\n${JSON.stringify(req.body, null, 2)}` : ''),
412
+ });
413
+ detailBox.key(['escape', 'q', 'enter'], () => {
414
+ detailBox.destroy();
415
+ screen.render();
416
+ });
417
+ detailBox.focus();
418
+ screen.render();
419
+ }
420
+ // ========================================
421
+ // AI Chat Modal (Placeholder)
422
+ // ========================================
423
+ function showAIChatModal() {
424
+ const modal = blessed.box({
425
+ parent: screen,
426
+ top: 'center',
427
+ left: 'center',
428
+ width: '60%',
429
+ height: '50%',
430
+ border: 'line',
431
+ label: ' AI Chat — Esc to close ',
432
+ tags: true,
433
+ style: { border: { fg: THEME.borders.modal } },
434
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
435
+ content: '{bold}AI Chat{/bold}\n\nAI Chat modal coming soon.\n\nPress Esc to close.',
436
+ });
437
+ modal.key(['escape', 'q'], () => {
438
+ modal.destroy();
439
+ screen.render();
440
+ });
441
+ modal.focus();
442
+ screen.render();
443
+ }
444
+ // ========================================
445
+ // Escape Scan Modal (Placeholder)
446
+ // ========================================
447
+ function showEscapeModal() {
448
+ const modal = blessed.box({
449
+ parent: screen,
450
+ top: 'center',
451
+ left: 'center',
452
+ width: '60%',
453
+ height: '50%',
454
+ border: 'line',
455
+ label: ' Escape Artist — Esc to close ',
456
+ tags: true,
457
+ style: { border: { fg: THEME.borders.modal } },
458
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
459
+ content: '{bold}Escape Artist (Egress Tester){/bold}\n\nEgress scan modal coming soon.\n\nPress Esc to close.',
460
+ });
461
+ modal.key(['escape', 'q'], () => {
462
+ modal.destroy();
463
+ screen.render();
464
+ });
465
+ modal.focus();
466
+ screen.render();
467
+ }
468
+ // ========================================
469
+ // Update all panels
470
+ // ========================================
471
+ function render() {
472
+ updateHeader();
473
+ updateHealthBox();
474
+ updateTotalsBox();
475
+ updateDeceptionBox();
476
+ updateTarpitBox();
477
+ updateSignalsBox();
478
+ updateInfoBox();
479
+ updateRequestTable();
480
+ screen.render();
481
+ }
482
+ // ========================================
483
+ // Initialize API client and poller
484
+ // ========================================
485
+ const apiClient = new ApiClient({ baseUrl: target });
486
+ const poller = new ApiPoller(apiClient);
487
+ // Start polling
488
+ poller.start('health', () => apiClient.getHealth(), 5000, (data) => { state.health = data; state.lastUpdated = Date.now(); render(); }, (err) => { state.error = err.message; render(); });
489
+ poller.start('tarpit', () => apiClient.getTarpitStatus(), 10000, (data) => { state.tarpit = data; render(); }, (err) => { });
490
+ poller.start('deception', () => apiClient.getDeceptionHistory(), 10000, (data) => { state.deception = data; render(); }, (err) => { });
491
+ // ========================================
492
+ // Initialize SSE client
493
+ // ========================================
494
+ const sseClient = new SSEClient({ baseUrl: target });
495
+ sseClient.on('connected', () => {
496
+ state.connected = true;
497
+ state.error = undefined;
498
+ render();
499
+ });
500
+ sseClient.on('disconnected', () => {
501
+ state.connected = false;
502
+ render();
503
+ });
504
+ sseClient.on('request', (event) => {
505
+ state.requests.unshift(event.data);
506
+ if (state.requests.length > maxRequests) {
507
+ state.requests.pop();
508
+ }
509
+ render();
510
+ });
511
+ sseClient.on('deception', (event) => {
512
+ if (!state.deception) {
513
+ state.deception = { count: 0, events: [] };
514
+ }
515
+ state.deception.events.unshift(event.data);
516
+ state.deception.count++;
517
+ if (state.deception.events.length > 50) {
518
+ state.deception.events.pop();
519
+ }
520
+ render();
521
+ });
522
+ sseClient.on('tarpit', (event) => {
523
+ // Trigger a refresh of tarpit data
524
+ apiClient.getTarpitStatus().then(data => {
525
+ state.tarpit = data;
526
+ render();
527
+ }).catch(() => { });
528
+ });
529
+ sseClient.on('health', (event) => {
530
+ state.health = event.data;
531
+ state.lastUpdated = Date.now();
532
+ render();
533
+ });
534
+ sseClient.on('reconnecting', ({ attempt, delay }) => {
535
+ state.error = `Reconnecting... (${attempt})`;
536
+ render();
537
+ });
538
+ sseClient.on('max_reconnect_reached', () => {
539
+ state.error = 'Max reconnect attempts reached. Press R to retry.';
540
+ render();
541
+ });
542
+ // Connect SSE
543
+ sseClient.connect();
544
+ // ========================================
545
+ // Keyboard bindings
546
+ // ========================================
547
+ screen.key(['q', 'C-c'], () => {
548
+ sseClient.disconnect();
549
+ poller.stopAll();
550
+ screen.destroy();
551
+ process.exit(0);
552
+ });
553
+ screen.key(['?', 'h'], showHelp);
554
+ screen.key(['f'], showFilterDialog);
555
+ screen.key(['c'], () => {
556
+ logFilter.method = undefined;
557
+ logFilter.pathContains = undefined;
558
+ logFilter.ipContains = undefined;
559
+ updateRequestTable();
560
+ screen.render();
561
+ });
562
+ screen.key(['R'], () => {
563
+ state.error = 'Reconnecting...';
564
+ render();
565
+ sseClient.reconnect();
566
+ });
567
+ screen.key(['r'], async () => {
568
+ state.error = 'Refreshing...';
569
+ render();
570
+ try {
571
+ const [health, tarpit, deception] = await Promise.all([
572
+ apiClient.getHealth(),
573
+ apiClient.getTarpitStatus().catch(() => undefined),
574
+ apiClient.getDeceptionHistory().catch(() => undefined),
575
+ ]);
576
+ state.health = health;
577
+ if (tarpit)
578
+ state.tarpit = tarpit;
579
+ if (deception)
580
+ state.deception = deception;
581
+ state.lastUpdated = Date.now();
582
+ state.error = undefined;
583
+ }
584
+ catch (err) {
585
+ state.error = err instanceof Error ? err.message : 'Refresh failed';
586
+ }
587
+ render();
588
+ });
589
+ screen.key(['x'], async () => {
590
+ if (!state.tarpit?.trapped.length)
591
+ return;
592
+ const ip = state.tarpit.trapped[selectedTarpitIndex]?.ip;
593
+ if (!ip)
594
+ return;
595
+ try {
596
+ await apiClient.releaseTarpitIp(ip);
597
+ // Refresh tarpit data
598
+ const tarpit = await apiClient.getTarpitStatus();
599
+ state.tarpit = tarpit;
600
+ if (selectedTarpitIndex >= tarpit.trapped.length) {
601
+ selectedTarpitIndex = Math.max(0, tarpit.trapped.length - 1);
602
+ }
603
+ render();
604
+ }
605
+ catch (err) {
606
+ state.error = `Failed to release ${ip}`;
607
+ render();
608
+ }
609
+ });
610
+ screen.key(['X'], async () => {
611
+ try {
612
+ await apiClient.releaseAllTarpit();
613
+ const tarpit = await apiClient.getTarpitStatus();
614
+ state.tarpit = tarpit;
615
+ selectedTarpitIndex = 0;
616
+ render();
617
+ }
618
+ catch (err) {
619
+ state.error = 'Failed to release all IPs';
620
+ render();
621
+ }
622
+ });
623
+ screen.key(['j', 'down'], () => {
624
+ if (state.tarpit && selectedTarpitIndex < state.tarpit.trapped.length - 1) {
625
+ selectedTarpitIndex++;
626
+ updateTarpitBox();
627
+ screen.render();
628
+ }
629
+ });
630
+ screen.key(['k', 'up'], () => {
631
+ if (selectedTarpitIndex > 0) {
632
+ selectedTarpitIndex--;
633
+ updateTarpitBox();
634
+ screen.render();
635
+ }
636
+ });
637
+ requestTable.key(['enter'], showRequestDetails);
638
+ screen.key(['a'], showAIChatModal);
639
+ screen.key(['e'], showEscapeModal);
640
+ // Initial render
641
+ render();
642
+ return {
643
+ screen,
644
+ sseClient,
645
+ poller,
646
+ state,
647
+ };
648
+ }
649
+ //# sourceMappingURL=dashboard.js.map