s3db.js 13.6.1 → 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 +56 -15
- package/dist/s3db.cjs +72446 -39022
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72172 -38790
- 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 +85 -50
- 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/route-context.js +601 -0
- package/src/plugins/api/index.js +168 -40
- 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/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/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,745 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StorageManager
|
|
3
|
+
*
|
|
4
|
+
* Handles all storage operations for the ReconPlugin:
|
|
5
|
+
* - Report persistence to PluginStorage
|
|
6
|
+
* - Resource updates (hosts, reports, diffs, stages, etc.)
|
|
7
|
+
* - History pruning
|
|
8
|
+
* - Diff computation and alerts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getAllResourceConfigs } from '../config/resources.js';
|
|
12
|
+
import {
|
|
13
|
+
getNamespacedResourceName,
|
|
14
|
+
listPluginNamespaces
|
|
15
|
+
} from '../../namespace.js';
|
|
16
|
+
|
|
17
|
+
export class StorageManager {
|
|
18
|
+
constructor(plugin) {
|
|
19
|
+
this.plugin = plugin;
|
|
20
|
+
this.resources = {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* List all existing namespaces in storage
|
|
25
|
+
* Uses standardized plugin namespace detection
|
|
26
|
+
*/
|
|
27
|
+
async listNamespaces() {
|
|
28
|
+
return await listPluginNamespaces(this.plugin.getStorage(), 'recon');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize plugin storage resources
|
|
33
|
+
* Note: Namespace detection is now handled automatically by Plugin base class
|
|
34
|
+
*/
|
|
35
|
+
async initialize() {
|
|
36
|
+
if (!this.plugin.database) {
|
|
37
|
+
return; // No database configured, skip resource creation
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const namespace = this.plugin.namespace || '';
|
|
41
|
+
const resourceConfigs = getAllResourceConfigs();
|
|
42
|
+
|
|
43
|
+
for (const config of resourceConfigs) {
|
|
44
|
+
try {
|
|
45
|
+
// Add namespace to resource name using standardized helper
|
|
46
|
+
const namespacedName = getNamespacedResourceName(config.name, namespace, 'plg_recon');
|
|
47
|
+
|
|
48
|
+
const namespacedConfig = {
|
|
49
|
+
...config,
|
|
50
|
+
name: namespacedName
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Check if resource already exists
|
|
54
|
+
let resource = null;
|
|
55
|
+
try {
|
|
56
|
+
resource = await this.plugin.database.getResource(namespacedConfig.name);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Resource doesn't exist, create it
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!resource) {
|
|
62
|
+
resource = await this.plugin.database.createResource(namespacedConfig);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.resources[config.name] = resource; // Use original name as key
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`Failed to initialize resource ${config.name}:`, error.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get a resource by name
|
|
74
|
+
*/
|
|
75
|
+
getResource(name) {
|
|
76
|
+
return this.resources[name];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract timestampDay from ISO timestamp for partitioning
|
|
81
|
+
*/
|
|
82
|
+
_extractTimestampDay(isoTimestamp) {
|
|
83
|
+
if (!isoTimestamp) return null;
|
|
84
|
+
return isoTimestamp.split('T')[0]; // "2025-01-01T12:00:00.000Z" -> "2025-01-01"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Persist report to PluginStorage with per-tool artifacts
|
|
89
|
+
*/
|
|
90
|
+
async persistReport(target, report) {
|
|
91
|
+
const storage = this.plugin.getStorage();
|
|
92
|
+
const timestamp = report.endedAt.replace(/[:.]/g, '-');
|
|
93
|
+
const namespace = this.plugin.namespace || '';
|
|
94
|
+
const baseKey = storage.getPluginKey(null, namespace, 'reports', target.host);
|
|
95
|
+
const historyKey = `${baseKey}/${timestamp}.json`;
|
|
96
|
+
const stageStorageKeys = {};
|
|
97
|
+
const toolStorageKeys = {};
|
|
98
|
+
|
|
99
|
+
for (const [stageName, stageData] of Object.entries(report.results || {})) {
|
|
100
|
+
// Persist individual tools if present
|
|
101
|
+
if (stageData._individual && typeof stageData._individual === 'object') {
|
|
102
|
+
for (const [toolName, toolData] of Object.entries(stageData._individual)) {
|
|
103
|
+
const toolKey = `${baseKey}/stages/${timestamp}/tools/${toolName}.json`;
|
|
104
|
+
await storage.set(toolKey, toolData, { behavior: 'body-only' });
|
|
105
|
+
toolStorageKeys[toolName] = toolKey;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Persist aggregated stage view
|
|
110
|
+
const aggregatedData = stageData._aggregated || stageData;
|
|
111
|
+
const stageKey = `${baseKey}/stages/${timestamp}/aggregated/${stageName}.json`;
|
|
112
|
+
await storage.set(stageKey, aggregatedData, { behavior: 'body-only' });
|
|
113
|
+
stageStorageKeys[stageName] = stageKey;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
report.stageStorageKeys = stageStorageKeys;
|
|
117
|
+
report.toolStorageKeys = toolStorageKeys;
|
|
118
|
+
report.storageKey = historyKey;
|
|
119
|
+
|
|
120
|
+
await storage.set(historyKey, report, { behavior: 'body-only' });
|
|
121
|
+
await storage.set(`${baseKey}/latest.json`, report, { behavior: 'body-only' });
|
|
122
|
+
|
|
123
|
+
const indexKey = `${baseKey}/index.json`;
|
|
124
|
+
const existing = (await storage.get(indexKey)) || { target: target.host, history: [] };
|
|
125
|
+
|
|
126
|
+
existing.history.unshift({
|
|
127
|
+
timestamp: report.endedAt,
|
|
128
|
+
status: report.status,
|
|
129
|
+
reportKey: historyKey,
|
|
130
|
+
stageKeys: stageStorageKeys,
|
|
131
|
+
toolKeys: toolStorageKeys,
|
|
132
|
+
summary: {
|
|
133
|
+
latencyMs: report.fingerprint.latencyMs ?? null,
|
|
134
|
+
openPorts: report.fingerprint.openPorts?.length ?? 0,
|
|
135
|
+
subdomains: report.fingerprint.subdomainCount ?? 0,
|
|
136
|
+
primaryIp: report.fingerprint.primaryIp ?? null
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let pruned = [];
|
|
141
|
+
if (existing.history.length > this.plugin.config.storage.historyLimit) {
|
|
142
|
+
pruned = existing.history.splice(this.plugin.config.storage.historyLimit);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await storage.set(indexKey, existing, { behavior: 'body-only' });
|
|
146
|
+
|
|
147
|
+
if (pruned.length) {
|
|
148
|
+
await this.pruneHistory(target, pruned);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Persist report data to database resources
|
|
154
|
+
*/
|
|
155
|
+
async persistToResources(report) {
|
|
156
|
+
if (!this.plugin.database || !this.plugin.config.resources.persist) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const hostId = report.target.host;
|
|
161
|
+
const hostsResource = await this.plugin._getResource('hosts');
|
|
162
|
+
const stagesResource = await this.plugin._getResource('stages');
|
|
163
|
+
const reportsResource = await this.plugin._getResource('reports');
|
|
164
|
+
const subdomainsResource = await this.plugin._getResource('subdomains');
|
|
165
|
+
const pathsResource = await this.plugin._getResource('paths');
|
|
166
|
+
|
|
167
|
+
// Update hosts resource
|
|
168
|
+
if (hostsResource) {
|
|
169
|
+
let existing = null;
|
|
170
|
+
try {
|
|
171
|
+
existing = await hostsResource.get(hostId);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
existing = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const hostRecord = this._buildHostRecord(report);
|
|
177
|
+
|
|
178
|
+
if (existing) {
|
|
179
|
+
try {
|
|
180
|
+
await hostsResource.update(hostId, hostRecord);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (typeof hostsResource.replace === 'function') {
|
|
183
|
+
await hostsResource.replace(hostId, hostRecord);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
try {
|
|
188
|
+
await hostsResource.insert(hostRecord);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (typeof hostsResource.replace === 'function') {
|
|
191
|
+
await hostsResource.replace(hostRecord.id, hostRecord);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Compute and save diffs
|
|
197
|
+
const diffs = this._computeDiffs(existing, report);
|
|
198
|
+
if (diffs.length) {
|
|
199
|
+
await this.saveDiffs(hostId, report.endedAt, diffs);
|
|
200
|
+
await this._emitDiffAlerts(hostId, report, diffs);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Update subdomains resource
|
|
205
|
+
if (subdomainsResource) {
|
|
206
|
+
const list = Array.isArray(report.results?.subdomains?.list) ? report.results.subdomains.list : [];
|
|
207
|
+
const subdomainRecord = {
|
|
208
|
+
id: hostId,
|
|
209
|
+
host: hostId,
|
|
210
|
+
subdomains: list,
|
|
211
|
+
total: list.length,
|
|
212
|
+
sources: this._stripRawFields(report.results?.subdomains?.sources || {}),
|
|
213
|
+
lastScanAt: report.endedAt
|
|
214
|
+
};
|
|
215
|
+
await this._upsertResourceRecord(subdomainsResource, subdomainRecord);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update paths resource
|
|
219
|
+
if (pathsResource) {
|
|
220
|
+
const pathStage = report.results?.webDiscovery || {};
|
|
221
|
+
const paths = Array.isArray(pathStage.paths) ? pathStage.paths : [];
|
|
222
|
+
const pathRecord = {
|
|
223
|
+
id: hostId,
|
|
224
|
+
host: hostId,
|
|
225
|
+
paths,
|
|
226
|
+
total: paths.length,
|
|
227
|
+
sources: this._stripRawFields(pathStage.tools || pathStage.sources || {}),
|
|
228
|
+
lastScanAt: report.endedAt
|
|
229
|
+
};
|
|
230
|
+
await this._upsertResourceRecord(pathsResource, pathRecord);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Update reports resource
|
|
234
|
+
if (reportsResource) {
|
|
235
|
+
const timestamp = report.timestamp || report.endedAt || new Date().toISOString();
|
|
236
|
+
const reportRecord = {
|
|
237
|
+
id: report.id || `${hostId}|${timestamp}`,
|
|
238
|
+
reportId: report.id || `rpt_${Date.now()}`,
|
|
239
|
+
target: report.target || { host: hostId, original: hostId },
|
|
240
|
+
timestamp,
|
|
241
|
+
timestampDay: this._extractTimestampDay(timestamp),
|
|
242
|
+
duration: report.duration || 0,
|
|
243
|
+
status: report.status || 'completed',
|
|
244
|
+
results: report.results || {},
|
|
245
|
+
fingerprint: report.fingerprint || {},
|
|
246
|
+
summary: {
|
|
247
|
+
totalIPs: report.fingerprint?.infrastructure?.ips?.ipv4?.length || 0,
|
|
248
|
+
totalPorts: report.fingerprint?.attackSurface?.openPorts?.length || 0,
|
|
249
|
+
totalSubdomains: report.fingerprint?.attackSurface?.subdomains?.total || 0,
|
|
250
|
+
totalPaths: report.fingerprint?.attackSurface?.discoveredPaths?.total || 0,
|
|
251
|
+
detectedTechnologies: report.fingerprint?.technologies?.detected?.length || 0,
|
|
252
|
+
riskLevel: report.riskLevel || 'low'
|
|
253
|
+
},
|
|
254
|
+
uptime: report.uptime || null // Include uptime status from scan time
|
|
255
|
+
};
|
|
256
|
+
try {
|
|
257
|
+
await reportsResource.insert(reportRecord);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
try {
|
|
260
|
+
await reportsResource.update(reportRecord.id, reportRecord);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (typeof reportsResource.replace === 'function') {
|
|
263
|
+
await reportsResource.replace(reportRecord.id, reportRecord);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update stages resource
|
|
270
|
+
if (stagesResource && report.results) {
|
|
271
|
+
const reportTimestamp = report.timestamp || report.endedAt || new Date().toISOString();
|
|
272
|
+
|
|
273
|
+
for (const [stageName, stageData] of Object.entries(report.results || {})) {
|
|
274
|
+
const stageRecord = {
|
|
275
|
+
id: `${hostId}|${stageName}|${reportTimestamp}`,
|
|
276
|
+
reportId: report.id || `rpt_${Date.now()}`,
|
|
277
|
+
stageName,
|
|
278
|
+
host: hostId,
|
|
279
|
+
timestamp: reportTimestamp,
|
|
280
|
+
timestampDay: this._extractTimestampDay(reportTimestamp),
|
|
281
|
+
duration: stageData?.duration || 0,
|
|
282
|
+
status: stageData?.status || 'unknown',
|
|
283
|
+
toolsUsed: this._extractToolNames(stageData, 'all'),
|
|
284
|
+
toolsSucceeded: this._extractToolNames(stageData, 'succeeded'),
|
|
285
|
+
toolsFailed: this._extractToolNames(stageData, 'failed'),
|
|
286
|
+
resultCount: this._countResults(stageData),
|
|
287
|
+
errorMessage: stageData?.error || null
|
|
288
|
+
};
|
|
289
|
+
try {
|
|
290
|
+
await stagesResource.insert(stageRecord);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
try {
|
|
293
|
+
await stagesResource.update(stageRecord.id, stageRecord);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (typeof stagesResource.replace === 'function') {
|
|
296
|
+
await stagesResource.replace(stageRecord.id, stageRecord);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Prune old history entries
|
|
306
|
+
*/
|
|
307
|
+
async pruneHistory(target, pruned) {
|
|
308
|
+
const storage = this.plugin.getStorage();
|
|
309
|
+
for (const entry of pruned) {
|
|
310
|
+
try {
|
|
311
|
+
await storage.delete(entry.reportKey);
|
|
312
|
+
for (const stageKey of Object.values(entry.stageKeys || {})) {
|
|
313
|
+
await storage.delete(stageKey);
|
|
314
|
+
}
|
|
315
|
+
for (const toolKey of Object.values(entry.toolKeys || {})) {
|
|
316
|
+
await storage.delete(toolKey);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
// Ignore deletion errors
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Load latest report from storage
|
|
326
|
+
*/
|
|
327
|
+
async loadLatestReport(hostId) {
|
|
328
|
+
const storage = this.plugin.getStorage();
|
|
329
|
+
const baseKey = storage.getPluginKey(null, 'reports', hostId);
|
|
330
|
+
try {
|
|
331
|
+
return await storage.get(`${baseKey}/latest.json`);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Load host summary from database or build from report
|
|
339
|
+
*/
|
|
340
|
+
async loadHostSummary(hostId, report) {
|
|
341
|
+
if (!this.plugin.database || !this.plugin.config.resources.persist) {
|
|
342
|
+
return this._buildHostRecord(report);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const hostsResource = await this.plugin._getResource('hosts');
|
|
347
|
+
if (!hostsResource) {
|
|
348
|
+
return this._buildHostRecord(report);
|
|
349
|
+
}
|
|
350
|
+
return await hostsResource.get(hostId);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return this._buildHostRecord(report);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Save diffs to database
|
|
358
|
+
*/
|
|
359
|
+
async saveDiffs(hostId, timestamp, diffs) {
|
|
360
|
+
if (!this.plugin.database || !this.plugin.config.resources.persist) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const diffsResource = await this.plugin._getResource('diffs');
|
|
366
|
+
if (!diffsResource) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const diffRecord = {
|
|
371
|
+
id: `${hostId}|${timestamp}`,
|
|
372
|
+
host: hostId,
|
|
373
|
+
timestamp,
|
|
374
|
+
changes: diffs
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
await diffsResource.insert(diffRecord);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
try {
|
|
381
|
+
await diffsResource.update(diffRecord.id, diffRecord);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (typeof diffsResource.replace === 'function') {
|
|
384
|
+
await diffsResource.replace(diffRecord.id, diffRecord);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
// Ignore errors
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Load recent diffs for a host
|
|
395
|
+
*/
|
|
396
|
+
async loadRecentDiffs(hostId, limit = 10) {
|
|
397
|
+
if (!this.plugin.database || !this.plugin.config.resources.persist) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const diffsResource = await this.plugin._getResource('diffs');
|
|
403
|
+
if (!diffsResource) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const allDiffs = await diffsResource.query({ host: hostId });
|
|
408
|
+
const sorted = allDiffs
|
|
409
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
|
410
|
+
.slice(0, limit);
|
|
411
|
+
|
|
412
|
+
const flattened = [];
|
|
413
|
+
for (const entry of sorted) {
|
|
414
|
+
if (Array.isArray(entry.changes)) {
|
|
415
|
+
for (const change of entry.changes) {
|
|
416
|
+
flattened.push({
|
|
417
|
+
...change,
|
|
418
|
+
timestamp: entry.timestamp,
|
|
419
|
+
host: entry.host
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return flattened.slice(0, limit);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
// Try plugin storage as fallback
|
|
428
|
+
try {
|
|
429
|
+
const storage = this.plugin.getStorage();
|
|
430
|
+
const baseKey = storage.getPluginKey(null, 'reports', hostId);
|
|
431
|
+
const indexKey = `${baseKey}/index.json`;
|
|
432
|
+
const index = await storage.get(indexKey);
|
|
433
|
+
|
|
434
|
+
if (index?.history) {
|
|
435
|
+
const diffs = [];
|
|
436
|
+
for (const entry of index.history.slice(0, limit)) {
|
|
437
|
+
const report = await storage.get(entry.reportKey);
|
|
438
|
+
if (report?.diffs) {
|
|
439
|
+
diffs.push(...report.diffs.map(d => ({
|
|
440
|
+
...d,
|
|
441
|
+
timestamp: entry.timestamp,
|
|
442
|
+
host: hostId
|
|
443
|
+
})));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return diffs.slice(0, limit);
|
|
447
|
+
}
|
|
448
|
+
} catch (err) {
|
|
449
|
+
// Ignore
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ========================================
|
|
457
|
+
// Private Helper Methods
|
|
458
|
+
// ========================================
|
|
459
|
+
|
|
460
|
+
_buildHostRecord(report) {
|
|
461
|
+
const fingerprint = report.fingerprint || {};
|
|
462
|
+
const summary = {
|
|
463
|
+
target: report.target.original,
|
|
464
|
+
primaryIp: fingerprint.primaryIp || null,
|
|
465
|
+
ipAddresses: fingerprint.ipAddresses || [],
|
|
466
|
+
cdn: fingerprint.cdn || null,
|
|
467
|
+
server: fingerprint.server || null,
|
|
468
|
+
latencyMs: fingerprint.latencyMs ?? null,
|
|
469
|
+
subdomains: fingerprint.subdomains || [],
|
|
470
|
+
subdomainCount: (fingerprint.subdomains || []).length,
|
|
471
|
+
openPorts: fingerprint.openPorts || [],
|
|
472
|
+
openPortCount: (fingerprint.openPorts || []).length,
|
|
473
|
+
technologies: fingerprint.technologies || []
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
id: report.target.host,
|
|
478
|
+
target: report.target.original,
|
|
479
|
+
summary,
|
|
480
|
+
fingerprint,
|
|
481
|
+
lastScanAt: report.endedAt,
|
|
482
|
+
storageKey: report.storageKey || null
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
_computeDiffs(existingRecord, report) {
|
|
487
|
+
const diffs = [];
|
|
488
|
+
const prevFingerprint = existingRecord?.fingerprint || {};
|
|
489
|
+
const currFingerprint = report.fingerprint || {};
|
|
490
|
+
|
|
491
|
+
// Subdomain diffs
|
|
492
|
+
const prevSubs = new Set(prevFingerprint.subdomains || []);
|
|
493
|
+
const currSubs = new Set(currFingerprint.subdomains || (report.results?.subdomains?.list || []));
|
|
494
|
+
const addedSubs = [...currSubs].filter((value) => !prevSubs.has(value));
|
|
495
|
+
const removedSubs = [...prevSubs].filter((value) => !currSubs.has(value));
|
|
496
|
+
|
|
497
|
+
if (addedSubs.length) {
|
|
498
|
+
diffs.push(this._createDiff('subdomain:add', {
|
|
499
|
+
values: addedSubs,
|
|
500
|
+
description: `Novos subdomínios: ${addedSubs.join(', ')}`
|
|
501
|
+
}, { severity: 'medium', critical: false }));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (removedSubs.length) {
|
|
505
|
+
diffs.push(this._createDiff('subdomain:remove', {
|
|
506
|
+
values: removedSubs,
|
|
507
|
+
description: `Subdomínios removidos: ${removedSubs.join(', ')}`
|
|
508
|
+
}, { severity: 'low', critical: false }));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Port diffs
|
|
512
|
+
const normalizePort = (entry) => {
|
|
513
|
+
if (!entry) return entry;
|
|
514
|
+
if (typeof entry === 'string') return entry;
|
|
515
|
+
return entry.port || `${entry.service || 'unknown'}`;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const prevPorts = new Set((prevFingerprint.openPorts || []).map(normalizePort));
|
|
519
|
+
const currPorts = new Set((currFingerprint.openPorts || []).map(normalizePort));
|
|
520
|
+
const addedPorts = [...currPorts].filter((value) => !prevPorts.has(value));
|
|
521
|
+
const removedPorts = [...prevPorts].filter((value) => !currPorts.has(value));
|
|
522
|
+
|
|
523
|
+
if (addedPorts.length) {
|
|
524
|
+
diffs.push(this._createDiff('port:add', {
|
|
525
|
+
values: addedPorts,
|
|
526
|
+
description: `Novas portas expostas: ${addedPorts.join(', ')}`
|
|
527
|
+
}, { severity: 'high', critical: true }));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (removedPorts.length) {
|
|
531
|
+
diffs.push(this._createDiff('port:remove', {
|
|
532
|
+
values: removedPorts,
|
|
533
|
+
description: `Portas fechadas: ${removedPorts.join(', ')}`
|
|
534
|
+
}, { severity: 'low', critical: false }));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Technology diffs
|
|
538
|
+
const prevTech = new Set((prevFingerprint.technologies || []).map((tech) => tech.toLowerCase()));
|
|
539
|
+
const currTech = new Set((currFingerprint.technologies || []).map((tech) => tech.toLowerCase()));
|
|
540
|
+
const addedTech = [...currTech].filter((value) => !prevTech.has(value));
|
|
541
|
+
const removedTech = [...prevTech].filter((value) => !currTech.has(value));
|
|
542
|
+
|
|
543
|
+
if (addedTech.length) {
|
|
544
|
+
diffs.push(this._createDiff('technology:add', {
|
|
545
|
+
values: addedTech,
|
|
546
|
+
description: `Tecnologias adicionadas: ${addedTech.join(', ')}`
|
|
547
|
+
}, { severity: 'medium', critical: false }));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (removedTech.length) {
|
|
551
|
+
diffs.push(this._createDiff('technology:remove', {
|
|
552
|
+
values: removedTech,
|
|
553
|
+
description: `Tecnologias removidas: ${removedTech.join(', ')}`
|
|
554
|
+
}, { severity: 'low', critical: false }));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Primitive field diffs
|
|
558
|
+
const primitiveFields = ['primaryIp', 'cdn', 'server'];
|
|
559
|
+
for (const field of primitiveFields) {
|
|
560
|
+
const previous = prevFingerprint[field] ?? null;
|
|
561
|
+
const current = currFingerprint[field] ?? null;
|
|
562
|
+
if (previous !== current) {
|
|
563
|
+
const severity = field === 'primaryIp' ? 'high' : 'medium';
|
|
564
|
+
const critical = field === 'primaryIp';
|
|
565
|
+
diffs.push(this._createDiff(`field:${field}`, {
|
|
566
|
+
previous,
|
|
567
|
+
current,
|
|
568
|
+
description: `${field} alterado de ${previous || 'desconhecido'} para ${current || 'desconhecido'}`
|
|
569
|
+
}, { severity, critical }));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return diffs;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
_createDiff(type, data, meta = {}) {
|
|
577
|
+
return {
|
|
578
|
+
type,
|
|
579
|
+
...data,
|
|
580
|
+
severity: meta.severity || 'info',
|
|
581
|
+
critical: meta.critical === true,
|
|
582
|
+
detectedAt: new Date().toISOString()
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async _emitDiffAlerts(hostId, report, diffs) {
|
|
587
|
+
for (const diff of diffs) {
|
|
588
|
+
this.plugin.emit('recon:alert', {
|
|
589
|
+
host: hostId,
|
|
590
|
+
stage: diff.type?.split(':')[0] || 'unknown',
|
|
591
|
+
severity: diff.severity,
|
|
592
|
+
critical: diff.critical,
|
|
593
|
+
description: diff.description,
|
|
594
|
+
values: diff.values,
|
|
595
|
+
timestamp: report.endedAt
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
_summarizeStage(stageName, stageData) {
|
|
601
|
+
const summary = {};
|
|
602
|
+
|
|
603
|
+
if (stageData.status) {
|
|
604
|
+
summary.status = stageData.status;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
switch (stageName) {
|
|
608
|
+
case 'dns':
|
|
609
|
+
if (stageData.records) {
|
|
610
|
+
summary.recordTypes = Object.keys(stageData.records).filter(k => k !== 'reverse');
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
case 'ports':
|
|
614
|
+
if (stageData.openPorts) {
|
|
615
|
+
summary.openPortCount = stageData.openPorts.length;
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
case 'subdomains':
|
|
619
|
+
if (stageData.total !== undefined) {
|
|
620
|
+
summary.totalSubdomains = stageData.total;
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
case 'webDiscovery':
|
|
624
|
+
if (stageData.paths) {
|
|
625
|
+
summary.pathCount = stageData.paths.length;
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
default:
|
|
629
|
+
if (stageData.tools) {
|
|
630
|
+
summary.toolStatuses = Object.fromEntries(
|
|
631
|
+
Object.entries(stageData.tools).map(([tool, data]) => [tool, data.status])
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return summary;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
_stripRawFields(obj) {
|
|
640
|
+
if (!obj || typeof obj !== 'object') {
|
|
641
|
+
return obj;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const cleaned = {};
|
|
645
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
646
|
+
if (value && typeof value === 'object') {
|
|
647
|
+
const { raw, ...rest } = value;
|
|
648
|
+
cleaned[key] = rest;
|
|
649
|
+
} else {
|
|
650
|
+
cleaned[key] = value;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return cleaned;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async _upsertResourceRecord(resource, record) {
|
|
657
|
+
if (!resource) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
await resource.insert(record);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
// fallthrough to update/replace
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const methods = ['update', 'replace'];
|
|
668
|
+
for (const method of methods) {
|
|
669
|
+
if (typeof resource[method] !== 'function') {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
await resource[method](record.id, record);
|
|
674
|
+
return;
|
|
675
|
+
} catch (error) {
|
|
676
|
+
// try next
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Extract tool names from stage data
|
|
683
|
+
*/
|
|
684
|
+
_extractToolNames(stageData, filter = 'all') {
|
|
685
|
+
if (!stageData) return [];
|
|
686
|
+
|
|
687
|
+
const tools = [];
|
|
688
|
+
|
|
689
|
+
// Check for _individual tools
|
|
690
|
+
if (stageData._individual && typeof stageData._individual === 'object') {
|
|
691
|
+
for (const [toolName, toolData] of Object.entries(stageData._individual)) {
|
|
692
|
+
const status = toolData?.status;
|
|
693
|
+
|
|
694
|
+
if (filter === 'all') {
|
|
695
|
+
tools.push(toolName);
|
|
696
|
+
} else if (filter === 'succeeded' && status === 'ok') {
|
|
697
|
+
tools.push(toolName);
|
|
698
|
+
} else if (filter === 'failed' && status !== 'ok' && status) {
|
|
699
|
+
tools.push(toolName);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Check for tools object (legacy format)
|
|
705
|
+
if (stageData.tools && typeof stageData.tools === 'object') {
|
|
706
|
+
for (const [toolName, toolData] of Object.entries(stageData.tools)) {
|
|
707
|
+
const status = toolData?.status;
|
|
708
|
+
|
|
709
|
+
if (filter === 'all' && !tools.includes(toolName)) {
|
|
710
|
+
tools.push(toolName);
|
|
711
|
+
} else if (filter === 'succeeded' && status === 'ok' && !tools.includes(toolName)) {
|
|
712
|
+
tools.push(toolName);
|
|
713
|
+
} else if (filter === 'failed' && status !== 'ok' && status && !tools.includes(toolName)) {
|
|
714
|
+
tools.push(toolName);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return tools;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Count results in stage data
|
|
724
|
+
*/
|
|
725
|
+
_countResults(stageData) {
|
|
726
|
+
if (!stageData) return 0;
|
|
727
|
+
|
|
728
|
+
// Try common result fields
|
|
729
|
+
if (stageData.openPorts?.length) return stageData.openPorts.length;
|
|
730
|
+
if (stageData.list?.length) return stageData.list.length; // subdomains
|
|
731
|
+
if (stageData.paths?.length) return stageData.paths.length;
|
|
732
|
+
if (stageData.records && typeof stageData.records === 'object') {
|
|
733
|
+
return Object.values(stageData.records).flat().length; // DNS records
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Count _aggregated results
|
|
737
|
+
if (stageData._aggregated) {
|
|
738
|
+
if (stageData._aggregated.openPorts?.length) return stageData._aggregated.openPorts.length;
|
|
739
|
+
if (stageData._aggregated.list?.length) return stageData._aggregated.list.length;
|
|
740
|
+
if (stageData._aggregated.paths?.length) return stageData._aggregated.paths.length;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
}
|