s3db.js 13.6.0 → 14.0.2

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 (193) hide show
  1. package/README.md +139 -43
  2. package/dist/s3db.cjs +72425 -38970
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72177 -38764
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +94 -49
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  34. package/src/plugins/api/concerns/route-context.js +601 -0
  35. package/src/plugins/api/concerns/state-machine.js +288 -0
  36. package/src/plugins/api/index.js +180 -41
  37. package/src/plugins/api/routes/auth-routes.js +198 -30
  38. package/src/plugins/api/routes/resource-routes.js +19 -4
  39. package/src/plugins/api/server/health-manager.class.js +163 -0
  40. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  41. package/src/plugins/api/server/router.class.js +472 -0
  42. package/src/plugins/api/server.js +280 -1303
  43. package/src/plugins/api/utils/custom-routes.js +17 -5
  44. package/src/plugins/api/utils/guards.js +76 -17
  45. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  46. package/src/plugins/api/utils/openapi-generator.js +7 -6
  47. package/src/plugins/api/utils/template-engine.js +77 -3
  48. package/src/plugins/audit.plugin.js +30 -8
  49. package/src/plugins/backup.plugin.js +110 -14
  50. package/src/plugins/cache/cache.class.js +22 -5
  51. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  52. package/src/plugins/cache/memory-cache.class.js +211 -57
  53. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  54. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  55. package/src/plugins/cache/redis-cache.class.js +552 -0
  56. package/src/plugins/cache/s3-cache.class.js +17 -8
  57. package/src/plugins/cache.plugin.js +176 -61
  58. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  59. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  60. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  62. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  66. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  67. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  68. package/src/plugins/cloud-inventory/index.js +29 -8
  69. package/src/plugins/cloud-inventory/registry.js +64 -42
  70. package/src/plugins/cloud-inventory.plugin.js +240 -138
  71. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  72. package/src/plugins/concerns/resource-names.js +100 -0
  73. package/src/plugins/consumers/index.js +10 -2
  74. package/src/plugins/consumers/sqs-consumer.js +12 -2
  75. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  76. package/src/plugins/cookie-farm.errors.js +73 -0
  77. package/src/plugins/cookie-farm.plugin.js +869 -0
  78. package/src/plugins/costs.plugin.js +7 -1
  79. package/src/plugins/eventual-consistency/analytics.js +94 -19
  80. package/src/plugins/eventual-consistency/config.js +15 -7
  81. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  82. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  83. package/src/plugins/eventual-consistency/helpers.js +39 -14
  84. package/src/plugins/eventual-consistency/install.js +21 -2
  85. package/src/plugins/eventual-consistency/utils.js +32 -10
  86. package/src/plugins/fulltext.plugin.js +38 -11
  87. package/src/plugins/geo.plugin.js +61 -9
  88. package/src/plugins/identity/concerns/config.js +61 -0
  89. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  90. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  91. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  92. package/src/plugins/identity/concerns/token-generator.js +29 -4
  93. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  94. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  95. package/src/plugins/identity/drivers/index.js +18 -0
  96. package/src/plugins/identity/drivers/password-driver.js +122 -0
  97. package/src/plugins/identity/email-service.js +17 -2
  98. package/src/plugins/identity/index.js +413 -69
  99. package/src/plugins/identity/oauth2-server.js +413 -30
  100. package/src/plugins/identity/oidc-discovery.js +16 -8
  101. package/src/plugins/identity/rsa-keys.js +115 -35
  102. package/src/plugins/identity/server.js +166 -45
  103. package/src/plugins/identity/session-manager.js +53 -7
  104. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  105. package/src/plugins/identity/ui/routes.js +363 -255
  106. package/src/plugins/importer/index.js +153 -20
  107. package/src/plugins/index.js +9 -2
  108. package/src/plugins/kubernetes-inventory/index.js +6 -0
  109. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  110. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  111. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  112. package/src/plugins/metrics.plugin.js +64 -16
  113. package/src/plugins/ml/base-model.class.js +25 -15
  114. package/src/plugins/ml/regression-model.class.js +1 -1
  115. package/src/plugins/ml.errors.js +57 -25
  116. package/src/plugins/ml.plugin.js +28 -4
  117. package/src/plugins/namespace.js +210 -0
  118. package/src/plugins/plugin.class.js +180 -8
  119. package/src/plugins/puppeteer/console-monitor.js +729 -0
  120. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  121. package/src/plugins/puppeteer/network-monitor.js +816 -0
  122. package/src/plugins/puppeteer/performance-manager.js +746 -0
  123. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  124. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  125. package/src/plugins/puppeteer.errors.js +81 -0
  126. package/src/plugins/puppeteer.plugin.js +1327 -0
  127. package/src/plugins/queue-consumer.plugin.js +69 -14
  128. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  129. package/src/plugins/recon/concerns/command-runner.js +148 -0
  130. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  131. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  132. package/src/plugins/recon/concerns/process-manager.js +338 -0
  133. package/src/plugins/recon/concerns/report-generator.js +478 -0
  134. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  135. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  136. package/src/plugins/recon/config/defaults.js +321 -0
  137. package/src/plugins/recon/config/resources.js +370 -0
  138. package/src/plugins/recon/index.js +778 -0
  139. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  140. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  141. package/src/plugins/recon/managers/storage-manager.js +745 -0
  142. package/src/plugins/recon/managers/target-manager.js +274 -0
  143. package/src/plugins/recon/stages/asn-stage.js +314 -0
  144. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  145. package/src/plugins/recon/stages/dns-stage.js +107 -0
  146. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  147. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  148. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  149. package/src/plugins/recon/stages/http-stage.js +89 -0
  150. package/src/plugins/recon/stages/latency-stage.js +148 -0
  151. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  152. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  153. package/src/plugins/recon/stages/ports-stage.js +169 -0
  154. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  155. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  156. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  157. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  158. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  159. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  160. package/src/plugins/recon/stages/whois-stage.js +349 -0
  161. package/src/plugins/recon.plugin.js +75 -0
  162. package/src/plugins/recon.plugin.js.backup +2635 -0
  163. package/src/plugins/relation.errors.js +87 -14
  164. package/src/plugins/replicator.plugin.js +514 -137
  165. package/src/plugins/replicators/base-replicator.class.js +89 -1
  166. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  167. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  168. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  169. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  170. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  171. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  172. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  173. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  174. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  175. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  176. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  177. package/src/plugins/s3-queue.plugin.js +464 -65
  178. package/src/plugins/scheduler.plugin.js +20 -6
  179. package/src/plugins/state-machine.plugin.js +40 -9
  180. package/src/plugins/tfstate/README.md +126 -126
  181. package/src/plugins/tfstate/base-driver.js +28 -4
  182. package/src/plugins/tfstate/errors.js +65 -10
  183. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  184. package/src/plugins/tfstate/index.js +163 -90
  185. package/src/plugins/tfstate/s3-driver.js +64 -6
  186. package/src/plugins/ttl.plugin.js +72 -17
  187. package/src/plugins/vector/distances.js +18 -12
  188. package/src/plugins/vector/kmeans.js +26 -4
  189. package/src/resource.class.js +115 -19
  190. package/src/testing/factory.class.js +20 -3
  191. package/src/testing/seeder.class.js +7 -1
  192. package/src/clients/memory-client.md +0 -917
  193. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -0,0 +1,691 @@
1
+ /**
2
+ * UptimeBehavior
3
+ *
4
+ * Monitors target availability and calculates uptime metrics:
5
+ * - Periodic health checks (ping, HTTP, DNS)
6
+ * - Uptime percentage calculation
7
+ * - Downtime detection and alerting
8
+ * - Historical availability tracking
9
+ *
10
+ * Usage:
11
+ * ```javascript
12
+ * const plugin = new ReconPlugin({
13
+ * behaviors: {
14
+ * uptime: {
15
+ * enabled: true,
16
+ * interval: 60000, // Check every 60 seconds
17
+ * methods: ['ping', 'http', 'dns'],
18
+ * alertOnDowntime: true,
19
+ * downtimeThreshold: 3 // 3 failed checks = downtime
20
+ * }
21
+ * }
22
+ * });
23
+ * ```
24
+ */
25
+
26
+ import { spawn } from 'node:child_process';
27
+ import dns from 'node:dns/promises';
28
+ import https from 'node:https';
29
+ import http from 'node:http';
30
+
31
+ export class UptimeBehavior {
32
+ constructor(plugin, config = {}) {
33
+ this.plugin = plugin;
34
+ this.config = {
35
+ enabled: true,
36
+ checkInterval: 20000, // Check every 20 seconds
37
+ aggregationInterval: 60000, // Aggregate every 60 seconds (1 minute cohorts)
38
+ methods: ['ping', 'http'], // ping, http, dns
39
+ alertOnDowntime: true,
40
+ downtimeThreshold: 3, // Failed checks before considered down
41
+ timeout: 5000, // 5 seconds timeout
42
+ retainHistory: 30 * 24 * 60 * 60 * 1000, // 30 days
43
+ persistRawChecks: false, // Only persist aggregated data
44
+ ...config
45
+ };
46
+
47
+ this.checks = new Map(); // target.host -> { status, consecutiveFails, lastCheck, history }
48
+ this.checkIntervals = new Map(); // target.host -> intervalId (20s checks)
49
+ this.aggregationIntervals = new Map(); // target.host -> intervalId (60s aggregation)
50
+ this.minuteBuffer = new Map(); // target.host -> [check1, check2, check3] (buffer for current minute)
51
+ }
52
+
53
+ /**
54
+ * Start monitoring a target
55
+ */
56
+ async startMonitoring(target) {
57
+ const host = target.host;
58
+
59
+ // Initialize check state
60
+ if (!this.checks.has(host)) {
61
+ this.checks.set(host, {
62
+ status: 'unknown',
63
+ consecutiveFails: 0,
64
+ consecutiveSuccess: 0,
65
+ lastCheck: null,
66
+ lastUp: null,
67
+ lastDown: null,
68
+ totalChecks: 0,
69
+ successfulChecks: 0,
70
+ failedChecks: 0,
71
+ history: [] // Stores minute-aggregated data
72
+ });
73
+ }
74
+
75
+ // Initialize minute buffer
76
+ if (!this.minuteBuffer.has(host)) {
77
+ this.minuteBuffer.set(host, []);
78
+ }
79
+
80
+ // Start periodic checks (every 20 seconds)
81
+ if (!this.checkIntervals.has(host)) {
82
+ // Run first check immediately
83
+ await this._performCheck(target);
84
+
85
+ // Schedule periodic checks
86
+ const checkIntervalId = setInterval(async () => {
87
+ await this._performCheck(target);
88
+ }, this.config.checkInterval);
89
+
90
+ this.checkIntervals.set(host, checkIntervalId);
91
+ }
92
+
93
+ // Start aggregation interval (every 60 seconds)
94
+ if (!this.aggregationIntervals.has(host)) {
95
+ const aggregationIntervalId = setInterval(async () => {
96
+ await this._aggregateMinute(target);
97
+ }, this.config.aggregationInterval);
98
+
99
+ this.aggregationIntervals.set(host, aggregationIntervalId);
100
+ }
101
+
102
+ return this.getStatus(host);
103
+ }
104
+
105
+ /**
106
+ * Stop monitoring a target
107
+ */
108
+ stopMonitoring(host) {
109
+ // Stop check interval
110
+ const checkIntervalId = this.checkIntervals.get(host);
111
+ if (checkIntervalId) {
112
+ clearInterval(checkIntervalId);
113
+ this.checkIntervals.delete(host);
114
+ }
115
+
116
+ // Stop aggregation interval
117
+ const aggregationIntervalId = this.aggregationIntervals.get(host);
118
+ if (aggregationIntervalId) {
119
+ clearInterval(aggregationIntervalId);
120
+ this.aggregationIntervals.delete(host);
121
+ }
122
+
123
+ // Clear minute buffer
124
+ this.minuteBuffer.delete(host);
125
+ }
126
+
127
+ /**
128
+ * Get current uptime status for a target
129
+ */
130
+ getStatus(host) {
131
+ const check = this.checks.get(host);
132
+ if (!check) {
133
+ return null;
134
+ }
135
+
136
+ const uptimePercentage = check.totalChecks > 0
137
+ ? (check.successfulChecks / check.totalChecks) * 100
138
+ : 0;
139
+
140
+ return {
141
+ host,
142
+ status: check.status,
143
+ uptimePercentage: uptimePercentage.toFixed(2),
144
+ totalChecks: check.totalChecks,
145
+ successfulChecks: check.successfulChecks,
146
+ failedChecks: check.failedChecks,
147
+ lastCheck: check.lastCheck,
148
+ lastUp: check.lastUp,
149
+ lastDown: check.lastDown,
150
+ consecutiveFails: check.consecutiveFails,
151
+ consecutiveSuccess: check.consecutiveSuccess,
152
+ isDown: check.consecutiveFails >= this.config.downtimeThreshold,
153
+ recentHistory: check.history.slice(-10) // Last 10 checks
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Get uptime statistics for all monitored targets
159
+ */
160
+ getAllStatuses() {
161
+ const statuses = [];
162
+ for (const host of this.checks.keys()) {
163
+ statuses.push(this.getStatus(host));
164
+ }
165
+ return statuses;
166
+ }
167
+
168
+ /**
169
+ * Perform a health check on a target
170
+ */
171
+ async _performCheck(target) {
172
+ const host = target.host;
173
+ const check = this.checks.get(host);
174
+ if (!check) return;
175
+
176
+ const timestamp = new Date().toISOString();
177
+ const results = {
178
+ timestamp,
179
+ methods: {}
180
+ };
181
+
182
+ // Run all configured check methods
183
+ for (const method of this.config.methods) {
184
+ try {
185
+ switch (method) {
186
+ case 'ping':
187
+ results.methods.ping = await this._checkPing(target);
188
+ break;
189
+ case 'http':
190
+ results.methods.http = await this._checkHttp(target);
191
+ break;
192
+ case 'dns':
193
+ results.methods.dns = await this._checkDns(target);
194
+ break;
195
+ }
196
+ } catch (error) {
197
+ results.methods[method] = {
198
+ status: 'error',
199
+ error: error.message
200
+ };
201
+ }
202
+ }
203
+
204
+ // Determine overall status (at least one method succeeded)
205
+ const anySuccess = Object.values(results.methods).some(r => r.status === 'ok');
206
+ results.overallStatus = anySuccess ? 'up' : 'down';
207
+
208
+ // Update check state
209
+ check.totalChecks++;
210
+ check.lastCheck = timestamp;
211
+
212
+ if (results.overallStatus === 'up') {
213
+ check.successfulChecks++;
214
+ check.consecutiveFails = 0;
215
+ check.consecutiveSuccess++;
216
+ check.lastUp = timestamp;
217
+
218
+ // Transition from down to up
219
+ if (check.status === 'down') {
220
+ await this._handleTransition(target, 'down', 'up', results);
221
+ }
222
+
223
+ check.status = 'up';
224
+ } else {
225
+ check.failedChecks++;
226
+ check.consecutiveFails++;
227
+ check.consecutiveSuccess = 0;
228
+
229
+ // Check if threshold reached
230
+ if (check.consecutiveFails >= this.config.downtimeThreshold) {
231
+ check.lastDown = timestamp;
232
+
233
+ // Transition from up to down
234
+ if (check.status !== 'down') {
235
+ await this._handleTransition(target, check.status, 'down', results);
236
+ }
237
+
238
+ check.status = 'down';
239
+ }
240
+ }
241
+
242
+ // Add to minute buffer for aggregation
243
+ const buffer = this.minuteBuffer.get(host) || [];
244
+ buffer.push({
245
+ timestamp,
246
+ status: results.overallStatus,
247
+ methods: results.methods,
248
+ latency: this._extractLatency(results.methods)
249
+ });
250
+ this.minuteBuffer.set(host, buffer);
251
+
252
+ // Optionally persist raw checks (if enabled)
253
+ if (this.config.persistRawChecks) {
254
+ await this._persistRawCheck(host, results);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Aggregate minute buffer and persist
260
+ */
261
+ async _aggregateMinute(target) {
262
+ const host = target.host;
263
+ const check = this.checks.get(host);
264
+ const buffer = this.minuteBuffer.get(host) || [];
265
+
266
+ if (buffer.length === 0) return;
267
+
268
+ // Calculate minute-aggregated metrics
269
+ const minuteCohort = this._extractMinuteCohort(buffer[0].timestamp);
270
+ const successCount = buffer.filter(c => c.status === 'up').length;
271
+ const failCount = buffer.filter(c => c.status === 'down').length;
272
+ const uptimePercent = ((successCount / buffer.length) * 100).toFixed(2);
273
+
274
+ // Calculate average latency per method
275
+ const avgLatencies = this._calculateAverageLatencies(buffer);
276
+
277
+ // Create aggregated minute record
278
+ const minuteRecord = {
279
+ minuteCohort, // "2025-01-01T12:34" (minute precision)
280
+ timestamp: buffer[0].timestamp, // First check of the minute
281
+ sampleCount: buffer.length,
282
+ successCount,
283
+ failCount,
284
+ uptimePercent,
285
+ avgLatencies,
286
+ overallStatus: uptimePercent >= 66.67 ? 'up' : 'down' // 2/3 samples up = minute up
287
+ };
288
+
289
+ // Add to history (minute-aggregated)
290
+ check.history.push(minuteRecord);
291
+
292
+ // Prune old history
293
+ this._pruneHistory(check);
294
+
295
+ // Persist aggregated status
296
+ await this._persistStatus(host, check);
297
+
298
+ // Persist minute cohort
299
+ await this._persistMinuteCohort(host, minuteRecord);
300
+
301
+ // Clear buffer
302
+ this.minuteBuffer.set(host, []);
303
+ }
304
+
305
+ /**
306
+ * Extract minute cohort from ISO timestamp
307
+ */
308
+ _extractMinuteCohort(isoTimestamp) {
309
+ // "2025-01-01T12:34:56.789Z" -> "2025-01-01T12:34"
310
+ return isoTimestamp.substring(0, 16);
311
+ }
312
+
313
+ /**
314
+ * Calculate average latencies across samples
315
+ */
316
+ _calculateAverageLatencies(buffer) {
317
+ const latencies = {};
318
+ const methods = Object.keys(buffer[0]?.methods || {});
319
+
320
+ for (const method of methods) {
321
+ const values = buffer
322
+ .map(b => b.latency?.[method])
323
+ .filter(v => v != null && !isNaN(v));
324
+
325
+ if (values.length > 0) {
326
+ latencies[method] = {
327
+ avg: (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2),
328
+ min: Math.min(...values).toFixed(2),
329
+ max: Math.max(...values).toFixed(2),
330
+ samples: values.length
331
+ };
332
+ }
333
+ }
334
+
335
+ return latencies;
336
+ }
337
+
338
+ /**
339
+ * Extract latency from check methods
340
+ */
341
+ _extractLatency(methods) {
342
+ const latencies = {};
343
+
344
+ for (const [method, result] of Object.entries(methods)) {
345
+ if (result.status === 'ok') {
346
+ latencies[method] = result.latency || result.duration || null;
347
+ }
348
+ }
349
+
350
+ return latencies;
351
+ }
352
+
353
+ /**
354
+ * Check target via ICMP ping
355
+ */
356
+ async _checkPing(target) {
357
+ return new Promise((resolve) => {
358
+ const startTime = Date.now();
359
+
360
+ const proc = spawn('ping', ['-c', '1', '-W', String(Math.floor(this.config.timeout / 1000)), target.host]);
361
+
362
+ let stdout = '';
363
+ let stderr = '';
364
+
365
+ proc.stdout.on('data', (data) => {
366
+ stdout += data.toString();
367
+ });
368
+
369
+ proc.stderr.on('data', (data) => {
370
+ stderr += data.toString();
371
+ });
372
+
373
+ proc.on('close', (code) => {
374
+ const duration = Date.now() - startTime;
375
+
376
+ if (code === 0) {
377
+ // Extract latency from ping output
378
+ const match = stdout.match(/time=([0-9.]+)\s*ms/);
379
+ const latency = match ? parseFloat(match[1]) : null;
380
+
381
+ resolve({
382
+ status: 'ok',
383
+ latency,
384
+ duration
385
+ });
386
+ } else {
387
+ resolve({
388
+ status: 'error',
389
+ error: stderr || 'Ping failed',
390
+ duration
391
+ });
392
+ }
393
+ });
394
+
395
+ proc.on('error', (error) => {
396
+ resolve({
397
+ status: 'error',
398
+ error: error.message,
399
+ duration: Date.now() - startTime
400
+ });
401
+ });
402
+
403
+ // Timeout handling
404
+ setTimeout(() => {
405
+ proc.kill();
406
+ resolve({
407
+ status: 'timeout',
408
+ error: 'Ping timeout',
409
+ duration: this.config.timeout
410
+ });
411
+ }, this.config.timeout);
412
+ });
413
+ }
414
+
415
+ /**
416
+ * Check target via HTTP/HTTPS request
417
+ */
418
+ async _checkHttp(target) {
419
+ return new Promise((resolve) => {
420
+ const startTime = Date.now();
421
+ const protocol = target.protocol === 'http' ? http : https;
422
+ const port = target.port || (target.protocol === 'http' ? 80 : 443);
423
+ const url = `${target.protocol}://${target.host}:${port}${target.path || '/'}`;
424
+
425
+ const req = protocol.get(url, {
426
+ timeout: this.config.timeout,
427
+ rejectUnauthorized: false // Accept self-signed certs
428
+ }, (res) => {
429
+ const duration = Date.now() - startTime;
430
+
431
+ resolve({
432
+ status: 'ok',
433
+ statusCode: res.statusCode,
434
+ duration
435
+ });
436
+
437
+ // Consume response to free up memory
438
+ res.resume();
439
+ });
440
+
441
+ req.on('error', (error) => {
442
+ resolve({
443
+ status: 'error',
444
+ error: error.message,
445
+ duration: Date.now() - startTime
446
+ });
447
+ });
448
+
449
+ req.on('timeout', () => {
450
+ req.destroy();
451
+ resolve({
452
+ status: 'timeout',
453
+ error: 'HTTP timeout',
454
+ duration: this.config.timeout
455
+ });
456
+ });
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Check target via DNS resolution
462
+ */
463
+ async _checkDns(target) {
464
+ const startTime = Date.now();
465
+
466
+ try {
467
+ const addresses = await dns.resolve4(target.host);
468
+ const duration = Date.now() - startTime;
469
+
470
+ return {
471
+ status: 'ok',
472
+ addresses,
473
+ duration
474
+ };
475
+ } catch (error) {
476
+ return {
477
+ status: 'error',
478
+ error: error.message,
479
+ duration: Date.now() - startTime
480
+ };
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Handle status transitions (up->down, down->up)
486
+ */
487
+ async _handleTransition(target, fromStatus, toStatus, checkResults) {
488
+ const transition = {
489
+ host: target.host,
490
+ from: fromStatus,
491
+ to: toStatus,
492
+ timestamp: new Date().toISOString(),
493
+ checkResults
494
+ };
495
+
496
+ // Emit event
497
+ if (this.plugin.emit) {
498
+ this.plugin.emit('uptime:transition', transition);
499
+ }
500
+
501
+ // Alert on downtime
502
+ if (toStatus === 'down' && this.config.alertOnDowntime) {
503
+ await this._sendDowntimeAlert(target, transition);
504
+ }
505
+
506
+ // Log transition
507
+ console.log(`[UptimeBehavior] ${target.host}: ${fromStatus} -> ${toStatus}`);
508
+
509
+ // Persist transition event
510
+ await this._persistTransition(transition);
511
+ }
512
+
513
+ /**
514
+ * Send downtime alert
515
+ */
516
+ async _sendDowntimeAlert(target, transition) {
517
+ // This can be extended to send alerts via:
518
+ // - Webhook
519
+ // - Email
520
+ // - Slack/Discord
521
+ // - PagerDuty
522
+ // For now, just log
523
+
524
+ console.warn(`[ALERT] Target ${target.host} is DOWN!`, {
525
+ consecutiveFails: this.checks.get(target.host).consecutiveFails,
526
+ lastUp: this.checks.get(target.host).lastUp,
527
+ checkResults: transition.checkResults
528
+ });
529
+ }
530
+
531
+ /**
532
+ * Prune old history entries
533
+ */
534
+ _pruneHistory(check) {
535
+ const cutoffTime = Date.now() - this.config.retainHistory;
536
+ check.history = check.history.filter(entry => {
537
+ return new Date(entry.timestamp).getTime() > cutoffTime;
538
+ });
539
+ }
540
+
541
+ /**
542
+ * Persist uptime status to plugin storage
543
+ */
544
+ async _persistStatus(host, check) {
545
+ try {
546
+ const storage = this.plugin.getStorage();
547
+ if (!storage) return;
548
+
549
+ const namespace = this.plugin.namespace || '';
550
+ const key = storage.getPluginKey(null, namespace, 'uptime', host, 'status.json');
551
+ await storage.set(key, {
552
+ host,
553
+ status: check.status,
554
+ totalChecks: check.totalChecks,
555
+ successfulChecks: check.successfulChecks,
556
+ failedChecks: check.failedChecks,
557
+ uptimePercentage: ((check.successfulChecks / check.totalChecks) * 100).toFixed(2),
558
+ lastCheck: check.lastCheck,
559
+ lastUp: check.lastUp,
560
+ lastDown: check.lastDown,
561
+ consecutiveFails: check.consecutiveFails,
562
+ consecutiveSuccess: check.consecutiveSuccess,
563
+ updatedAt: new Date().toISOString()
564
+ }, { behavior: 'body-only' });
565
+ } catch (error) {
566
+ console.error(`Failed to persist uptime status for ${host}:`, error.message);
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Persist transition event
572
+ */
573
+ async _persistTransition(transition) {
574
+ try {
575
+ const storage = this.plugin.getStorage();
576
+ if (!storage) return;
577
+
578
+ const namespace = this.plugin.namespace || '';
579
+ const timestamp = transition.timestamp.replace(/[:.]/g, '-');
580
+ const key = storage.getPluginKey(null, namespace, 'uptime', transition.host, 'transitions', `${timestamp}.json`);
581
+
582
+ await storage.set(key, transition, { behavior: 'body-only' });
583
+ } catch (error) {
584
+ console.error(`Failed to persist transition for ${transition.host}:`, error.message);
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Persist minute cohort (aggregated data)
590
+ */
591
+ async _persistMinuteCohort(host, minuteRecord) {
592
+ try {
593
+ const storage = this.plugin.getStorage();
594
+ if (!storage) return;
595
+
596
+ const namespace = this.plugin.namespace || '';
597
+
598
+ // Store minute cohorts: plugin=recon/<namespace>/uptime/<host>/cohorts/<YYYY-MM-DD>/<HH-MM>.json
599
+ const day = minuteRecord.minuteCohort.substring(0, 10); // "2025-01-01"
600
+ const hourMinute = minuteRecord.minuteCohort.substring(11).replace(':', '-'); // "12-34"
601
+
602
+ const key = storage.getPluginKey(null, namespace, 'uptime', host, 'cohorts', day, `${hourMinute}.json`);
603
+
604
+ await storage.set(key, minuteRecord, { behavior: 'body-only' });
605
+ } catch (error) {
606
+ console.error(`Failed to persist minute cohort for ${host}:`, error.message);
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Persist raw check (if enabled)
612
+ */
613
+ async _persistRawCheck(host, checkResult) {
614
+ try {
615
+ const storage = this.plugin.getStorage();
616
+ if (!storage) return;
617
+
618
+ const namespace = this.plugin.namespace || '';
619
+ const timestamp = checkResult.timestamp.replace(/[:.]/g, '-');
620
+ const key = storage.getPluginKey(null, namespace, 'uptime', host, 'raw', `${timestamp}.json`);
621
+
622
+ await storage.set(key, checkResult, { behavior: 'body-only' });
623
+ } catch (error) {
624
+ console.error(`Failed to persist raw check for ${host}:`, error.message);
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Load historical status from storage
630
+ */
631
+ async loadStatus(host) {
632
+ try {
633
+ const storage = this.plugin.getStorage();
634
+ if (!storage) return null;
635
+
636
+ const namespace = this.plugin.namespace || '';
637
+ const key = storage.getPluginKey(null, namespace, 'uptime', host, 'status.json');
638
+ return await storage.get(key);
639
+ } catch (error) {
640
+ return null;
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Associate a scan report with uptime history
646
+ * This creates a link between reconnaissance scans and uptime monitoring
647
+ */
648
+ async linkReportToUptime(host, reportId, reportTimestamp) {
649
+ try {
650
+ const storage = this.plugin.getStorage();
651
+ if (!storage) return;
652
+
653
+ const status = this.getStatus(host);
654
+ if (!status) return;
655
+
656
+ const namespace = this.plugin.namespace || '';
657
+
658
+ // Create a link entry
659
+ const timestamp = reportTimestamp.replace(/[:.]/g, '-');
660
+ const key = storage.getPluginKey(null, namespace, 'uptime', host, 'scans', `${timestamp}.json`);
661
+
662
+ await storage.set(key, {
663
+ host,
664
+ reportId,
665
+ reportTimestamp,
666
+ uptimeStatus: status.status,
667
+ uptimePercentage: status.uptimePercentage,
668
+ consecutiveFails: status.consecutiveFails,
669
+ linkedAt: new Date().toISOString()
670
+ }, { behavior: 'body-only' });
671
+ } catch (error) {
672
+ console.error(`Failed to link report to uptime for ${host}:`, error.message);
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Cleanup - stop all monitoring
678
+ */
679
+ cleanup() {
680
+ // Stop all check intervals
681
+ for (const host of this.checkIntervals.keys()) {
682
+ this.stopMonitoring(host);
683
+ }
684
+
685
+ // Clear all maps
686
+ this.checks.clear();
687
+ this.checkIntervals.clear();
688
+ this.aggregationIntervals.clear();
689
+ this.minuteBuffer.clear();
690
+ }
691
+ }