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