s3db.js 13.6.0 → 14.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +139 -43
  2. package/dist/s3db.cjs +72425 -38970
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72177 -38764
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +94 -49
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  34. package/src/plugins/api/concerns/route-context.js +601 -0
  35. package/src/plugins/api/concerns/state-machine.js +288 -0
  36. package/src/plugins/api/index.js +180 -41
  37. package/src/plugins/api/routes/auth-routes.js +198 -30
  38. package/src/plugins/api/routes/resource-routes.js +19 -4
  39. package/src/plugins/api/server/health-manager.class.js +163 -0
  40. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  41. package/src/plugins/api/server/router.class.js +472 -0
  42. package/src/plugins/api/server.js +280 -1303
  43. package/src/plugins/api/utils/custom-routes.js +17 -5
  44. package/src/plugins/api/utils/guards.js +76 -17
  45. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  46. package/src/plugins/api/utils/openapi-generator.js +7 -6
  47. package/src/plugins/api/utils/template-engine.js +77 -3
  48. package/src/plugins/audit.plugin.js +30 -8
  49. package/src/plugins/backup.plugin.js +110 -14
  50. package/src/plugins/cache/cache.class.js +22 -5
  51. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  52. package/src/plugins/cache/memory-cache.class.js +211 -57
  53. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  54. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  55. package/src/plugins/cache/redis-cache.class.js +552 -0
  56. package/src/plugins/cache/s3-cache.class.js +17 -8
  57. package/src/plugins/cache.plugin.js +176 -61
  58. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  59. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  60. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  62. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  66. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  67. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  68. package/src/plugins/cloud-inventory/index.js +29 -8
  69. package/src/plugins/cloud-inventory/registry.js +64 -42
  70. package/src/plugins/cloud-inventory.plugin.js +240 -138
  71. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  72. package/src/plugins/concerns/resource-names.js +100 -0
  73. package/src/plugins/consumers/index.js +10 -2
  74. package/src/plugins/consumers/sqs-consumer.js +12 -2
  75. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  76. package/src/plugins/cookie-farm.errors.js +73 -0
  77. package/src/plugins/cookie-farm.plugin.js +869 -0
  78. package/src/plugins/costs.plugin.js +7 -1
  79. package/src/plugins/eventual-consistency/analytics.js +94 -19
  80. package/src/plugins/eventual-consistency/config.js +15 -7
  81. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  82. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  83. package/src/plugins/eventual-consistency/helpers.js +39 -14
  84. package/src/plugins/eventual-consistency/install.js +21 -2
  85. package/src/plugins/eventual-consistency/utils.js +32 -10
  86. package/src/plugins/fulltext.plugin.js +38 -11
  87. package/src/plugins/geo.plugin.js +61 -9
  88. package/src/plugins/identity/concerns/config.js +61 -0
  89. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  90. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  91. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  92. package/src/plugins/identity/concerns/token-generator.js +29 -4
  93. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  94. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  95. package/src/plugins/identity/drivers/index.js +18 -0
  96. package/src/plugins/identity/drivers/password-driver.js +122 -0
  97. package/src/plugins/identity/email-service.js +17 -2
  98. package/src/plugins/identity/index.js +413 -69
  99. package/src/plugins/identity/oauth2-server.js +413 -30
  100. package/src/plugins/identity/oidc-discovery.js +16 -8
  101. package/src/plugins/identity/rsa-keys.js +115 -35
  102. package/src/plugins/identity/server.js +166 -45
  103. package/src/plugins/identity/session-manager.js +53 -7
  104. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  105. package/src/plugins/identity/ui/routes.js +363 -255
  106. package/src/plugins/importer/index.js +153 -20
  107. package/src/plugins/index.js +9 -2
  108. package/src/plugins/kubernetes-inventory/index.js +6 -0
  109. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  110. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  111. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  112. package/src/plugins/metrics.plugin.js +64 -16
  113. package/src/plugins/ml/base-model.class.js +25 -15
  114. package/src/plugins/ml/regression-model.class.js +1 -1
  115. package/src/plugins/ml.errors.js +57 -25
  116. package/src/plugins/ml.plugin.js +28 -4
  117. package/src/plugins/namespace.js +210 -0
  118. package/src/plugins/plugin.class.js +180 -8
  119. package/src/plugins/puppeteer/console-monitor.js +729 -0
  120. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  121. package/src/plugins/puppeteer/network-monitor.js +816 -0
  122. package/src/plugins/puppeteer/performance-manager.js +746 -0
  123. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  124. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  125. package/src/plugins/puppeteer.errors.js +81 -0
  126. package/src/plugins/puppeteer.plugin.js +1327 -0
  127. package/src/plugins/queue-consumer.plugin.js +69 -14
  128. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  129. package/src/plugins/recon/concerns/command-runner.js +148 -0
  130. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  131. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  132. package/src/plugins/recon/concerns/process-manager.js +338 -0
  133. package/src/plugins/recon/concerns/report-generator.js +478 -0
  134. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  135. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  136. package/src/plugins/recon/config/defaults.js +321 -0
  137. package/src/plugins/recon/config/resources.js +370 -0
  138. package/src/plugins/recon/index.js +778 -0
  139. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  140. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  141. package/src/plugins/recon/managers/storage-manager.js +745 -0
  142. package/src/plugins/recon/managers/target-manager.js +274 -0
  143. package/src/plugins/recon/stages/asn-stage.js +314 -0
  144. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  145. package/src/plugins/recon/stages/dns-stage.js +107 -0
  146. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  147. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  148. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  149. package/src/plugins/recon/stages/http-stage.js +89 -0
  150. package/src/plugins/recon/stages/latency-stage.js +148 -0
  151. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  152. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  153. package/src/plugins/recon/stages/ports-stage.js +169 -0
  154. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  155. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  156. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  157. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  158. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  159. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  160. package/src/plugins/recon/stages/whois-stage.js +349 -0
  161. package/src/plugins/recon.plugin.js +75 -0
  162. package/src/plugins/recon.plugin.js.backup +2635 -0
  163. package/src/plugins/relation.errors.js +87 -14
  164. package/src/plugins/replicator.plugin.js +514 -137
  165. package/src/plugins/replicators/base-replicator.class.js +89 -1
  166. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  167. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  168. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  169. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  170. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  171. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  172. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  173. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  174. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  175. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  176. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  177. package/src/plugins/s3-queue.plugin.js +464 -65
  178. package/src/plugins/scheduler.plugin.js +20 -6
  179. package/src/plugins/state-machine.plugin.js +40 -9
  180. package/src/plugins/tfstate/README.md +126 -126
  181. package/src/plugins/tfstate/base-driver.js +28 -4
  182. package/src/plugins/tfstate/errors.js +65 -10
  183. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  184. package/src/plugins/tfstate/index.js +163 -90
  185. package/src/plugins/tfstate/s3-driver.js +64 -6
  186. package/src/plugins/ttl.plugin.js +72 -17
  187. package/src/plugins/vector/distances.js +18 -12
  188. package/src/plugins/vector/kmeans.js +26 -4
  189. package/src/resource.class.js +115 -19
  190. package/src/testing/factory.class.js +20 -3
  191. package/src/testing/seeder.class.js +7 -1
  192. package/src/clients/memory-client.md +0 -917
  193. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -0,0 +1,107 @@
1
+ /**
2
+ * DnsStage
3
+ *
4
+ * DNS enumeration stage:
5
+ * - A, AAAA, NS, MX, TXT records
6
+ * - Reverse DNS lookups
7
+ * - Error tracking per record type
8
+ */
9
+
10
+ import dns from 'dns/promises';
11
+
12
+ export class DnsStage {
13
+ constructor(plugin) {
14
+ this.plugin = plugin;
15
+ }
16
+
17
+ async execute(target) {
18
+ const result = {
19
+ status: 'ok',
20
+ records: {},
21
+ errors: {}
22
+ };
23
+
24
+ try {
25
+ const lookups = await Promise.allSettled([
26
+ dns.lookup(target.host, { all: true }),
27
+ dns.resolve4(target.host),
28
+ dns.resolve6(target.host).catch(() => []),
29
+ dns.resolveNs(target.host).catch(() => []),
30
+ dns.resolveMx(target.host).catch(() => []),
31
+ dns.resolveTxt(target.host).catch(() => [])
32
+ ]);
33
+
34
+ const [lookupAll, aRecords, aaaaRecords, nsRecords, mxRecords, txtRecords] = lookups;
35
+
36
+ if (lookupAll.status === 'fulfilled') {
37
+ result.records.lookup = lookupAll.value;
38
+ } else {
39
+ result.errors.lookup = lookupAll.reason?.message;
40
+ }
41
+
42
+ result.records.a = aRecords.status === 'fulfilled' ? aRecords.value : [];
43
+ if (aRecords.status === 'rejected') {
44
+ result.errors.a = aRecords.reason?.message;
45
+ }
46
+
47
+ result.records.aaaa = aaaaRecords.status === 'fulfilled' ? aaaaRecords.value : [];
48
+ if (aaaaRecords.status === 'rejected') {
49
+ result.errors.aaaa = aaaaRecords.reason?.message;
50
+ }
51
+
52
+ result.records.ns = nsRecords.status === 'fulfilled' ? nsRecords.value : [];
53
+ if (nsRecords.status === 'rejected') {
54
+ result.errors.ns = nsRecords.reason?.message;
55
+ }
56
+
57
+ result.records.mx = mxRecords.status === 'fulfilled' ? mxRecords.value : [];
58
+ if (mxRecords.status === 'rejected') {
59
+ result.errors.mx = mxRecords.reason?.message;
60
+ }
61
+
62
+ result.records.txt = txtRecords.status === 'fulfilled' ? txtRecords.value : [];
63
+ if (txtRecords.status === 'rejected') {
64
+ result.errors.txt = txtRecords.reason?.message;
65
+ }
66
+
67
+ const allIps = [
68
+ ...(result.records.a || []),
69
+ ...(result.records.aaaa || [])
70
+ ];
71
+
72
+ if (allIps.length > 0) {
73
+ const reverseLookups = await Promise.allSettled(
74
+ allIps.map(async (ip) => {
75
+ try {
76
+ const hosts = await dns.reverse(ip);
77
+ return { ip, hosts };
78
+ } catch (error) {
79
+ return { ip, hosts: [], error };
80
+ }
81
+ })
82
+ );
83
+
84
+ result.records.reverse = {};
85
+ for (const entry of reverseLookups) {
86
+ if (entry.status === 'fulfilled') {
87
+ const { ip, hosts, error } = entry.value;
88
+ result.records.reverse[ip] = hosts;
89
+ if (error) {
90
+ result.errors[`reverse:${ip}`] = error?.message;
91
+ }
92
+ } else if (entry.reason?.ip) {
93
+ result.records.reverse[entry.reason.ip] = [];
94
+ result.errors[`reverse:${entry.reason.ip}`] = entry.reason.error?.message;
95
+ }
96
+ }
97
+ } else {
98
+ result.records.reverse = {};
99
+ }
100
+ } catch (error) {
101
+ result.status = 'error';
102
+ result.message = error?.message || 'DNS lookup failed';
103
+ }
104
+
105
+ return result;
106
+ }
107
+ }
@@ -0,0 +1,362 @@
1
+ /**
2
+ * DNSDumpster Stage
3
+ *
4
+ * DNS Intelligence via dnsdumpster.com web scraping
5
+ *
6
+ * Discovers:
7
+ * - DNS records (A, AAAA, MX, TXT, NS)
8
+ * - Subdomains
9
+ * - Related domains
10
+ * - Network map data
11
+ *
12
+ * Uses 100% free web scraping (no API key required)
13
+ * - dnsdumpster.com (unlimited, requires CSRF token handling)
14
+ */
15
+
16
+ export class DNSDumpsterStage {
17
+ constructor(plugin) {
18
+ this.plugin = plugin;
19
+ this.commandRunner = plugin.commandRunner;
20
+ this.config = plugin.config;
21
+ }
22
+
23
+ /**
24
+ * Execute DNSDumpster lookup
25
+ * @param {Object} target - Target object with host property
26
+ * @param {Object} options - DNSDumpster options
27
+ * @returns {Promise<Object>} DNSDumpster results
28
+ */
29
+ async execute(target, options = {}) {
30
+ const result = {
31
+ status: 'ok',
32
+ host: target.host,
33
+ dnsRecords: {
34
+ A: [],
35
+ AAAA: [],
36
+ MX: [],
37
+ TXT: [],
38
+ NS: []
39
+ },
40
+ subdomains: [],
41
+ relatedDomains: [],
42
+ errors: {}
43
+ };
44
+
45
+ // Track individual tool results for artifact persistence
46
+ const individual = {
47
+ dnsdumpster: { status: 'ok', data: null, raw: null },
48
+ dig: { status: 'ok', records: {} }
49
+ };
50
+
51
+ try {
52
+ // DNSDumpster requires two-step process:
53
+ // 1. GET to obtain CSRF token
54
+ // 2. POST with token to get results
55
+
56
+ const baseUrl = 'https://dnsdumpster.com/';
57
+
58
+ // Step 1: Get CSRF token
59
+ const [csrfToken, cookie] = await this.getCsrfToken(baseUrl, options);
60
+
61
+ if (!csrfToken) {
62
+ result.status = 'error';
63
+ result.errors.csrf = 'Failed to obtain CSRF token from DNSDumpster';
64
+ individual.dnsdumpster.status = 'error';
65
+
66
+ return {
67
+ _individual: individual,
68
+ _aggregated: result,
69
+ ...result
70
+ };
71
+ }
72
+
73
+ // Step 2: Submit query
74
+ const data = await this.submitQuery(baseUrl, target.host, csrfToken, cookie, options);
75
+
76
+ if (!data) {
77
+ result.status = 'error';
78
+ result.errors.query = 'Failed to retrieve data from DNSDumpster';
79
+ individual.dnsdumpster.status = 'error';
80
+
81
+ return {
82
+ _individual: individual,
83
+ _aggregated: result,
84
+ ...result
85
+ };
86
+ }
87
+
88
+ // Save raw HTML if persistRawOutput is enabled
89
+ if (this.config?.storage?.persistRawOutput) {
90
+ individual.dnsdumpster.raw = data.substring(0, 50000); // Truncate to 50KB
91
+ }
92
+
93
+ // Step 3: Parse HTML response
94
+ const parsed = this.parseHtmlResponse(data);
95
+
96
+ result.dnsRecords = parsed.dnsRecords;
97
+ result.subdomains = parsed.subdomains;
98
+ result.relatedDomains = parsed.relatedDomains;
99
+
100
+ // Store parsed data in individual results
101
+ individual.dnsdumpster.data = parsed;
102
+
103
+ } catch (error) {
104
+ result.status = 'error';
105
+ result.errors.general = error.message;
106
+ individual.dnsdumpster.status = 'error';
107
+ }
108
+
109
+ // Fallback to dig if DNSDumpster fails
110
+ if (result.status === 'error' && options.fallbackToDig !== false) {
111
+ const digResults = await this.fallbackDigLookup(target.host);
112
+ result.dnsRecords = digResults.dnsRecords;
113
+ result.status = 'ok_fallback';
114
+ individual.dig = digResults;
115
+ }
116
+
117
+ return {
118
+ _individual: individual,
119
+ _aggregated: result,
120
+ ...result // Root level for compatibility
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Get CSRF token from DNSDumpster homepage
126
+ */
127
+ async getCsrfToken(baseUrl, options = {}) {
128
+ try {
129
+ const response = await fetch(baseUrl, {
130
+ headers: {
131
+ 'User-Agent': this.config.curl?.userAgent || 'Mozilla/5.0 (compatible; ReconBot/1.0)'
132
+ },
133
+ signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined
134
+ });
135
+
136
+ if (!response.ok) {
137
+ return [null, null];
138
+ }
139
+
140
+ const html = await response.text();
141
+ const cookies = response.headers.get('set-cookie') || '';
142
+
143
+ // Extract CSRF token from HTML
144
+ // Format: <input type='hidden' name='csrfmiddlewaretoken' value='TOKEN' />
145
+ const csrfMatch = html.match(/name='csrfmiddlewaretoken'\s+value='([^']+)'/);
146
+
147
+ if (!csrfMatch) {
148
+ return [null, null];
149
+ }
150
+
151
+ const csrfToken = csrfMatch[1];
152
+
153
+ return [csrfToken, cookies];
154
+
155
+ } catch (error) {
156
+ return [null, null];
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Submit query to DNSDumpster
162
+ */
163
+ async submitQuery(baseUrl, domain, csrfToken, cookie, options = {}) {
164
+ try {
165
+ const formData = new URLSearchParams();
166
+ formData.append('csrfmiddlewaretoken', csrfToken);
167
+ formData.append('targetip', domain);
168
+ formData.append('user', 'free');
169
+
170
+ const response = await fetch(baseUrl, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'User-Agent': this.config.curl?.userAgent || 'Mozilla/5.0 (compatible; ReconBot/1.0)',
174
+ 'Content-Type': 'application/x-www-form-urlencoded',
175
+ 'Referer': baseUrl,
176
+ 'Cookie': cookie
177
+ },
178
+ body: formData.toString(),
179
+ signal: AbortSignal.timeout ? AbortSignal.timeout(15000) : undefined
180
+ });
181
+
182
+ if (!response.ok) {
183
+ return null;
184
+ }
185
+
186
+ return await response.text();
187
+
188
+ } catch (error) {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Parse HTML response from DNSDumpster
195
+ *
196
+ * DNSDumpster returns HTML tables with DNS records.
197
+ * We need to extract data from these tables.
198
+ */
199
+ parseHtmlResponse(html) {
200
+ const result = {
201
+ dnsRecords: {
202
+ A: [],
203
+ AAAA: [],
204
+ MX: [],
205
+ TXT: [],
206
+ NS: []
207
+ },
208
+ subdomains: [],
209
+ relatedDomains: []
210
+ };
211
+
212
+ // Extract DNS Host Records (A)
213
+ // Format: <td class="col-md-4">subdomain.example.com<br>IP</td>
214
+ const aRecordMatches = html.matchAll(/<tr[^>]*>[\s\S]*?<td[^>]*>([\w\-\.]+)<br>([\d\.]+)<\/td>[\s\S]*?<\/tr>/g);
215
+ for (const match of aRecordMatches) {
216
+ const hostname = match[1];
217
+ const ip = match[2];
218
+ if (hostname && ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
219
+ result.dnsRecords.A.push({ hostname, ip });
220
+
221
+ // Also add to subdomains if it's a subdomain
222
+ if (hostname.includes('.')) {
223
+ result.subdomains.push(hostname);
224
+ }
225
+ }
226
+ }
227
+
228
+ // Extract MX Records
229
+ // Format: <td class="col-md-4">priority mail.example.com<br>IP</td>
230
+ const mxRecordMatches = html.matchAll(/<tr[^>]*>[\s\S]*?<td[^>]*>(\d+)\s+([\w\-\.]+)<br>([\d\.]+)<\/td>[\s\S]*?<\/tr>/g);
231
+ for (const match of mxRecordMatches) {
232
+ const priority = match[1];
233
+ const hostname = match[2];
234
+ const ip = match[3];
235
+ if (hostname && ip) {
236
+ result.dnsRecords.MX.push({ priority, hostname, ip });
237
+ }
238
+ }
239
+
240
+ // Extract TXT Records
241
+ // Format: <td class="col-md-10">TXT content</td>
242
+ const txtSectionMatch = html.match(/TXT Records[\s\S]*?<table[^>]*>([\s\S]*?)<\/table>/i);
243
+ if (txtSectionMatch) {
244
+ const txtMatches = txtSectionMatch[1].matchAll(/<td[^>]*>([^<]+)<\/td>/g);
245
+ for (const match of txtMatches) {
246
+ const content = match[1].trim();
247
+ if (content && content.length > 0) {
248
+ result.dnsRecords.TXT.push({ content });
249
+ }
250
+ }
251
+ }
252
+
253
+ // Extract NS Records
254
+ // Format similar to A records
255
+ const nsSectionMatch = html.match(/DNS Servers[\s\S]*?<table[^>]*>([\s\S]*?)<\/table>/i);
256
+ if (nsSectionMatch) {
257
+ const nsMatches = nsSectionMatch[1].matchAll(/<td[^>]*>([\w\-\.]+)<br>([\d\.]+)<\/td>/g);
258
+ for (const match of nsMatches) {
259
+ const hostname = match[1];
260
+ const ip = match[2];
261
+ if (hostname && ip) {
262
+ result.dnsRecords.NS.push({ hostname, ip });
263
+ }
264
+ }
265
+ }
266
+
267
+ // Deduplicate subdomains
268
+ result.subdomains = [...new Set(result.subdomains)];
269
+
270
+ return result;
271
+ }
272
+
273
+ /**
274
+ * Fallback: Use dig commands for basic DNS records
275
+ * This is used if DNSDumpster scraping fails
276
+ */
277
+ async fallbackDigLookup(host) {
278
+ const result = {
279
+ dnsRecords: {
280
+ A: [],
281
+ AAAA: [],
282
+ MX: [],
283
+ TXT: [],
284
+ NS: []
285
+ },
286
+ subdomains: [],
287
+ relatedDomains: []
288
+ };
289
+
290
+ try {
291
+ // A records
292
+ const aRun = await this.commandRunner.run('dig', ['+short', 'A', host], { timeout: 5000 });
293
+ if (aRun.ok && aRun.stdout) {
294
+ const ips = aRun.stdout
295
+ .split('\n')
296
+ .map(line => line.trim())
297
+ .filter(line => /^\d+\.\d+\.\d+\.\d+$/.test(line));
298
+
299
+ result.dnsRecords.A = ips.map(ip => ({ hostname: host, ip }));
300
+ }
301
+
302
+ // AAAA records
303
+ const aaaaRun = await this.commandRunner.run('dig', ['+short', 'AAAA', host], { timeout: 5000 });
304
+ if (aaaaRun.ok && aaaaRun.stdout) {
305
+ const ips = aaaaRun.stdout
306
+ .split('\n')
307
+ .map(line => line.trim())
308
+ .filter(line => /^[0-9a-f:]+$/i.test(line) && line.includes(':'));
309
+
310
+ result.dnsRecords.AAAA = ips.map(ip => ({ hostname: host, ip }));
311
+ }
312
+
313
+ // MX records
314
+ const mxRun = await this.commandRunner.run('dig', ['+short', 'MX', host], { timeout: 5000 });
315
+ if (mxRun.ok && mxRun.stdout) {
316
+ const mxRecords = mxRun.stdout
317
+ .split('\n')
318
+ .map(line => line.trim())
319
+ .filter(line => line.length > 0)
320
+ .map(line => {
321
+ const parts = line.split(' ');
322
+ if (parts.length === 2) {
323
+ return { priority: parts[0], hostname: parts[1].replace(/\.$/, ''), ip: null };
324
+ }
325
+ return null;
326
+ })
327
+ .filter(Boolean);
328
+
329
+ result.dnsRecords.MX = mxRecords;
330
+ }
331
+
332
+ // TXT records
333
+ const txtRun = await this.commandRunner.run('dig', ['+short', 'TXT', host], { timeout: 5000 });
334
+ if (txtRun.ok && txtRun.stdout) {
335
+ const txtRecords = txtRun.stdout
336
+ .split('\n')
337
+ .map(line => line.trim().replace(/"/g, ''))
338
+ .filter(line => line.length > 0)
339
+ .map(content => ({ content }));
340
+
341
+ result.dnsRecords.TXT = txtRecords;
342
+ }
343
+
344
+ // NS records
345
+ const nsRun = await this.commandRunner.run('dig', ['+short', 'NS', host], { timeout: 5000 });
346
+ if (nsRun.ok && nsRun.stdout) {
347
+ const nsRecords = nsRun.stdout
348
+ .split('\n')
349
+ .map(line => line.trim().replace(/\.$/, ''))
350
+ .filter(line => line.length > 0)
351
+ .map(hostname => ({ hostname, ip: null }));
352
+
353
+ result.dnsRecords.NS = nsRecords;
354
+ }
355
+
356
+ } catch (error) {
357
+ // Silently fail, return empty results
358
+ }
359
+
360
+ return result;
361
+ }
362
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * FingerprintStage
3
+ *
4
+ * Technology fingerprinting:
5
+ * - whatweb (web technology identification)
6
+ */
7
+
8
+ export class FingerprintStage {
9
+ constructor(plugin) {
10
+ this.plugin = plugin;
11
+ this.commandRunner = plugin.commandRunner;
12
+ this.config = plugin.config;
13
+ }
14
+
15
+ async execute(target, featureConfig = {}) {
16
+ const technologies = new Set();
17
+ const tools = {};
18
+
19
+ if (featureConfig.whatweb) {
20
+ const run = await this.commandRunner.run('whatweb', ['-q', this._buildUrl(target)], {
21
+ timeout: featureConfig.timeout ?? 20000,
22
+ maxBuffer: 2 * 1024 * 1024
23
+ });
24
+ if (run.ok) {
25
+ const parsed = run.stdout
26
+ .split(/[\r\n]+/)
27
+ .flatMap((line) => line.split(' '))
28
+ .map((token) => token.trim())
29
+ .filter((token) => token.includes('[') && token.includes(']'))
30
+ .map((token) => token.substring(0, token.indexOf('[')));
31
+ parsed.forEach((tech) => technologies.add(tech));
32
+ tools.whatweb = { status: 'ok', technologies: parsed.slice(0, 20) };
33
+ if (this.config.storage.persistRawOutput) {
34
+ tools.whatweb.raw = this._truncateOutput(run.stdout);
35
+ }
36
+ } else {
37
+ tools.whatweb = {
38
+ status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
39
+ message: run.error?.message || 'whatweb failed'
40
+ };
41
+ }
42
+ }
43
+
44
+ if (technologies.size === 0 && Object.keys(tools).length === 0) {
45
+ return { status: 'skipped' };
46
+ }
47
+
48
+ return {
49
+ _individual: tools,
50
+ _aggregated: {
51
+ status: technologies.size ? 'ok' : 'empty',
52
+ technologies: Array.from(technologies),
53
+ tools
54
+ },
55
+ status: technologies.size ? 'ok' : 'empty',
56
+ technologies: Array.from(technologies),
57
+ tools
58
+ };
59
+ }
60
+
61
+ _buildUrl(target) {
62
+ const protocol = target.protocol || 'https';
63
+ const port = target.port && target.port !== (protocol === 'http' ? 80 : 443) ? `:${target.port}` : '';
64
+ return `${protocol}://${target.host}${port}${target.path || ''}`;
65
+ }
66
+
67
+ _truncateOutput(text, maxLength = 10000) {
68
+ if (!text || text.length <= maxLength) return text;
69
+ return text.substring(0, maxLength) + '\n... (truncated)';
70
+ }
71
+ }