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.
- package/README.md +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- 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
|
+
}
|