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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubdomainsStage
|
|
3
|
+
*
|
|
4
|
+
* Subdomain enumeration with multiple tools:
|
|
5
|
+
* - amass (OWASP, comprehensive)
|
|
6
|
+
* - subfinder (fast, API-based)
|
|
7
|
+
* - assetfinder (passive)
|
|
8
|
+
* - crt.sh (certificate transparency logs)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class SubdomainsStage {
|
|
12
|
+
constructor(plugin) {
|
|
13
|
+
this.plugin = plugin;
|
|
14
|
+
this.commandRunner = plugin.commandRunner;
|
|
15
|
+
this.config = plugin.config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async execute(target, featureConfig = {}) {
|
|
19
|
+
const aggregated = new Set();
|
|
20
|
+
const sources = {};
|
|
21
|
+
|
|
22
|
+
const executeCliCollector = async (name, command, args, parser) => {
|
|
23
|
+
if (!featureConfig[name]) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const run = await this.commandRunner.run(command, args, { timeout: 60000, maxBuffer: 8 * 1024 * 1024 });
|
|
27
|
+
if (!run.ok) {
|
|
28
|
+
sources[name] = {
|
|
29
|
+
status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
|
|
30
|
+
message: run.error?.message || `${command} failed`,
|
|
31
|
+
stderr: run.stderr
|
|
32
|
+
};
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const items = parser(run.stdout, run.stderr);
|
|
36
|
+
items.forEach((item) => aggregated.add(item));
|
|
37
|
+
sources[name] = {
|
|
38
|
+
status: 'ok',
|
|
39
|
+
count: items.length,
|
|
40
|
+
sample: items.slice(0, 10)
|
|
41
|
+
};
|
|
42
|
+
if (this.config.storage.persistRawOutput) {
|
|
43
|
+
sources[name].raw = this._truncateOutput(run.stdout);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await executeCliCollector('amass', 'amass', ['enum', '-d', target.host, '-o', '-'], (stdout) =>
|
|
48
|
+
stdout
|
|
49
|
+
.split(/\r?\n/)
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await executeCliCollector('subfinder', 'subfinder', ['-d', target.host, '-silent'], (stdout) =>
|
|
55
|
+
stdout
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.map((line) => line.trim())
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await executeCliCollector('assetfinder', 'assetfinder', ['--subs-only', target.host], (stdout) =>
|
|
62
|
+
stdout
|
|
63
|
+
.split(/\r?\n/)
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (featureConfig.crtsh) {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`https://crt.sh/?q=%25.${target.host}&output=json`, {
|
|
71
|
+
headers: { 'User-Agent': this.config.curl.userAgent },
|
|
72
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined
|
|
73
|
+
});
|
|
74
|
+
if (response.ok) {
|
|
75
|
+
const data = await response.json();
|
|
76
|
+
const entries = Array.isArray(data) ? data : [];
|
|
77
|
+
const hostnames = entries
|
|
78
|
+
.map((entry) => entry.name_value)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.flatMap((value) => value.split('\n'))
|
|
81
|
+
.map((value) => value.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
hostnames.forEach((hostname) => aggregated.add(hostname));
|
|
84
|
+
sources.crtsh = {
|
|
85
|
+
status: 'ok',
|
|
86
|
+
count: hostnames.length,
|
|
87
|
+
sample: hostnames.slice(0, 10)
|
|
88
|
+
};
|
|
89
|
+
} else {
|
|
90
|
+
sources.crtsh = {
|
|
91
|
+
status: 'error',
|
|
92
|
+
message: `crt.sh responded with status ${response.status}`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
sources.crtsh = {
|
|
97
|
+
status: 'error',
|
|
98
|
+
message: error?.message || 'crt.sh lookup failed'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const list = Array.from(aggregated).sort();
|
|
104
|
+
|
|
105
|
+
// Check for subdomain takeover vulnerabilities
|
|
106
|
+
let takeoverResults = null;
|
|
107
|
+
if (featureConfig.checkTakeover && list.length > 0) {
|
|
108
|
+
takeoverResults = await this.checkSubdomainTakeover(list, featureConfig);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
_individual: sources,
|
|
113
|
+
_aggregated: {
|
|
114
|
+
status: list.length > 0 ? 'ok' : 'empty',
|
|
115
|
+
total: list.length,
|
|
116
|
+
list,
|
|
117
|
+
sources,
|
|
118
|
+
takeover: takeoverResults
|
|
119
|
+
},
|
|
120
|
+
status: list.length > 0 ? 'ok' : 'empty',
|
|
121
|
+
total: list.length,
|
|
122
|
+
list,
|
|
123
|
+
sources,
|
|
124
|
+
takeover: takeoverResults
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check for subdomain takeover vulnerabilities
|
|
130
|
+
* @param {Array<string>} subdomains - List of subdomains
|
|
131
|
+
* @param {Object} options - Takeover check options
|
|
132
|
+
* @returns {Promise<Object>} Takeover check results
|
|
133
|
+
*/
|
|
134
|
+
async checkSubdomainTakeover(subdomains, options = {}) {
|
|
135
|
+
const results = {
|
|
136
|
+
status: 'ok',
|
|
137
|
+
vulnerable: [],
|
|
138
|
+
checked: 0,
|
|
139
|
+
errors: []
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Known fingerprints for subdomain takeover
|
|
143
|
+
const takeoverFingerprints = {
|
|
144
|
+
'github': {
|
|
145
|
+
cname: 'github.io',
|
|
146
|
+
response: ['There isn\'t a GitHub Pages site here', 'For root URLs'],
|
|
147
|
+
severity: 'high'
|
|
148
|
+
},
|
|
149
|
+
'heroku': {
|
|
150
|
+
cname: 'herokuapp.com',
|
|
151
|
+
response: ['No such app', 'There\'s nothing here'],
|
|
152
|
+
severity: 'high'
|
|
153
|
+
},
|
|
154
|
+
'aws-s3': {
|
|
155
|
+
cname: 's3.amazonaws.com',
|
|
156
|
+
response: ['NoSuchBucket', 'The specified bucket does not exist'],
|
|
157
|
+
severity: 'high'
|
|
158
|
+
},
|
|
159
|
+
'aws-cloudfront': {
|
|
160
|
+
cname: 'cloudfront.net',
|
|
161
|
+
response: ['The request could not be satisfied', 'Bad request'],
|
|
162
|
+
severity: 'medium'
|
|
163
|
+
},
|
|
164
|
+
'azure': {
|
|
165
|
+
cname: 'azurewebsites.net',
|
|
166
|
+
response: ['404 Web Site not found', 'Error 404'],
|
|
167
|
+
severity: 'high'
|
|
168
|
+
},
|
|
169
|
+
'bitbucket': {
|
|
170
|
+
cname: 'bitbucket.io',
|
|
171
|
+
response: ['Repository not found'],
|
|
172
|
+
severity: 'high'
|
|
173
|
+
},
|
|
174
|
+
'fastly': {
|
|
175
|
+
cname: 'fastly.net',
|
|
176
|
+
response: ['Fastly error: unknown domain'],
|
|
177
|
+
severity: 'medium'
|
|
178
|
+
},
|
|
179
|
+
'shopify': {
|
|
180
|
+
cname: 'myshopify.com',
|
|
181
|
+
response: ['Sorry, this shop is currently unavailable'],
|
|
182
|
+
severity: 'high'
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const maxSubdomains = options.maxSubdomains || 50;
|
|
187
|
+
const subdomainsToCheck = subdomains.slice(0, maxSubdomains);
|
|
188
|
+
|
|
189
|
+
for (const subdomain of subdomainsToCheck) {
|
|
190
|
+
try {
|
|
191
|
+
results.checked++;
|
|
192
|
+
|
|
193
|
+
// Check CNAME record
|
|
194
|
+
const cname = await this.resolveCNAME(subdomain);
|
|
195
|
+
|
|
196
|
+
if (cname) {
|
|
197
|
+
// Check if CNAME matches known vulnerable patterns
|
|
198
|
+
for (const [provider, fingerprint] of Object.entries(takeoverFingerprints)) {
|
|
199
|
+
if (cname.toLowerCase().includes(fingerprint.cname)) {
|
|
200
|
+
// Fetch the subdomain to check for error responses
|
|
201
|
+
const httpCheck = await this.checkHttpResponse(subdomain);
|
|
202
|
+
|
|
203
|
+
if (httpCheck && httpCheck.status >= 400) {
|
|
204
|
+
// Check if response contains takeover indicators
|
|
205
|
+
const isVulnerable = fingerprint.response.some(indicator =>
|
|
206
|
+
httpCheck.body?.toLowerCase().includes(indicator.toLowerCase())
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (isVulnerable) {
|
|
210
|
+
results.vulnerable.push({
|
|
211
|
+
subdomain,
|
|
212
|
+
provider,
|
|
213
|
+
cname,
|
|
214
|
+
severity: fingerprint.severity,
|
|
215
|
+
evidence: `CNAME points to ${cname} but returns ${httpCheck.status}`,
|
|
216
|
+
status: httpCheck.status,
|
|
217
|
+
recommendation: `Claim the ${provider} resource or remove the DNS record`
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
results.errors.push({
|
|
226
|
+
subdomain,
|
|
227
|
+
error: error.message
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (results.vulnerable.length > 0) {
|
|
233
|
+
results.status = 'vulnerable';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve CNAME record for a domain
|
|
241
|
+
* @param {string} domain - Domain to resolve
|
|
242
|
+
* @returns {Promise<string|null>} CNAME record or null
|
|
243
|
+
*/
|
|
244
|
+
async resolveCNAME(domain) {
|
|
245
|
+
const run = await this.commandRunner.run('dig', ['+short', 'CNAME', domain], {
|
|
246
|
+
timeout: 5000
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (run.ok && run.stdout) {
|
|
250
|
+
const cname = run.stdout.trim().replace(/\.$/, ''); // Remove trailing dot
|
|
251
|
+
return cname || null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check HTTP response for a subdomain
|
|
259
|
+
* @param {string} subdomain - Subdomain to check
|
|
260
|
+
* @returns {Promise<Object|null>} HTTP response details
|
|
261
|
+
*/
|
|
262
|
+
async checkHttpResponse(subdomain) {
|
|
263
|
+
try {
|
|
264
|
+
const run = await this.commandRunner.run('curl', [
|
|
265
|
+
'-sL',
|
|
266
|
+
'-w', '%{http_code}',
|
|
267
|
+
'-m', '10',
|
|
268
|
+
`https://${subdomain}`
|
|
269
|
+
], {
|
|
270
|
+
timeout: 15000,
|
|
271
|
+
maxBuffer: 1024 * 1024 // 1MB max
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (run.ok) {
|
|
275
|
+
const output = run.stdout;
|
|
276
|
+
const statusMatch = output.match(/(\d{3})$/);
|
|
277
|
+
const status = statusMatch ? parseInt(statusMatch[1]) : 0;
|
|
278
|
+
const body = output.replace(/\d{3}$/, '');
|
|
279
|
+
|
|
280
|
+
return { status, body };
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// Ignore errors, subdomain might not be accessible
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_truncateOutput(text, maxLength = 10000) {
|
|
290
|
+
if (!text || text.length <= maxLength) {
|
|
291
|
+
return text;
|
|
292
|
+
}
|
|
293
|
+
return text.substring(0, maxLength) + '\n... (truncated)';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TlsAuditStage
|
|
3
|
+
*
|
|
4
|
+
* TLS/SSL security auditing:
|
|
5
|
+
* - openssl (basic TLS info)
|
|
6
|
+
* - sslyze (comprehensive TLS scanner)
|
|
7
|
+
* - testssl.sh (detailed SSL/TLS testing)
|
|
8
|
+
* - sslscan (fast SSL/TLS scanner)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class TlsAuditStage {
|
|
12
|
+
constructor(plugin) {
|
|
13
|
+
this.plugin = plugin;
|
|
14
|
+
this.commandRunner = plugin.commandRunner;
|
|
15
|
+
this.config = plugin.config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async execute(target, featureConfig = {}) {
|
|
19
|
+
const tools = {};
|
|
20
|
+
const port = target.port || 443;
|
|
21
|
+
|
|
22
|
+
const executeAudit = async (name, command, args) => {
|
|
23
|
+
const run = await this.commandRunner.run(command, args, {
|
|
24
|
+
timeout: featureConfig.timeout ?? 20000,
|
|
25
|
+
maxBuffer: 4 * 1024 * 1024
|
|
26
|
+
});
|
|
27
|
+
if (!run.ok) {
|
|
28
|
+
tools[name] = {
|
|
29
|
+
status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
|
|
30
|
+
message: run.error?.message || `${command} failed`,
|
|
31
|
+
stderr: run.stderr
|
|
32
|
+
};
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
tools[name] = {
|
|
36
|
+
status: 'ok'
|
|
37
|
+
};
|
|
38
|
+
if (this.config.storage.persistRawOutput) {
|
|
39
|
+
tools[name].raw = this._truncateOutput(run.stdout);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (featureConfig.openssl) {
|
|
44
|
+
await executeAudit('openssl', 'openssl', ['s_client', '-servername', target.host, '-connect', `${target.host}:${port}`, '-brief']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (featureConfig.sslyze) {
|
|
48
|
+
await executeAudit('sslyze', 'sslyze', [target.host]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (featureConfig.testssl) {
|
|
52
|
+
await executeAudit('testssl', 'testssl.sh', ['--quiet', `${target.host}:${port}`]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (featureConfig.sslscan) {
|
|
56
|
+
await executeAudit('sslscan', 'sslscan', [`${target.host}:${port}`]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Object.keys(tools).length === 0) {
|
|
60
|
+
return { status: 'skipped' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
_individual: tools,
|
|
65
|
+
_aggregated: {
|
|
66
|
+
status: Object.values(tools).some((tool) => tool.status === 'ok') ? 'ok' : 'empty',
|
|
67
|
+
tools
|
|
68
|
+
},
|
|
69
|
+
status: Object.values(tools).some((tool) => tool.status === 'ok') ? 'ok' : 'empty',
|
|
70
|
+
tools
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_truncateOutput(text, maxLength = 10000) {
|
|
75
|
+
if (!text || text.length <= maxLength) return text;
|
|
76
|
+
return text.substring(0, maxLength) + '\n... (truncated)';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VulnerabilityStage
|
|
3
|
+
*
|
|
4
|
+
* Vulnerability scanning:
|
|
5
|
+
* - nikto (web server scanner)
|
|
6
|
+
* - wpscan (WordPress scanner)
|
|
7
|
+
* - droopescan (Drupal/Joomla scanner)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class VulnerabilityStage {
|
|
11
|
+
constructor(plugin) {
|
|
12
|
+
this.plugin = plugin;
|
|
13
|
+
this.commandRunner = plugin.commandRunner;
|
|
14
|
+
this.config = plugin.config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async execute(target, featureConfig = {}) {
|
|
18
|
+
const tools = {};
|
|
19
|
+
|
|
20
|
+
const executeScanner = async (name, command, args) => {
|
|
21
|
+
const run = await this.commandRunner.run(command, args, {
|
|
22
|
+
timeout: featureConfig.timeout ?? 60000,
|
|
23
|
+
maxBuffer: 8 * 1024 * 1024
|
|
24
|
+
});
|
|
25
|
+
if (!run.ok) {
|
|
26
|
+
tools[name] = {
|
|
27
|
+
status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
|
|
28
|
+
message: run.error?.message || `${command} failed`,
|
|
29
|
+
stderr: run.stderr
|
|
30
|
+
};
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
tools[name] = {
|
|
34
|
+
status: 'ok'
|
|
35
|
+
};
|
|
36
|
+
if (this.config.storage.persistRawOutput) {
|
|
37
|
+
tools[name].raw = this._truncateOutput(run.stdout);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (featureConfig.nikto) {
|
|
42
|
+
await executeScanner('nikto', 'nikto', ['-h', this._buildUrl(target), '-ask', 'no']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (featureConfig.wpscan) {
|
|
46
|
+
await executeScanner('wpscan', 'wpscan', ['--url', this._buildUrl(target), '--random-user-agent']);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (featureConfig.droopescan) {
|
|
50
|
+
await executeScanner('droopescan', 'droopescan', ['scan', 'drupal', '-u', this._buildUrl(target)]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Object.keys(tools).length === 0) {
|
|
54
|
+
return { status: 'skipped' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
_individual: tools,
|
|
59
|
+
_aggregated: {
|
|
60
|
+
status: Object.values(tools).some((tool) => tool.status === 'ok') ? 'ok' : 'empty',
|
|
61
|
+
tools
|
|
62
|
+
},
|
|
63
|
+
status: Object.values(tools).some((tool) => tool.status === 'ok') ? 'ok' : 'empty',
|
|
64
|
+
tools
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_buildUrl(target) {
|
|
69
|
+
const protocol = target.protocol || 'https';
|
|
70
|
+
const port = target.port && target.port !== (protocol === 'http' ? 80 : 443) ? `:${target.port}` : '';
|
|
71
|
+
return `${protocol}://${target.host}${port}${target.path || ''}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_truncateOutput(text, maxLength = 10000) {
|
|
75
|
+
if (!text || text.length <= maxLength) return text;
|
|
76
|
+
return text.substring(0, maxLength) + '\n... (truncated)';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebDiscoveryStage
|
|
3
|
+
*
|
|
4
|
+
* Directory and endpoint fuzzing:
|
|
5
|
+
* - ffuf (fast, flexible)
|
|
6
|
+
* - feroxbuster (recursive)
|
|
7
|
+
* - gobuster (reliable)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class WebDiscoveryStage {
|
|
11
|
+
constructor(plugin) {
|
|
12
|
+
this.plugin = plugin;
|
|
13
|
+
this.commandRunner = plugin.commandRunner;
|
|
14
|
+
this.config = plugin.config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async execute(target, featureConfig = {}) {
|
|
18
|
+
if (!featureConfig) {
|
|
19
|
+
return { status: 'disabled' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tools = {};
|
|
23
|
+
const discovered = {};
|
|
24
|
+
const allPaths = new Set();
|
|
25
|
+
const wordlist = featureConfig.wordlist;
|
|
26
|
+
const threads = featureConfig.threads ?? 50;
|
|
27
|
+
|
|
28
|
+
const runDirBuster = async (name, command, args) => {
|
|
29
|
+
const run = await this.commandRunner.run(command, args, {
|
|
30
|
+
timeout: featureConfig.timeout ?? 60000,
|
|
31
|
+
maxBuffer: 8 * 1024 * 1024
|
|
32
|
+
});
|
|
33
|
+
if (!run.ok) {
|
|
34
|
+
tools[name] = {
|
|
35
|
+
status: run.error?.code === 'ENOENT' ? 'unavailable' : 'error',
|
|
36
|
+
message: run.error?.message || `${command} failed`,
|
|
37
|
+
stderr: run.stderr
|
|
38
|
+
};
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const findings = run.stdout
|
|
42
|
+
.split(/\r?\n/)
|
|
43
|
+
.map((line) => line.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
discovered[name] = findings;
|
|
46
|
+
findings.forEach((item) => allPaths.add(item));
|
|
47
|
+
tools[name] = {
|
|
48
|
+
status: 'ok',
|
|
49
|
+
count: findings.length,
|
|
50
|
+
sample: findings.slice(0, 10)
|
|
51
|
+
};
|
|
52
|
+
if (this.config.storage.persistRawOutput) {
|
|
53
|
+
tools[name].raw = this._truncateOutput(run.stdout);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (featureConfig.ffuf && wordlist) {
|
|
58
|
+
await runDirBuster('ffuf', 'ffuf', ['-u', `${this._buildUrl(target)}/FUZZ`, '-w', wordlist, '-t', String(threads), '-mc', '200,204,301,302,307,401,403']);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (featureConfig.feroxbuster && wordlist) {
|
|
62
|
+
await runDirBuster('feroxbuster', 'feroxbuster', ['-u', this._buildUrl(target), '-w', wordlist, '--threads', String(threads), '--silent']);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (featureConfig.gobuster && wordlist) {
|
|
66
|
+
await runDirBuster('gobuster', 'gobuster', ['dir', '-u', this._buildUrl(target), '-w', wordlist, '-t', String(threads)]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const total = Object.values(discovered).reduce((acc, list) => acc + list.length, 0);
|
|
70
|
+
|
|
71
|
+
if (!total) {
|
|
72
|
+
return {
|
|
73
|
+
status: wordlist ? 'empty' : 'skipped',
|
|
74
|
+
message: wordlist ? 'No endpoints discovered' : 'Wordlist not provided',
|
|
75
|
+
tools
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const paths = Array.from(allPaths);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
_individual: tools,
|
|
83
|
+
_aggregated: {
|
|
84
|
+
status: 'ok',
|
|
85
|
+
total,
|
|
86
|
+
tools,
|
|
87
|
+
paths
|
|
88
|
+
},
|
|
89
|
+
status: 'ok',
|
|
90
|
+
total,
|
|
91
|
+
tools,
|
|
92
|
+
paths
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_buildUrl(target) {
|
|
97
|
+
const protocol = target.protocol || 'https';
|
|
98
|
+
const port = target.port && target.port !== this._defaultPortForProtocol(protocol)
|
|
99
|
+
? `:${target.port}`
|
|
100
|
+
: '';
|
|
101
|
+
const path = target.path || '';
|
|
102
|
+
return `${protocol}://${target.host}${port}${path}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_defaultPortForProtocol(protocol) {
|
|
106
|
+
return protocol === 'http' ? 80 : protocol === 'https' ? 443 : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_truncateOutput(text, maxLength = 10000) {
|
|
110
|
+
if (!text || text.length <= maxLength) return text;
|
|
111
|
+
return text.substring(0, maxLength) + '\n... (truncated)';
|
|
112
|
+
}
|
|
113
|
+
}
|