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,1373 @@
1
+ /**
2
+ * OsintStage
3
+ *
4
+ * Open Source Intelligence - Digital footprint mapping for organizations and individuals.
5
+ *
6
+ * LEGAL DISCLAIMER:
7
+ * - Only collect publicly available information
8
+ * - Do NOT use social engineering, exploits, or unauthorized access
9
+ * - Respect rate limits and terms of service
10
+ * - Use for defensive security and authorized testing only
11
+ *
12
+ * Categories:
13
+ * 1. Username Enumeration (Sherlock, Maigret, WhatsMyName)
14
+ * 2. Email Collection (theHarvester - 100% free)
15
+ * 3. Leak Detection (HaveIBeenPwned v2, Scylla.sh - 100% free)
16
+ * 4. GitHub Reconnaissance (repos, mentions, code search)
17
+ * 5. SaaS Footprint (DNS records, JS fingerprinting)
18
+ * 6. Social Media Mapping (LinkedIn, Twitter, etc.)
19
+ */
20
+
21
+ export class OsintStage {
22
+ constructor(plugin) {
23
+ this.plugin = plugin;
24
+ this.commandRunner = plugin.commandRunner;
25
+ this.config = plugin.config;
26
+ }
27
+
28
+ /**
29
+ * Execute OSINT scan
30
+ * @param {Object} target - Target object with host/domain property
31
+ * @param {Object} options - OSINT options
32
+ * @returns {Promise<Object>} OSINT results
33
+ */
34
+ async execute(target, featureConfig = {}) {
35
+ const domain = this.extractBaseDomain(target.host);
36
+ const companyName = this.extractCompanyName(domain);
37
+
38
+ const result = {
39
+ status: 'ok',
40
+ domain,
41
+ companyName,
42
+ categories: {
43
+ usernames: null,
44
+ emails: null,
45
+ leaks: null,
46
+ github: null,
47
+ saas: null,
48
+ socialMedia: null
49
+ },
50
+ summary: {
51
+ totalProfiles: 0,
52
+ totalEmails: 0,
53
+ totalLeaks: 0,
54
+ totalRepos: 0,
55
+ totalSaasServices: 0
56
+ },
57
+ errors: {}
58
+ };
59
+
60
+ // 1. Username Enumeration
61
+ if (featureConfig.usernames !== false) {
62
+ try {
63
+ result.categories.usernames = await this.enumerateUsernames(companyName, featureConfig);
64
+ result.summary.totalProfiles = result.categories.usernames.profiles?.length || 0;
65
+ } catch (error) {
66
+ result.errors.usernames = error.message;
67
+ }
68
+ }
69
+
70
+ // 2. Email Collection
71
+ if (featureConfig.emails !== false) {
72
+ try {
73
+ result.categories.emails = await this.collectEmails(domain, featureConfig);
74
+ result.summary.totalEmails = result.categories.emails.addresses?.length || 0;
75
+ } catch (error) {
76
+ result.errors.emails = error.message;
77
+ }
78
+ }
79
+
80
+ // 3. Leak Detection
81
+ if (featureConfig.leaks !== false && result.categories.emails?.addresses?.length > 0) {
82
+ try {
83
+ result.categories.leaks = await this.detectLeaks(result.categories.emails.addresses, featureConfig);
84
+ result.summary.totalLeaks = result.categories.leaks.breaches?.length || 0;
85
+ } catch (error) {
86
+ result.errors.leaks = error.message;
87
+ }
88
+ }
89
+
90
+ // 4. GitHub Reconnaissance
91
+ if (featureConfig.github !== false) {
92
+ try {
93
+ result.categories.github = await this.githubRecon(companyName, domain, featureConfig);
94
+ result.summary.totalRepos = result.categories.github.repositories?.length || 0;
95
+ } catch (error) {
96
+ result.errors.github = error.message;
97
+ }
98
+ }
99
+
100
+ // 5. SaaS Footprint Detection
101
+ if (featureConfig.saas !== false) {
102
+ try {
103
+ result.categories.saas = await this.detectSaasFootprint(domain, featureConfig);
104
+ result.summary.totalSaasServices = Object.keys(result.categories.saas.services || {}).length;
105
+ } catch (error) {
106
+ result.errors.saas = error.message;
107
+ }
108
+ }
109
+
110
+ // 6. Social Media Mapping
111
+ if (featureConfig.socialMedia !== false) {
112
+ try {
113
+ result.categories.socialMedia = await this.mapSocialMedia(companyName, domain, featureConfig);
114
+ } catch (error) {
115
+ result.errors.socialMedia = error.message;
116
+ }
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * 1. Username Enumeration
124
+ * Search for company/brand username across social platforms
125
+ */
126
+ async enumerateUsernames(companyName, options = {}) {
127
+ const result = {
128
+ status: 'ok',
129
+ searchTerm: companyName,
130
+ profiles: [],
131
+ sources: {}
132
+ };
133
+
134
+ // Try Sherlock (username enumeration across 300+ sites)
135
+ if (options.sherlock !== false) {
136
+ const sherlockResult = await this.runSherlock(companyName, options);
137
+ if (sherlockResult.status === 'ok') {
138
+ result.sources.sherlock = sherlockResult;
139
+ result.profiles.push(...sherlockResult.profiles);
140
+ } else {
141
+ result.sources.sherlock = sherlockResult;
142
+ }
143
+ }
144
+
145
+ // Try Maigret (similar to Sherlock but more sites)
146
+ if (options.maigret !== false) {
147
+ const maigretResult = await this.runMaigret(companyName, options);
148
+ if (maigretResult.status === 'ok') {
149
+ result.sources.maigret = maigretResult;
150
+ result.profiles.push(...maigretResult.profiles);
151
+ } else {
152
+ result.sources.maigret = maigretResult;
153
+ }
154
+ }
155
+
156
+ // Try WhatsMyName (API-based, 400+ sites)
157
+ if (options.whatsmyname !== false) {
158
+ const wmnResult = await this.runWhatsMyName(companyName, options);
159
+ if (wmnResult.status === 'ok') {
160
+ result.sources.whatsmyname = wmnResult;
161
+ result.profiles.push(...wmnResult.found.map(item => ({
162
+ platform: item.site,
163
+ url: item.url,
164
+ username: companyName,
165
+ category: item.category,
166
+ source: 'whatsmyname'
167
+ })));
168
+ } else {
169
+ result.sources.whatsmyname = wmnResult;
170
+ }
171
+ }
172
+
173
+ // Deduplicate profiles by URL
174
+ result.profiles = this.deduplicateProfiles(result.profiles);
175
+
176
+ return result;
177
+ }
178
+
179
+ /**
180
+ * Run Sherlock username search
181
+ */
182
+ async runSherlock(username, options = {}) {
183
+ const run = await this.commandRunner.run('sherlock', [username, '--json'], {
184
+ timeout: 60000,
185
+ maxBuffer: 5 * 1024 * 1024
186
+ });
187
+
188
+ if (!run.ok) {
189
+ return {
190
+ status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
191
+ message: run.error?.message || 'Sherlock failed',
192
+ stderr: run.stderr
193
+ };
194
+ }
195
+
196
+ try {
197
+ const profiles = [];
198
+ const lines = run.stdout.split('\n').filter(Boolean);
199
+
200
+ for (const line of lines) {
201
+ try {
202
+ const data = JSON.parse(line);
203
+ if (data.url && data.status === 'claimed') {
204
+ profiles.push({
205
+ platform: data.site_name,
206
+ url: data.url,
207
+ username: username,
208
+ source: 'sherlock'
209
+ });
210
+ }
211
+ } catch (e) {
212
+ // Skip malformed JSON lines
213
+ }
214
+ }
215
+
216
+ return {
217
+ status: 'ok',
218
+ profiles,
219
+ count: profiles.length
220
+ };
221
+ } catch (error) {
222
+ return {
223
+ status: 'error',
224
+ message: 'Failed to parse Sherlock output',
225
+ error: error.message
226
+ };
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Run Maigret username search
232
+ */
233
+ async runMaigret(username, options = {}) {
234
+ const run = await this.commandRunner.run('maigret', [username, '--json', '--timeout', '30'], {
235
+ timeout: 90000,
236
+ maxBuffer: 10 * 1024 * 1024
237
+ });
238
+
239
+ if (!run.ok) {
240
+ return {
241
+ status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
242
+ message: run.error?.message || 'Maigret failed',
243
+ stderr: run.stderr
244
+ };
245
+ }
246
+
247
+ try {
248
+ const profiles = [];
249
+ const data = JSON.parse(run.stdout);
250
+
251
+ for (const [siteName, siteData] of Object.entries(data)) {
252
+ if (siteData.status === 'FOUND' && siteData.url) {
253
+ profiles.push({
254
+ platform: siteName,
255
+ url: siteData.url,
256
+ username: username,
257
+ source: 'maigret',
258
+ tags: siteData.tags || []
259
+ });
260
+ }
261
+ }
262
+
263
+ return {
264
+ status: 'ok',
265
+ profiles,
266
+ count: profiles.length
267
+ };
268
+ } catch (error) {
269
+ return {
270
+ status: 'error',
271
+ message: 'Failed to parse Maigret output',
272
+ error: error.message
273
+ };
274
+ }
275
+ }
276
+
277
+ /**
278
+ * 2. Email Collection
279
+ * Collect public emails associated with domain
280
+ */
281
+ async collectEmails(domain, options = {}) {
282
+ const result = {
283
+ status: 'ok',
284
+ domain,
285
+ addresses: [],
286
+ sources: {}
287
+ };
288
+
289
+ // theHarvester (already in config)
290
+ if (options.theHarvester !== false) {
291
+ const harvesterResult = await this.runTheHarvester(domain, options);
292
+ if (harvesterResult.status === 'ok') {
293
+ result.sources.theHarvester = harvesterResult;
294
+ result.addresses.push(...harvesterResult.emails);
295
+ } else {
296
+ result.sources.theHarvester = harvesterResult;
297
+ }
298
+ }
299
+
300
+ // Deduplicate emails
301
+ result.addresses = [...new Set(result.addresses)].sort();
302
+
303
+ return result;
304
+ }
305
+
306
+ /**
307
+ * Run theHarvester for email collection
308
+ */
309
+ async runTheHarvester(domain, options = {}) {
310
+ const sources = options.harvesterSources || ['bing', 'google', 'duckduckgo'];
311
+ const run = await this.commandRunner.run('theHarvester', [
312
+ '-d', domain,
313
+ '-b', sources.join(','),
314
+ '-l', '500'
315
+ ], {
316
+ timeout: 120000,
317
+ maxBuffer: 10 * 1024 * 1024
318
+ });
319
+
320
+ if (!run.ok) {
321
+ return {
322
+ status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
323
+ message: run.error?.message || 'theHarvester failed',
324
+ stderr: run.stderr
325
+ };
326
+ }
327
+
328
+ // Parse email addresses from output
329
+ const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
330
+ const emails = [...new Set((run.stdout.match(emailRegex) || []).filter(email =>
331
+ email.toLowerCase().includes(domain.toLowerCase())
332
+ ))];
333
+
334
+ return {
335
+ status: 'ok',
336
+ emails,
337
+ count: emails.length
338
+ };
339
+ }
340
+
341
+
342
+ /**
343
+ * 3. Leak Detection
344
+ * Check if emails appear in known breaches (HaveIBeenPwned)
345
+ */
346
+ async detectLeaks(emails, options = {}) {
347
+ const result = {
348
+ status: 'ok',
349
+ checked: 0,
350
+ breaches: [],
351
+ emailBreaches: {},
352
+ sources: {
353
+ hibp: [],
354
+ scylla: []
355
+ }
356
+ };
357
+
358
+ const maxEmails = options.maxEmailsToCheck || 10;
359
+ const emailsToCheck = emails.slice(0, maxEmails);
360
+
361
+ for (const email of emailsToCheck) {
362
+ try {
363
+ result.checked++;
364
+
365
+ // Check HaveIBeenPwned
366
+ if (options.hibp !== false) {
367
+ const breaches = await this.checkHaveIBeenPwned(email, options);
368
+
369
+ if (breaches.length > 0) {
370
+ result.emailBreaches[email] = result.emailBreaches[email] || [];
371
+ result.emailBreaches[email].push(...breaches);
372
+ result.sources.hibp.push(...breaches.map(b => ({
373
+ ...b,
374
+ email,
375
+ source: 'HaveIBeenPwned'
376
+ })));
377
+ }
378
+
379
+ // Rate limiting (HIBP requires 1.5s between requests)
380
+ await this.sleep(1500);
381
+ }
382
+
383
+ // Check Scylla.sh
384
+ if (options.scylla !== false) {
385
+ const scyllaResult = await this.checkScylla(email, options);
386
+
387
+ if (scyllaResult.status === 'ok' && scyllaResult.breaches.length > 0) {
388
+ result.emailBreaches[email] = result.emailBreaches[email] || [];
389
+ result.emailBreaches[email].push(...scyllaResult.breaches);
390
+ result.sources.scylla.push(...scyllaResult.breaches.map(b => ({
391
+ ...b,
392
+ source: 'Scylla.sh'
393
+ })));
394
+ }
395
+
396
+ // Rate limiting for Scylla
397
+ await this.sleep(1000);
398
+ }
399
+
400
+ } catch (error) {
401
+ // Continue with next email
402
+ }
403
+ }
404
+
405
+ // Combine all breaches from both sources
406
+ result.breaches = [
407
+ ...result.sources.hibp,
408
+ ...result.sources.scylla
409
+ ];
410
+
411
+ // Deduplicate breaches by name/source
412
+ const uniqueBreaches = new Map();
413
+ for (const breach of result.breaches) {
414
+ const key = `${breach.Name || breach.source}-${breach.email}`;
415
+ if (!uniqueBreaches.has(key)) {
416
+ uniqueBreaches.set(key, breach);
417
+ }
418
+ }
419
+ result.breaches = Array.from(uniqueBreaches.values());
420
+
421
+ return result;
422
+ }
423
+
424
+ /**
425
+ * Check HaveIBeenPwned API for email breaches
426
+ * NOTE: Uses public API (no key required) but has strict rate limits
427
+ */
428
+ async checkHaveIBeenPwned(email, options = {}) {
429
+ try {
430
+ // Using v2 API which is still free (v3 requires paid API key)
431
+ // Note: v2 is deprecated but still works for basic breach checking
432
+ const url = `https://haveibeenpwned.com/api/v2/breachedaccount/${encodeURIComponent(email)}`;
433
+
434
+ const headers = {
435
+ 'User-Agent': this.config.curl?.userAgent || 'ReconPlugin/1.0'
436
+ };
437
+
438
+ const response = await fetch(url, {
439
+ headers,
440
+ signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined
441
+ });
442
+
443
+ if (response.status === 404) {
444
+ // No breaches found
445
+ return [];
446
+ }
447
+
448
+ if (response.status === 429) {
449
+ // Rate limited - too many requests
450
+ return [];
451
+ }
452
+
453
+ if (!response.ok) {
454
+ throw new Error(`HIBP API returned ${response.status}`);
455
+ }
456
+
457
+ const breaches = await response.json();
458
+ return breaches;
459
+
460
+ } catch (error) {
461
+ // Rate limited or error - continue gracefully
462
+ return [];
463
+ }
464
+ }
465
+
466
+ /**
467
+ * 4. GitHub Reconnaissance
468
+ * Search for organization repos, code mentions, and potential leaks
469
+ */
470
+ async githubRecon(companyName, domain, options = {}) {
471
+ const result = {
472
+ status: 'ok',
473
+ searchTerms: {
474
+ company: companyName,
475
+ domain: domain
476
+ },
477
+ repositories: [],
478
+ codeMentions: [],
479
+ users: []
480
+ };
481
+
482
+ // Search for organization repositories
483
+ if (options.githubRepos !== false) {
484
+ const repoResults = await this.searchGitHubRepos(companyName, options);
485
+ result.repositories = repoResults.repositories || [];
486
+ }
487
+
488
+ // Search for code mentions
489
+ if (options.githubCode !== false) {
490
+ const codeResults = await this.searchGitHubCode(domain, options);
491
+ result.codeMentions = codeResults.mentions || [];
492
+ }
493
+
494
+ // Search for users
495
+ if (options.githubUsers !== false) {
496
+ const userResults = await this.searchGitHubUsers(companyName, options);
497
+ result.users = userResults.users || [];
498
+ }
499
+
500
+ return result;
501
+ }
502
+
503
+ /**
504
+ * Search GitHub repositories
505
+ */
506
+ async searchGitHubRepos(query, options = {}) {
507
+ try {
508
+ const url = `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=${options.maxRepos || 10}`;
509
+
510
+ const headers = {
511
+ 'User-Agent': this.config.curl?.userAgent || 'ReconPlugin/1.0',
512
+ 'Accept': 'application/vnd.github+json'
513
+ };
514
+
515
+ if (options.githubToken) {
516
+ headers['Authorization'] = `Bearer ${options.githubToken}`;
517
+ }
518
+
519
+ const response = await fetch(url, {
520
+ headers,
521
+ signal: AbortSignal.timeout ? AbortSignal.timeout(15000) : undefined
522
+ });
523
+
524
+ if (!response.ok) {
525
+ return {
526
+ status: 'error',
527
+ message: `GitHub API returned ${response.status}`,
528
+ repositories: []
529
+ };
530
+ }
531
+
532
+ const data = await response.json();
533
+ const repositories = (data.items || []).map(repo => ({
534
+ name: repo.full_name,
535
+ url: repo.html_url,
536
+ description: repo.description,
537
+ stars: repo.stargazers_count,
538
+ forks: repo.forks_count,
539
+ language: repo.language,
540
+ updated: repo.updated_at
541
+ }));
542
+
543
+ return {
544
+ status: 'ok',
545
+ repositories,
546
+ count: repositories.length
547
+ };
548
+
549
+ } catch (error) {
550
+ return {
551
+ status: 'error',
552
+ message: error.message,
553
+ repositories: []
554
+ };
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Search GitHub code for domain mentions
560
+ */
561
+ async searchGitHubCode(query, options = {}) {
562
+ try {
563
+ const url = `https://api.github.com/search/code?q=${encodeURIComponent(query)}&per_page=${options.maxCodeResults || 10}`;
564
+
565
+ const headers = {
566
+ 'User-Agent': this.config.curl?.userAgent || 'ReconPlugin/1.0',
567
+ 'Accept': 'application/vnd.github+json'
568
+ };
569
+
570
+ if (options.githubToken) {
571
+ headers['Authorization'] = `Bearer ${options.githubToken}`;
572
+ }
573
+
574
+ const response = await fetch(url, {
575
+ headers,
576
+ signal: AbortSignal.timeout ? AbortSignal.timeout(15000) : undefined
577
+ });
578
+
579
+ if (!response.ok) {
580
+ return {
581
+ status: 'error',
582
+ message: `GitHub API returned ${response.status}`,
583
+ mentions: []
584
+ };
585
+ }
586
+
587
+ const data = await response.json();
588
+ const mentions = (data.items || []).map(item => ({
589
+ repository: item.repository.full_name,
590
+ path: item.path,
591
+ url: item.html_url,
592
+ sha: item.sha
593
+ }));
594
+
595
+ return {
596
+ status: 'ok',
597
+ mentions,
598
+ count: mentions.length
599
+ };
600
+
601
+ } catch (error) {
602
+ return {
603
+ status: 'error',
604
+ message: error.message,
605
+ mentions: []
606
+ };
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Search GitHub users
612
+ */
613
+ async searchGitHubUsers(query, options = {}) {
614
+ try {
615
+ const url = `https://api.github.com/search/users?q=${encodeURIComponent(query)}&per_page=${options.maxUsers || 10}`;
616
+
617
+ const headers = {
618
+ 'User-Agent': this.config.curl?.userAgent || 'ReconPlugin/1.0',
619
+ 'Accept': 'application/vnd.github+json'
620
+ };
621
+
622
+ if (options.githubToken) {
623
+ headers['Authorization'] = `Bearer ${options.githubToken}`;
624
+ }
625
+
626
+ const response = await fetch(url, {
627
+ headers,
628
+ signal: AbortSignal.timeout ? AbortSignal.timeout(15000) : undefined
629
+ });
630
+
631
+ if (!response.ok) {
632
+ return {
633
+ status: 'error',
634
+ message: `GitHub API returned ${response.status}`,
635
+ users: []
636
+ };
637
+ }
638
+
639
+ const data = await response.json();
640
+ const users = (data.items || []).map(user => ({
641
+ username: user.login,
642
+ url: user.html_url,
643
+ avatar: user.avatar_url,
644
+ type: user.type
645
+ }));
646
+
647
+ return {
648
+ status: 'ok',
649
+ users,
650
+ count: users.length
651
+ };
652
+
653
+ } catch (error) {
654
+ return {
655
+ status: 'error',
656
+ message: error.message,
657
+ users: []
658
+ };
659
+ }
660
+ }
661
+
662
+ /**
663
+ * 5. SaaS Footprint Detection
664
+ * Detect third-party services via DNS, JS, headers
665
+ */
666
+ async detectSaasFootprint(domain, options = {}) {
667
+ const result = {
668
+ status: 'ok',
669
+ domain,
670
+ services: {}
671
+ };
672
+
673
+ // Check DNS records for SaaS indicators
674
+ const dnsServices = await this.detectSaasFromDNS(domain, options);
675
+ Object.assign(result.services, dnsServices);
676
+
677
+ // Check HTTP headers and JS for SaaS fingerprints
678
+ const httpServices = await this.detectSaasFromHTTP(domain, options);
679
+ Object.assign(result.services, httpServices);
680
+
681
+ return result;
682
+ }
683
+
684
+ /**
685
+ * Detect SaaS from DNS records (MX, TXT, CNAME, A, NS)
686
+ */
687
+ async detectSaasFromDNS(domain, options = {}) {
688
+ const services = {};
689
+
690
+ // Check MX records for email providers
691
+ const mxRun = await this.commandRunner.run('dig', ['+short', 'MX', domain], { timeout: 5000 });
692
+ if (mxRun.ok && mxRun.stdout) {
693
+ const mx = mxRun.stdout.toLowerCase();
694
+
695
+ if (mx.includes('google') || mx.includes('gmail')) {
696
+ services.email = { provider: 'Google Workspace', evidence: 'MX records' };
697
+ } else if (mx.includes('outlook') || mx.includes('microsoft')) {
698
+ services.email = { provider: 'Microsoft 365', evidence: 'MX records' };
699
+ } else if (mx.includes('mail.protection.outlook')) {
700
+ services.email = { provider: 'Microsoft Exchange Online', evidence: 'MX records' };
701
+ } else if (mx.includes('mailgun')) {
702
+ services.email = { provider: 'Mailgun', evidence: 'MX records' };
703
+ } else if (mx.includes('sendgrid')) {
704
+ services.email = { provider: 'SendGrid', evidence: 'MX records' };
705
+ } else if (mx.includes('postmark')) {
706
+ services.email = { provider: 'Postmark', evidence: 'MX records' };
707
+ } else if (mx.includes('zoho')) {
708
+ services.email = { provider: 'Zoho Mail', evidence: 'MX records' };
709
+ } else if (mx.includes('protonmail')) {
710
+ services.email = { provider: 'ProtonMail', evidence: 'MX records' };
711
+ }
712
+ }
713
+
714
+ // Check TXT records for SPF/DKIM/DMARC
715
+ const txtRun = await this.commandRunner.run('dig', ['+short', 'TXT', domain], { timeout: 5000 });
716
+ if (txtRun.ok && txtRun.stdout) {
717
+ const txt = txtRun.stdout.toLowerCase();
718
+
719
+ // SPF providers
720
+ if (txt.includes('spf') && txt.includes('include:')) {
721
+ const spfIncludes = txt.match(/include:([^\s"]+)/g) || [];
722
+ const providers = spfIncludes.map(s => s.replace('include:', ''));
723
+
724
+ services.spf = {
725
+ providers,
726
+ evidence: 'SPF TXT record'
727
+ };
728
+
729
+ // Identify specific services from SPF
730
+ if (providers.some(p => p.includes('mailgun'))) {
731
+ services.emailSending = services.emailSending || [];
732
+ services.emailSending.push({ provider: 'Mailgun', evidence: 'SPF include' });
733
+ }
734
+ if (providers.some(p => p.includes('sendgrid'))) {
735
+ services.emailSending = services.emailSending || [];
736
+ services.emailSending.push({ provider: 'SendGrid', evidence: 'SPF include' });
737
+ }
738
+ if (providers.some(p => p.includes('mailchimp'))) {
739
+ services.emailMarketing = { provider: 'Mailchimp', evidence: 'SPF include' };
740
+ }
741
+ if (providers.some(p => p.includes('constantcontact'))) {
742
+ services.emailMarketing = { provider: 'Constant Contact', evidence: 'SPF include' };
743
+ }
744
+ }
745
+
746
+ // DMARC
747
+ if (txt.includes('v=dmarc')) {
748
+ services.dmarc = { enabled: true, evidence: 'DMARC TXT record' };
749
+ }
750
+
751
+ // Domain verification TXT records
752
+ if (txt.includes('google-site-verification')) {
753
+ services.domainVerification = services.domainVerification || [];
754
+ services.domainVerification.push({ provider: 'Google', evidence: 'TXT record' });
755
+ }
756
+ if (txt.includes('facebook-domain-verification')) {
757
+ services.domainVerification = services.domainVerification || [];
758
+ services.domainVerification.push({ provider: 'Facebook', evidence: 'TXT record' });
759
+ }
760
+ if (txt.includes('ms=ms')) {
761
+ services.domainVerification = services.domainVerification || [];
762
+ services.domainVerification.push({ provider: 'Microsoft', evidence: 'TXT record' });
763
+ }
764
+ }
765
+
766
+ // Check DKIM selectors (common ones)
767
+ const dkimSelectors = ['default', 'google', 'k1', 's1', 'selector1', 'selector2', 'dkim', 'mail'];
768
+ for (const selector of dkimSelectors) {
769
+ const dkimRun = await this.commandRunner.run('dig', ['+short', 'TXT', `${selector}._domainkey.${domain}`], { timeout: 3000 });
770
+ if (dkimRun.ok && dkimRun.stdout && dkimRun.stdout.includes('v=DKIM1')) {
771
+ services.dkim = services.dkim || { selectors: [], evidence: 'DKIM TXT records' };
772
+ services.dkim.selectors.push(selector);
773
+ }
774
+ }
775
+
776
+ // Check CNAME for CDN/hosting
777
+ const cnameRun = await this.commandRunner.run('dig', ['+short', 'CNAME', domain], { timeout: 5000 });
778
+ if (cnameRun.ok && cnameRun.stdout) {
779
+ const cname = cnameRun.stdout.toLowerCase();
780
+
781
+ if (cname.includes('cloudflare')) {
782
+ services.cdn = { provider: 'Cloudflare', evidence: 'CNAME record' };
783
+ } else if (cname.includes('fastly')) {
784
+ services.cdn = { provider: 'Fastly', evidence: 'CNAME record' };
785
+ } else if (cname.includes('akamai')) {
786
+ services.cdn = { provider: 'Akamai', evidence: 'CNAME record' };
787
+ } else if (cname.includes('cloudfront')) {
788
+ services.cdn = { provider: 'AWS CloudFront', evidence: 'CNAME record' };
789
+ } else if (cname.includes('vercel')) {
790
+ services.hosting = { provider: 'Vercel', evidence: 'CNAME record' };
791
+ } else if (cname.includes('netlify')) {
792
+ services.hosting = { provider: 'Netlify', evidence: 'CNAME record' };
793
+ } else if (cname.includes('herokuapp')) {
794
+ services.hosting = { provider: 'Heroku', evidence: 'CNAME record' };
795
+ } else if (cname.includes('github.io')) {
796
+ services.hosting = { provider: 'GitHub Pages', evidence: 'CNAME record' };
797
+ } else if (cname.includes('digitaloceanspaces')) {
798
+ services.hosting = { provider: 'DigitalOcean Spaces', evidence: 'CNAME record' };
799
+ } else if (cname.includes('s3.amazonaws') || cname.includes('s3-website')) {
800
+ services.hosting = { provider: 'AWS S3', evidence: 'CNAME record' };
801
+ }
802
+ }
803
+
804
+ // Check A records for hosting providers (common IP ranges)
805
+ const aRun = await this.commandRunner.run('dig', ['+short', 'A', domain], { timeout: 5000 });
806
+ if (aRun.ok && aRun.stdout) {
807
+ const ips = aRun.stdout.split('\n').filter(line => line.trim());
808
+
809
+ for (const ip of ips) {
810
+ // Cloudflare IP ranges
811
+ if (ip.startsWith('104.') || ip.startsWith('172.') || ip.startsWith('173.')) {
812
+ services.cdn = services.cdn || { provider: 'Cloudflare (detected by IP)', evidence: 'A record IP range' };
813
+ }
814
+ // AWS IP ranges (partial detection)
815
+ else if (ip.startsWith('52.') || ip.startsWith('54.') || ip.startsWith('18.')) {
816
+ services.cloud = services.cloud || { provider: 'AWS (likely)', evidence: 'A record IP range' };
817
+ }
818
+ // DigitalOcean IP ranges
819
+ else if (ip.startsWith('159.') || ip.startsWith('167.')) {
820
+ services.cloud = services.cloud || { provider: 'DigitalOcean (likely)', evidence: 'A record IP range' };
821
+ }
822
+ }
823
+ }
824
+
825
+ // Check NS records for DNS providers
826
+ const nsRun = await this.commandRunner.run('dig', ['+short', 'NS', domain], { timeout: 5000 });
827
+ if (nsRun.ok && nsRun.stdout) {
828
+ const ns = nsRun.stdout.toLowerCase();
829
+
830
+ if (ns.includes('cloudflare')) {
831
+ services.dns = { provider: 'Cloudflare DNS', evidence: 'NS records' };
832
+ } else if (ns.includes('awsdns')) {
833
+ services.dns = { provider: 'AWS Route53', evidence: 'NS records' };
834
+ } else if (ns.includes('googledomains') || ns.includes('ns-cloud')) {
835
+ services.dns = { provider: 'Google Cloud DNS', evidence: 'NS records' };
836
+ } else if (ns.includes('nsone')) {
837
+ services.dns = { provider: 'NS1', evidence: 'NS records' };
838
+ } else if (ns.includes('dnsimple')) {
839
+ services.dns = { provider: 'DNSimple', evidence: 'NS records' };
840
+ } else if (ns.includes('digitalocean')) {
841
+ services.dns = { provider: 'DigitalOcean DNS', evidence: 'NS records' };
842
+ } else if (ns.includes('namecheap')) {
843
+ services.dns = { provider: 'Namecheap DNS', evidence: 'NS records' };
844
+ } else if (ns.includes('godaddy')) {
845
+ services.dns = { provider: 'GoDaddy DNS', evidence: 'NS records' };
846
+ }
847
+ }
848
+
849
+ return services;
850
+ }
851
+
852
+ /**
853
+ * Detect SaaS from HTTP headers and JavaScript
854
+ */
855
+ async detectSaasFromHTTP(domain, options = {}) {
856
+ const services = {};
857
+
858
+ try {
859
+ const url = `https://${domain}`;
860
+ const response = await fetch(url, {
861
+ headers: {
862
+ 'User-Agent': this.config.curl?.userAgent || 'Mozilla/5.0 (compatible; ReconBot/1.0)'
863
+ },
864
+ signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined
865
+ });
866
+
867
+ // Check headers
868
+ const headers = Object.fromEntries(response.headers.entries());
869
+
870
+ if (headers['server']) {
871
+ services.server = {
872
+ software: headers['server'],
873
+ evidence: 'Server header'
874
+ };
875
+
876
+ // Detect server-specific services
877
+ const server = headers['server'].toLowerCase();
878
+ if (server.includes('cloudflare')) {
879
+ services.cdn = services.cdn || { provider: 'Cloudflare', evidence: 'Server header' };
880
+ } else if (server.includes('vercel')) {
881
+ services.hosting = { provider: 'Vercel', evidence: 'Server header' };
882
+ } else if (server.includes('netlify')) {
883
+ services.hosting = { provider: 'Netlify', evidence: 'Server header' };
884
+ }
885
+ }
886
+
887
+ if (headers['x-powered-by']) {
888
+ services.framework = {
889
+ name: headers['x-powered-by'],
890
+ evidence: 'X-Powered-By header'
891
+ };
892
+ }
893
+
894
+ // CDN/hosting-specific headers
895
+ if (headers['cf-ray']) {
896
+ services.cdn = services.cdn || { provider: 'Cloudflare', evidence: 'CF-Ray header' };
897
+ }
898
+ if (headers['x-vercel-id'] || headers['x-vercel-cache']) {
899
+ services.hosting = { provider: 'Vercel', evidence: 'Vercel headers' };
900
+ }
901
+ if (headers['x-nf-request-id']) {
902
+ services.hosting = { provider: 'Netlify', evidence: 'Netlify headers' };
903
+ }
904
+ if (headers['x-amz-cf-id'] || headers['x-amz-request-id']) {
905
+ services.cloud = { provider: 'AWS', evidence: 'AWS headers' };
906
+ }
907
+
908
+ // Check body for analytics/tracking/SaaS
909
+ const html = await response.text();
910
+
911
+ // Analytics
912
+ const analytics = [];
913
+ if (html.includes('google-analytics') || html.includes('gtag.js') || html.includes('ga.js')) {
914
+ analytics.push({ provider: 'Google Analytics', evidence: 'JavaScript tag' });
915
+ }
916
+ if (html.includes('googletagmanager')) {
917
+ analytics.push({ provider: 'Google Tag Manager', evidence: 'JavaScript tag' });
918
+ }
919
+ if (html.includes('segment.com') || html.includes('analytics.js')) {
920
+ analytics.push({ provider: 'Segment', evidence: 'JavaScript tag' });
921
+ }
922
+ if (html.includes('mixpanel')) {
923
+ analytics.push({ provider: 'Mixpanel', evidence: 'JavaScript tag' });
924
+ }
925
+ if (html.includes('amplitude')) {
926
+ analytics.push({ provider: 'Amplitude', evidence: 'JavaScript tag' });
927
+ }
928
+ if (html.includes('heap.io') || html.includes('heapanalytics')) {
929
+ analytics.push({ provider: 'Heap Analytics', evidence: 'JavaScript tag' });
930
+ }
931
+ if (html.includes('matomo') || html.includes('piwik')) {
932
+ analytics.push({ provider: 'Matomo', evidence: 'JavaScript tag' });
933
+ }
934
+ if (html.includes('plausible.io')) {
935
+ analytics.push({ provider: 'Plausible', evidence: 'JavaScript tag' });
936
+ }
937
+ if (analytics.length > 0) {
938
+ services.analytics = analytics;
939
+ }
940
+
941
+ // Heatmap / Session Recording
942
+ const heatmap = [];
943
+ if (html.includes('hotjar')) {
944
+ heatmap.push({ provider: 'Hotjar', evidence: 'JavaScript tag' });
945
+ }
946
+ if (html.includes('fullstory')) {
947
+ heatmap.push({ provider: 'FullStory', evidence: 'JavaScript tag' });
948
+ }
949
+ if (html.includes('logrocket')) {
950
+ heatmap.push({ provider: 'LogRocket', evidence: 'JavaScript tag' });
951
+ }
952
+ if (html.includes('smartlook')) {
953
+ heatmap.push({ provider: 'Smartlook', evidence: 'JavaScript tag' });
954
+ }
955
+ if (html.includes('mouseflow')) {
956
+ heatmap.push({ provider: 'Mouseflow', evidence: 'JavaScript tag' });
957
+ }
958
+ if (heatmap.length > 0) {
959
+ services.heatmap = heatmap;
960
+ }
961
+
962
+ // Chat / Customer Support
963
+ const chat = [];
964
+ if (html.includes('intercom')) {
965
+ chat.push({ provider: 'Intercom', evidence: 'JavaScript tag' });
966
+ }
967
+ if (html.includes('drift') && html.includes('drift.com')) {
968
+ chat.push({ provider: 'Drift', evidence: 'JavaScript tag' });
969
+ }
970
+ if (html.includes('zendesk')) {
971
+ chat.push({ provider: 'Zendesk', evidence: 'JavaScript tag' });
972
+ }
973
+ if (html.includes('livechat')) {
974
+ chat.push({ provider: 'LiveChat', evidence: 'JavaScript tag' });
975
+ }
976
+ if (html.includes('crisp.chat')) {
977
+ chat.push({ provider: 'Crisp', evidence: 'JavaScript tag' });
978
+ }
979
+ if (html.includes('tawk.to')) {
980
+ chat.push({ provider: 'Tawk.to', evidence: 'JavaScript tag' });
981
+ }
982
+ if (html.includes('olark')) {
983
+ chat.push({ provider: 'Olark', evidence: 'JavaScript tag' });
984
+ }
985
+ if (chat.length > 0) {
986
+ services.chat = chat;
987
+ }
988
+
989
+ // Error Tracking / Monitoring
990
+ const monitoring = [];
991
+ if (html.includes('sentry.io') || html.includes('sentry-cdn')) {
992
+ monitoring.push({ provider: 'Sentry', evidence: 'JavaScript tag' });
993
+ }
994
+ if (html.includes('bugsnag')) {
995
+ monitoring.push({ provider: 'Bugsnag', evidence: 'JavaScript tag' });
996
+ }
997
+ if (html.includes('rollbar')) {
998
+ monitoring.push({ provider: 'Rollbar', evidence: 'JavaScript tag' });
999
+ }
1000
+ if (html.includes('newrelic')) {
1001
+ monitoring.push({ provider: 'New Relic', evidence: 'JavaScript tag' });
1002
+ }
1003
+ if (html.includes('datadoghq')) {
1004
+ monitoring.push({ provider: 'Datadog', evidence: 'JavaScript tag' });
1005
+ }
1006
+ if (monitoring.length > 0) {
1007
+ services.monitoring = monitoring;
1008
+ }
1009
+
1010
+ // Payment Processors
1011
+ const payment = [];
1012
+ if (html.includes('stripe.com') || html.includes('stripe.js')) {
1013
+ payment.push({ provider: 'Stripe', evidence: 'JavaScript tag' });
1014
+ }
1015
+ if (html.includes('paypal.com')) {
1016
+ payment.push({ provider: 'PayPal', evidence: 'JavaScript tag' });
1017
+ }
1018
+ if (html.includes('braintree')) {
1019
+ payment.push({ provider: 'Braintree', evidence: 'JavaScript tag' });
1020
+ }
1021
+ if (html.includes('adyen')) {
1022
+ payment.push({ provider: 'Adyen', evidence: 'JavaScript tag' });
1023
+ }
1024
+ if (html.includes('square.com')) {
1025
+ payment.push({ provider: 'Square', evidence: 'JavaScript tag' });
1026
+ }
1027
+ if (payment.length > 0) {
1028
+ services.payment = payment;
1029
+ }
1030
+
1031
+ // Authentication
1032
+ const auth = [];
1033
+ if (html.includes('auth0')) {
1034
+ auth.push({ provider: 'Auth0', evidence: 'JavaScript tag' });
1035
+ }
1036
+ if (html.includes('firebase')) {
1037
+ auth.push({ provider: 'Firebase Auth', evidence: 'JavaScript tag' });
1038
+ }
1039
+ if (html.includes('okta')) {
1040
+ auth.push({ provider: 'Okta', evidence: 'JavaScript tag' });
1041
+ }
1042
+ if (html.includes('clerk.dev') || html.includes('clerk.com')) {
1043
+ auth.push({ provider: 'Clerk', evidence: 'JavaScript tag' });
1044
+ }
1045
+ if (auth.length > 0) {
1046
+ services.auth = auth;
1047
+ }
1048
+
1049
+ // CRM / Marketing
1050
+ const crm = [];
1051
+ if (html.includes('hubspot')) {
1052
+ crm.push({ provider: 'HubSpot', evidence: 'JavaScript tag' });
1053
+ }
1054
+ if (html.includes('salesforce')) {
1055
+ crm.push({ provider: 'Salesforce', evidence: 'JavaScript tag' });
1056
+ }
1057
+ if (html.includes('marketo')) {
1058
+ crm.push({ provider: 'Marketo', evidence: 'JavaScript tag' });
1059
+ }
1060
+ if (html.includes('pardot')) {
1061
+ crm.push({ provider: 'Pardot', evidence: 'JavaScript tag' });
1062
+ }
1063
+ if (html.includes('activecampaign')) {
1064
+ crm.push({ provider: 'ActiveCampaign', evidence: 'JavaScript tag' });
1065
+ }
1066
+ if (crm.length > 0) {
1067
+ services.crm = crm;
1068
+ }
1069
+
1070
+ // A/B Testing / Personalization
1071
+ const abTesting = [];
1072
+ if (html.includes('optimizely')) {
1073
+ abTesting.push({ provider: 'Optimizely', evidence: 'JavaScript tag' });
1074
+ }
1075
+ if (html.includes('vwo.com')) {
1076
+ abTesting.push({ provider: 'VWO', evidence: 'JavaScript tag' });
1077
+ }
1078
+ if (html.includes('launchdarkly')) {
1079
+ abTesting.push({ provider: 'LaunchDarkly', evidence: 'JavaScript tag' });
1080
+ }
1081
+ if (html.includes('split.io')) {
1082
+ abTesting.push({ provider: 'Split', evidence: 'JavaScript tag' });
1083
+ }
1084
+ if (abTesting.length > 0) {
1085
+ services.abTesting = abTesting;
1086
+ }
1087
+
1088
+ // Content / CMS
1089
+ const cms = [];
1090
+ if (html.includes('wordpress') || html.includes('wp-content')) {
1091
+ cms.push({ provider: 'WordPress', evidence: 'HTML structure' });
1092
+ }
1093
+ if (html.includes('contentful')) {
1094
+ cms.push({ provider: 'Contentful', evidence: 'JavaScript tag' });
1095
+ }
1096
+ if (html.includes('sanity.io')) {
1097
+ cms.push({ provider: 'Sanity', evidence: 'JavaScript tag' });
1098
+ }
1099
+ if (html.includes('prismic.io')) {
1100
+ cms.push({ provider: 'Prismic', evidence: 'JavaScript tag' });
1101
+ }
1102
+ if (html.includes('strapi')) {
1103
+ cms.push({ provider: 'Strapi', evidence: 'JavaScript tag' });
1104
+ }
1105
+ if (cms.length > 0) {
1106
+ services.cms = cms;
1107
+ }
1108
+
1109
+ // Social Media Pixels
1110
+ const socialPixels = [];
1111
+ if (html.includes('facebook.net/en_US/fbevents.js') || html.includes('fbq(')) {
1112
+ socialPixels.push({ provider: 'Facebook Pixel', evidence: 'JavaScript tag' });
1113
+ }
1114
+ if (html.includes('linkedin.com/insight')) {
1115
+ socialPixels.push({ provider: 'LinkedIn Insight Tag', evidence: 'JavaScript tag' });
1116
+ }
1117
+ if (html.includes('twitter.com/i/adsct')) {
1118
+ socialPixels.push({ provider: 'Twitter Pixel', evidence: 'JavaScript tag' });
1119
+ }
1120
+ if (html.includes('pinterest.com/ct/')) {
1121
+ socialPixels.push({ provider: 'Pinterest Tag', evidence: 'JavaScript tag' });
1122
+ }
1123
+ if (html.includes('reddit.com/pixel')) {
1124
+ socialPixels.push({ provider: 'Reddit Pixel', evidence: 'JavaScript tag' });
1125
+ }
1126
+ if (socialPixels.length > 0) {
1127
+ services.socialPixels = socialPixels;
1128
+ }
1129
+
1130
+ // Advertising
1131
+ const advertising = [];
1132
+ if (html.includes('googleadservices') || html.includes('googlesyndication')) {
1133
+ advertising.push({ provider: 'Google Ads', evidence: 'JavaScript tag' });
1134
+ }
1135
+ if (html.includes('doubleclick')) {
1136
+ advertising.push({ provider: 'DoubleClick', evidence: 'JavaScript tag' });
1137
+ }
1138
+ if (advertising.length > 0) {
1139
+ services.advertising = advertising;
1140
+ }
1141
+
1142
+ } catch (error) {
1143
+ // Ignore HTTP errors
1144
+ }
1145
+
1146
+ return services;
1147
+ }
1148
+
1149
+ /**
1150
+ * 6. Social Media Mapping
1151
+ * Map company presence across social platforms
1152
+ */
1153
+ async mapSocialMedia(companyName, domain, options = {}) {
1154
+ const result = {
1155
+ status: 'ok',
1156
+ platforms: {}
1157
+ };
1158
+
1159
+ // LinkedIn
1160
+ if (options.linkedin !== false) {
1161
+ result.platforms.linkedin = {
1162
+ status: 'manual',
1163
+ message: 'LinkedIn search requires manual verification or API access',
1164
+ searchUrl: `https://www.linkedin.com/search/results/companies/?keywords=${encodeURIComponent(companyName)}`
1165
+ };
1166
+ }
1167
+
1168
+ // Twitter/X
1169
+ if (options.twitter !== false) {
1170
+ result.platforms.twitter = {
1171
+ status: 'manual',
1172
+ message: 'Twitter search requires API access',
1173
+ searchUrl: `https://twitter.com/search?q=${encodeURIComponent(companyName)}&f=user`
1174
+ };
1175
+ }
1176
+
1177
+ // Facebook
1178
+ if (options.facebook !== false) {
1179
+ result.platforms.facebook = {
1180
+ status: 'manual',
1181
+ message: 'Facebook search requires manual verification',
1182
+ searchUrl: `https://www.facebook.com/search/pages/?q=${encodeURIComponent(companyName)}`
1183
+ };
1184
+ }
1185
+
1186
+ return result;
1187
+ }
1188
+
1189
+ // ========================================
1190
+ // Helper Methods
1191
+ // ========================================
1192
+
1193
+ extractBaseDomain(host) {
1194
+ // Remove subdomain, keep base domain
1195
+ const parts = host.split('.');
1196
+ if (parts.length > 2) {
1197
+ // Handle special TLDs like .co.uk
1198
+ const specialTLDs = ['co.uk', 'com.br', 'co.jp', 'co.za', 'com.mx', 'com.ar'];
1199
+ const lastTwo = parts.slice(-2).join('.');
1200
+
1201
+ if (specialTLDs.includes(lastTwo)) {
1202
+ return parts.slice(-3).join('.');
1203
+ }
1204
+
1205
+ return parts.slice(-2).join('.');
1206
+ }
1207
+ return host;
1208
+ }
1209
+
1210
+ extractCompanyName(domain) {
1211
+ // Extract company name from domain (simple heuristic)
1212
+ return domain.split('.')[0];
1213
+ }
1214
+
1215
+ /**
1216
+ * WhatsMyName - Username enumeration across 400+ sites
1217
+ * Uses JSON data from WhatsMyName project
1218
+ */
1219
+ async runWhatsMyName(username, options = {}) {
1220
+ const results = {
1221
+ status: 'ok',
1222
+ username,
1223
+ found: [],
1224
+ notFound: [],
1225
+ errors: []
1226
+ };
1227
+
1228
+ try {
1229
+ // Fetch WhatsMyName data
1230
+ const wmn_url = 'https://raw.githubusercontent.com/WebBreacher/WhatsMyName/main/wmn-data.json';
1231
+ const response = await fetch(wmn_url, {
1232
+ signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined
1233
+ });
1234
+
1235
+ if (!response.ok) {
1236
+ results.status = 'error';
1237
+ results.message = 'Failed to fetch WhatsMyName data';
1238
+ return results;
1239
+ }
1240
+
1241
+ const data = await response.json();
1242
+ const sites = data.sites || [];
1243
+
1244
+ // Limit sites to check (to avoid rate limiting)
1245
+ const maxSites = options.maxSites || 50;
1246
+ const sitesToCheck = sites.slice(0, maxSites);
1247
+
1248
+ // Check each site
1249
+ for (const site of sitesToCheck) {
1250
+ if (!site.uri_check) continue;
1251
+
1252
+ try {
1253
+ const checkUrl = site.uri_check.replace('{account}', encodeURIComponent(username));
1254
+
1255
+ const siteResponse = await fetch(checkUrl, {
1256
+ method: 'GET',
1257
+ headers: {
1258
+ 'User-Agent': this.config.curl?.userAgent || 'Mozilla/5.0 (compatible; ReconBot/1.0)'
1259
+ },
1260
+ signal: AbortSignal.timeout ? AbortSignal.timeout(5000) : undefined,
1261
+ redirect: 'follow'
1262
+ });
1263
+
1264
+ // Determine if profile exists based on status code
1265
+ const exists = siteResponse.status === 200;
1266
+
1267
+ if (exists) {
1268
+ results.found.push({
1269
+ site: site.name,
1270
+ url: checkUrl,
1271
+ category: site.cat || 'unknown'
1272
+ });
1273
+ } else {
1274
+ results.notFound.push(site.name);
1275
+ }
1276
+
1277
+ } catch (error) {
1278
+ results.errors.push({
1279
+ site: site.name,
1280
+ error: error.message
1281
+ });
1282
+ }
1283
+
1284
+ // Rate limiting - small delay between requests
1285
+ await this.sleep(200);
1286
+ }
1287
+
1288
+ } catch (error) {
1289
+ results.status = 'error';
1290
+ results.message = error.message;
1291
+ }
1292
+
1293
+ return results;
1294
+ }
1295
+
1296
+ /**
1297
+ * Scylla.sh - Free breach data API
1298
+ * Check if email/domain appears in breaches
1299
+ */
1300
+ async checkScylla(email, options = {}) {
1301
+ const results = {
1302
+ status: 'ok',
1303
+ email,
1304
+ breaches: []
1305
+ };
1306
+
1307
+ try {
1308
+ // Scylla.sh API endpoint
1309
+ const url = `https://scylla.sh/search?q=email:${encodeURIComponent(email)}`;
1310
+
1311
+ const response = await fetch(url, {
1312
+ headers: {
1313
+ 'User-Agent': this.config.curl?.userAgent || 'Mozilla/5.0 (compatible; ReconBot/1.0)',
1314
+ 'Accept': 'application/json'
1315
+ },
1316
+ signal: AbortSignal.timeout ? AbortSignal.timeout(15000) : undefined
1317
+ });
1318
+
1319
+ if (!response.ok) {
1320
+ if (response.status === 404) {
1321
+ // No breaches found
1322
+ return results;
1323
+ }
1324
+
1325
+ results.status = 'error';
1326
+ results.message = `Scylla API returned ${response.status}`;
1327
+ return results;
1328
+ }
1329
+
1330
+ const data = await response.json();
1331
+
1332
+ // Parse Scylla response
1333
+ if (Array.isArray(data)) {
1334
+ results.breaches = data.map(breach => ({
1335
+ source: breach.Source || breach.Database || 'Unknown',
1336
+ email: breach.Email || email,
1337
+ username: breach.Username,
1338
+ password: breach.Password ? '[REDACTED]' : null, // Don't store actual passwords
1339
+ hash: breach.Hash,
1340
+ salt: breach.Salt,
1341
+ ip: breach.IP,
1342
+ fields: Object.keys(breach)
1343
+ }));
1344
+ }
1345
+
1346
+ } catch (error) {
1347
+ results.status = 'error';
1348
+ results.message = error.message;
1349
+ }
1350
+
1351
+ return results;
1352
+ }
1353
+
1354
+ deduplicateProfiles(profiles) {
1355
+ const seen = new Set();
1356
+ return profiles.filter(profile => {
1357
+ if (seen.has(profile.url)) {
1358
+ return false;
1359
+ }
1360
+ seen.add(profile.url);
1361
+ return true;
1362
+ });
1363
+ }
1364
+
1365
+ sleep(ms) {
1366
+ return new Promise(resolve => setTimeout(resolve, ms));
1367
+ }
1368
+
1369
+ _truncateOutput(text, maxLength = 10000) {
1370
+ if (!text || text.length <= maxLength) return text;
1371
+ return text.substring(0, maxLength) + '\n... (truncated)';
1372
+ }
1373
+ }