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.
Files changed (189) hide show
  1. package/README.md +56 -15
  2. package/dist/s3db.cjs +72446 -39022
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72172 -38790
  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 +85 -50
  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/route-context.js +601 -0
  34. package/src/plugins/api/index.js +168 -40
  35. package/src/plugins/api/routes/auth-routes.js +198 -30
  36. package/src/plugins/api/routes/resource-routes.js +19 -4
  37. package/src/plugins/api/server/health-manager.class.js +163 -0
  38. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  39. package/src/plugins/api/server/router.class.js +472 -0
  40. package/src/plugins/api/server.js +280 -1303
  41. package/src/plugins/api/utils/custom-routes.js +17 -5
  42. package/src/plugins/api/utils/guards.js +76 -17
  43. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  44. package/src/plugins/api/utils/openapi-generator.js +7 -6
  45. package/src/plugins/audit.plugin.js +30 -8
  46. package/src/plugins/backup.plugin.js +110 -14
  47. package/src/plugins/cache/cache.class.js +22 -5
  48. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  49. package/src/plugins/cache/memory-cache.class.js +211 -57
  50. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  51. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  52. package/src/plugins/cache/redis-cache.class.js +552 -0
  53. package/src/plugins/cache/s3-cache.class.js +17 -8
  54. package/src/plugins/cache.plugin.js +176 -61
  55. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  56. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  57. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  58. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  59. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  60. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  62. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/index.js +29 -8
  66. package/src/plugins/cloud-inventory/registry.js +64 -42
  67. package/src/plugins/cloud-inventory.plugin.js +240 -138
  68. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  69. package/src/plugins/concerns/resource-names.js +100 -0
  70. package/src/plugins/consumers/index.js +10 -2
  71. package/src/plugins/consumers/sqs-consumer.js +12 -2
  72. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  73. package/src/plugins/cookie-farm.errors.js +73 -0
  74. package/src/plugins/cookie-farm.plugin.js +869 -0
  75. package/src/plugins/costs.plugin.js +7 -1
  76. package/src/plugins/eventual-consistency/analytics.js +94 -19
  77. package/src/plugins/eventual-consistency/config.js +15 -7
  78. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  79. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  80. package/src/plugins/eventual-consistency/helpers.js +39 -14
  81. package/src/plugins/eventual-consistency/install.js +21 -2
  82. package/src/plugins/eventual-consistency/utils.js +32 -10
  83. package/src/plugins/fulltext.plugin.js +38 -11
  84. package/src/plugins/geo.plugin.js +61 -9
  85. package/src/plugins/identity/concerns/config.js +61 -0
  86. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  87. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  88. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  89. package/src/plugins/identity/concerns/token-generator.js +29 -4
  90. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  91. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  92. package/src/plugins/identity/drivers/index.js +18 -0
  93. package/src/plugins/identity/drivers/password-driver.js +122 -0
  94. package/src/plugins/identity/email-service.js +17 -2
  95. package/src/plugins/identity/index.js +413 -69
  96. package/src/plugins/identity/oauth2-server.js +413 -30
  97. package/src/plugins/identity/oidc-discovery.js +16 -8
  98. package/src/plugins/identity/rsa-keys.js +115 -35
  99. package/src/plugins/identity/server.js +166 -45
  100. package/src/plugins/identity/session-manager.js +53 -7
  101. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  102. package/src/plugins/identity/ui/routes.js +363 -255
  103. package/src/plugins/importer/index.js +153 -20
  104. package/src/plugins/index.js +9 -2
  105. package/src/plugins/kubernetes-inventory/index.js +6 -0
  106. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  107. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  108. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  109. package/src/plugins/metrics.plugin.js +64 -16
  110. package/src/plugins/ml/base-model.class.js +25 -15
  111. package/src/plugins/ml/regression-model.class.js +1 -1
  112. package/src/plugins/ml.errors.js +57 -25
  113. package/src/plugins/ml.plugin.js +28 -4
  114. package/src/plugins/namespace.js +210 -0
  115. package/src/plugins/plugin.class.js +180 -8
  116. package/src/plugins/puppeteer/console-monitor.js +729 -0
  117. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  118. package/src/plugins/puppeteer/network-monitor.js +816 -0
  119. package/src/plugins/puppeteer/performance-manager.js +746 -0
  120. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  121. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  122. package/src/plugins/puppeteer.errors.js +81 -0
  123. package/src/plugins/puppeteer.plugin.js +1327 -0
  124. package/src/plugins/queue-consumer.plugin.js +69 -14
  125. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  126. package/src/plugins/recon/concerns/command-runner.js +148 -0
  127. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  128. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  129. package/src/plugins/recon/concerns/process-manager.js +338 -0
  130. package/src/plugins/recon/concerns/report-generator.js +478 -0
  131. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  132. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  133. package/src/plugins/recon/config/defaults.js +321 -0
  134. package/src/plugins/recon/config/resources.js +370 -0
  135. package/src/plugins/recon/index.js +778 -0
  136. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  137. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  138. package/src/plugins/recon/managers/storage-manager.js +745 -0
  139. package/src/plugins/recon/managers/target-manager.js +274 -0
  140. package/src/plugins/recon/stages/asn-stage.js +314 -0
  141. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  142. package/src/plugins/recon/stages/dns-stage.js +107 -0
  143. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  144. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  145. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  146. package/src/plugins/recon/stages/http-stage.js +89 -0
  147. package/src/plugins/recon/stages/latency-stage.js +148 -0
  148. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  149. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  150. package/src/plugins/recon/stages/ports-stage.js +169 -0
  151. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  152. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  153. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  154. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  155. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  156. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  157. package/src/plugins/recon/stages/whois-stage.js +349 -0
  158. package/src/plugins/recon.plugin.js +75 -0
  159. package/src/plugins/recon.plugin.js.backup +2635 -0
  160. package/src/plugins/relation.errors.js +87 -14
  161. package/src/plugins/replicator.plugin.js +514 -137
  162. package/src/plugins/replicators/base-replicator.class.js +89 -1
  163. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  164. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  165. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  166. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  167. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  168. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  169. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  170. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  171. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  172. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  173. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  174. package/src/plugins/s3-queue.plugin.js +464 -65
  175. package/src/plugins/scheduler.plugin.js +20 -6
  176. package/src/plugins/state-machine.plugin.js +40 -9
  177. package/src/plugins/tfstate/base-driver.js +28 -4
  178. package/src/plugins/tfstate/errors.js +65 -10
  179. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  180. package/src/plugins/tfstate/index.js +163 -90
  181. package/src/plugins/tfstate/s3-driver.js +64 -6
  182. package/src/plugins/ttl.plugin.js +72 -17
  183. package/src/plugins/vector/distances.js +18 -12
  184. package/src/plugins/vector/kmeans.js +26 -4
  185. package/src/resource.class.js +115 -19
  186. package/src/testing/factory.class.js +20 -3
  187. package/src/testing/seeder.class.js +7 -1
  188. package/src/clients/memory-client.md +0 -917
  189. 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
+ }