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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TargetManager
|
|
3
|
+
*
|
|
4
|
+
* Handles dynamic target management:
|
|
5
|
+
* - CRUD operations for targets
|
|
6
|
+
* - Target normalization
|
|
7
|
+
* - Resource persistence
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class TargetManager {
|
|
11
|
+
constructor(plugin) {
|
|
12
|
+
this.plugin = plugin;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a new target to the monitoring list
|
|
17
|
+
*/
|
|
18
|
+
async add(targetInput, options = {}) {
|
|
19
|
+
const normalized = this._normalizeTarget(targetInput);
|
|
20
|
+
const targetId = normalized.host;
|
|
21
|
+
|
|
22
|
+
// Check if target already exists
|
|
23
|
+
const existing = await this.get(targetInput);
|
|
24
|
+
if (existing) {
|
|
25
|
+
throw new Error(`Target "${targetId}" already exists. Use updateTarget() to modify it.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const targetRecord = {
|
|
29
|
+
id: targetId,
|
|
30
|
+
target: targetInput,
|
|
31
|
+
enabled: options.enabled !== false,
|
|
32
|
+
behavior: options.behavior || this.plugin.config.behavior,
|
|
33
|
+
features: options.features || {},
|
|
34
|
+
tools: options.tools || null,
|
|
35
|
+
schedule: options.schedule || null,
|
|
36
|
+
metadata: options.metadata || {},
|
|
37
|
+
lastScanAt: null,
|
|
38
|
+
lastScanStatus: null,
|
|
39
|
+
scanCount: 0,
|
|
40
|
+
addedBy: options.addedBy || 'manual',
|
|
41
|
+
tags: options.tags || [],
|
|
42
|
+
createdAt: new Date().toISOString(),
|
|
43
|
+
updatedAt: new Date().toISOString()
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const targetsResource = await this._getResource();
|
|
47
|
+
await targetsResource.insert(targetRecord);
|
|
48
|
+
|
|
49
|
+
this.plugin.emit('recon:target-added', {
|
|
50
|
+
targetId,
|
|
51
|
+
target: targetInput,
|
|
52
|
+
enabled: targetRecord.enabled,
|
|
53
|
+
behavior: targetRecord.behavior
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return targetRecord;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove a target from the monitoring list
|
|
61
|
+
*/
|
|
62
|
+
async remove(targetInput) {
|
|
63
|
+
const normalized = this._normalizeTarget(targetInput);
|
|
64
|
+
const targetId = normalized.host;
|
|
65
|
+
|
|
66
|
+
const targetsResource = await this._getResource();
|
|
67
|
+
await targetsResource.delete(targetId);
|
|
68
|
+
|
|
69
|
+
this.plugin.emit('recon:target-removed', {
|
|
70
|
+
targetId,
|
|
71
|
+
target: targetInput
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return { targetId, removed: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update target configuration
|
|
79
|
+
*/
|
|
80
|
+
async update(targetInput, updates) {
|
|
81
|
+
const normalized = this._normalizeTarget(targetInput);
|
|
82
|
+
const targetId = normalized.host;
|
|
83
|
+
|
|
84
|
+
const existing = await this.get(targetInput);
|
|
85
|
+
if (!existing) {
|
|
86
|
+
throw new Error(`Target "${targetId}" not found. Use addTarget() to create it.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const updatedRecord = {
|
|
90
|
+
...existing,
|
|
91
|
+
...updates,
|
|
92
|
+
updatedAt: new Date().toISOString()
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const targetsResource = await this._getResource();
|
|
96
|
+
await targetsResource.update(targetId, updatedRecord);
|
|
97
|
+
|
|
98
|
+
this.plugin.emit('recon:target-updated', {
|
|
99
|
+
targetId,
|
|
100
|
+
updates
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return updatedRecord;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* List all configured targets
|
|
108
|
+
*/
|
|
109
|
+
async list(options = {}) {
|
|
110
|
+
const includeDisabled = options.includeDisabled !== false;
|
|
111
|
+
const fromResource = options.fromResource !== false;
|
|
112
|
+
const limit = options.limit || 1000;
|
|
113
|
+
|
|
114
|
+
if (!fromResource) {
|
|
115
|
+
// Fallback to config targets (legacy mode)
|
|
116
|
+
return this._normalizeConfigTargets();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const targetsResource = await this._getResource();
|
|
121
|
+
let targets = await targetsResource.list({ limit });
|
|
122
|
+
|
|
123
|
+
if (!includeDisabled) {
|
|
124
|
+
targets = targets.filter(t => t.enabled !== false);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return targets;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Fallback to config targets if resource not available
|
|
130
|
+
return this._normalizeConfigTargets();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get details of a specific target
|
|
136
|
+
*/
|
|
137
|
+
async get(targetInput) {
|
|
138
|
+
const normalized = this._normalizeTarget(targetInput);
|
|
139
|
+
const targetId = normalized.host;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const targetsResource = await this._getResource();
|
|
143
|
+
return await targetsResource.get(targetId);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// Check if it exists in config targets (legacy mode)
|
|
146
|
+
const configTargets = this._normalizeConfigTargets();
|
|
147
|
+
return configTargets.find(t => t.id === targetId) || null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Increment scan count and update last scan metadata
|
|
153
|
+
*/
|
|
154
|
+
async updateScanMetadata(targetId, report) {
|
|
155
|
+
try {
|
|
156
|
+
const target = await this.get(targetId);
|
|
157
|
+
if (!target) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await this.update(targetId, {
|
|
162
|
+
lastScanAt: report.endedAt,
|
|
163
|
+
lastScanStatus: report.status,
|
|
164
|
+
scanCount: (target.scanCount || 0) + 1
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Ignore errors (target might not be in resource)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ========================================
|
|
172
|
+
// Private Helper Methods
|
|
173
|
+
// ========================================
|
|
174
|
+
|
|
175
|
+
async _getResource() {
|
|
176
|
+
// Get namespaced targets resource
|
|
177
|
+
const namespace = this.plugin.namespace || '';
|
|
178
|
+
const resourceName = namespace === ''
|
|
179
|
+
? 'plg_recon_targets'
|
|
180
|
+
: `plg_recon_${namespace}_targets`;
|
|
181
|
+
|
|
182
|
+
return await this.plugin.database.getResource(resourceName);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_normalizeTarget(targetInput) {
|
|
186
|
+
if (!targetInput || typeof targetInput !== 'string') {
|
|
187
|
+
throw new Error('Target must be a non-empty string');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let url;
|
|
191
|
+
try {
|
|
192
|
+
url = new URL(targetInput.includes('://') ? targetInput : `https://${targetInput}`);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
url = new URL(`https://${targetInput}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const protocol = url.protocol ? url.protocol.replace(':', '') : null;
|
|
198
|
+
const host = url.hostname || targetInput;
|
|
199
|
+
const port = url.port ? Number(url.port) : this._defaultPortForProtocol(protocol);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
original: targetInput,
|
|
203
|
+
host,
|
|
204
|
+
protocol,
|
|
205
|
+
port,
|
|
206
|
+
path: url.pathname === '/' ? null : url.pathname
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_defaultPortForProtocol(protocol) {
|
|
211
|
+
switch (protocol) {
|
|
212
|
+
case 'http':
|
|
213
|
+
return 80;
|
|
214
|
+
case 'https':
|
|
215
|
+
return 443;
|
|
216
|
+
case 'ftp':
|
|
217
|
+
return 21;
|
|
218
|
+
case 'ssh':
|
|
219
|
+
return 22;
|
|
220
|
+
default:
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_normalizeConfigTargets() {
|
|
226
|
+
const targets = this.plugin.config.targets || [];
|
|
227
|
+
return targets.map((entry, index) => {
|
|
228
|
+
if (typeof entry === 'string') {
|
|
229
|
+
const normalized = this._normalizeTarget(entry);
|
|
230
|
+
return {
|
|
231
|
+
id: normalized.host,
|
|
232
|
+
target: entry,
|
|
233
|
+
enabled: true,
|
|
234
|
+
behavior: this.plugin.config.behavior,
|
|
235
|
+
features: {},
|
|
236
|
+
tools: null,
|
|
237
|
+
schedule: null,
|
|
238
|
+
metadata: {},
|
|
239
|
+
lastScanAt: null,
|
|
240
|
+
lastScanStatus: null,
|
|
241
|
+
scanCount: 0,
|
|
242
|
+
addedBy: 'config',
|
|
243
|
+
tags: [],
|
|
244
|
+
createdAt: new Date().toISOString(),
|
|
245
|
+
updatedAt: new Date().toISOString()
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (entry && typeof entry === 'object') {
|
|
250
|
+
const target = entry.target || entry.host || entry.domain;
|
|
251
|
+
const normalized = this._normalizeTarget(target);
|
|
252
|
+
return {
|
|
253
|
+
id: normalized.host,
|
|
254
|
+
target,
|
|
255
|
+
enabled: entry.enabled !== false,
|
|
256
|
+
behavior: entry.behavior || this.plugin.config.behavior,
|
|
257
|
+
features: entry.features || {},
|
|
258
|
+
tools: entry.tools || null,
|
|
259
|
+
schedule: entry.schedule || null,
|
|
260
|
+
metadata: entry.metadata || {},
|
|
261
|
+
lastScanAt: null,
|
|
262
|
+
lastScanStatus: null,
|
|
263
|
+
scanCount: 0,
|
|
264
|
+
addedBy: 'config',
|
|
265
|
+
tags: entry.tags || [],
|
|
266
|
+
createdAt: new Date().toISOString(),
|
|
267
|
+
updatedAt: new Date().toISOString()
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
throw new Error(`Invalid target configuration at index ${index}`);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASNStage
|
|
3
|
+
*
|
|
4
|
+
* ASN (Autonomous System Number) and Network Intelligence
|
|
5
|
+
*
|
|
6
|
+
* Discovers:
|
|
7
|
+
* - ASN ownership and organization
|
|
8
|
+
* - IP ranges (CIDR blocks)
|
|
9
|
+
* - Network provider information
|
|
10
|
+
* - BGP routing data
|
|
11
|
+
*
|
|
12
|
+
* Uses 100% free APIs:
|
|
13
|
+
* - iptoasn.com (unlimited, free)
|
|
14
|
+
* - hackertarget.com (100 queries/day free)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class ASNStage {
|
|
18
|
+
constructor(plugin) {
|
|
19
|
+
this.plugin = plugin;
|
|
20
|
+
this.commandRunner = plugin.commandRunner;
|
|
21
|
+
this.config = plugin.config;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute ASN lookup
|
|
26
|
+
* @param {Object} target - Target object with host property
|
|
27
|
+
* @param {Object} options - ASN options
|
|
28
|
+
* @returns {Promise<Object>} ASN results
|
|
29
|
+
*/
|
|
30
|
+
async execute(target, options = {}) {
|
|
31
|
+
const result = {
|
|
32
|
+
status: 'ok',
|
|
33
|
+
host: target.host,
|
|
34
|
+
ipAddresses: [],
|
|
35
|
+
asns: [],
|
|
36
|
+
networks: [],
|
|
37
|
+
organizations: new Set(),
|
|
38
|
+
errors: {}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Track individual tool results for artifact persistence
|
|
42
|
+
const individual = {
|
|
43
|
+
iptoasn: { status: 'ok', results: [] },
|
|
44
|
+
hackertarget: { status: 'ok', results: [] },
|
|
45
|
+
dig: { status: 'ok', ipv4: [], ipv6: [] }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Step 1: Resolve host to IP addresses
|
|
49
|
+
const ipAddresses = await this.resolveHostToIPs(target.host, individual.dig);
|
|
50
|
+
result.ipAddresses = ipAddresses;
|
|
51
|
+
|
|
52
|
+
if (ipAddresses.length === 0) {
|
|
53
|
+
result.status = 'error';
|
|
54
|
+
result.errors.dns = 'Could not resolve host to IP addresses';
|
|
55
|
+
individual.dig.status = 'error';
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
_individual: individual,
|
|
59
|
+
_aggregated: result,
|
|
60
|
+
...result // Root level for compatibility
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Step 2: Lookup ASN for each IP
|
|
65
|
+
for (const ip of ipAddresses) {
|
|
66
|
+
try {
|
|
67
|
+
// Try iptoasn.com first (faster, unlimited)
|
|
68
|
+
let asnData = await this.lookupASNViaIPToASN(ip, options);
|
|
69
|
+
|
|
70
|
+
if (asnData) {
|
|
71
|
+
asnData._source = 'iptoasn';
|
|
72
|
+
individual.iptoasn.results.push({ ip, ...asnData });
|
|
73
|
+
result.asns.push(asnData);
|
|
74
|
+
|
|
75
|
+
if (asnData.network) {
|
|
76
|
+
result.networks.push(asnData.network);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (asnData.organization) {
|
|
80
|
+
result.organizations.add(asnData.organization);
|
|
81
|
+
}
|
|
82
|
+
} else if (options.hackertarget !== false) {
|
|
83
|
+
// Fallback to hackertarget if iptoasn fails
|
|
84
|
+
asnData = await this.lookupASNViaHackerTarget(ip, options);
|
|
85
|
+
|
|
86
|
+
if (asnData) {
|
|
87
|
+
asnData._source = 'hackertarget';
|
|
88
|
+
individual.hackertarget.results.push({ ip, ...asnData });
|
|
89
|
+
result.asns.push(asnData);
|
|
90
|
+
|
|
91
|
+
if (asnData.network) {
|
|
92
|
+
result.networks.push(asnData.network);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (asnData.organization) {
|
|
96
|
+
result.organizations.add(asnData.organization);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
} catch (error) {
|
|
102
|
+
result.errors[ip] = error.message;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Convert Set to Array for JSON serialization
|
|
107
|
+
result.organizations = Array.from(result.organizations);
|
|
108
|
+
|
|
109
|
+
// Deduplicate ASNs by ASN number
|
|
110
|
+
result.asns = this.deduplicateASNs(result.asns);
|
|
111
|
+
|
|
112
|
+
// Mark tools as unavailable if no results
|
|
113
|
+
if (individual.iptoasn.results.length === 0) {
|
|
114
|
+
individual.iptoasn.status = 'unavailable';
|
|
115
|
+
}
|
|
116
|
+
if (individual.hackertarget.results.length === 0 && options.hackertarget !== false) {
|
|
117
|
+
individual.hackertarget.status = 'unavailable';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
_individual: individual,
|
|
122
|
+
_aggregated: result,
|
|
123
|
+
...result // Root level for compatibility
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve host to IP addresses using dig
|
|
129
|
+
*/
|
|
130
|
+
async resolveHostToIPs(host, digResults = null) {
|
|
131
|
+
const ips = [];
|
|
132
|
+
|
|
133
|
+
// Resolve A records (IPv4)
|
|
134
|
+
const aRun = await this.commandRunner.run('dig', ['+short', 'A', host], { timeout: 5000 });
|
|
135
|
+
if (aRun.ok && aRun.stdout) {
|
|
136
|
+
const ipv4s = aRun.stdout
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map(line => line.trim())
|
|
139
|
+
.filter(line => /^\d+\.\d+\.\d+\.\d+$/.test(line));
|
|
140
|
+
ips.push(...ipv4s);
|
|
141
|
+
|
|
142
|
+
if (digResults) {
|
|
143
|
+
digResults.ipv4 = ipv4s;
|
|
144
|
+
if (this.config?.storage?.persistRawOutput) {
|
|
145
|
+
digResults.raw_ipv4 = aRun.stdout;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Resolve AAAA records (IPv6)
|
|
151
|
+
const aaaaRun = await this.commandRunner.run('dig', ['+short', 'AAAA', host], { timeout: 5000 });
|
|
152
|
+
if (aaaaRun.ok && aaaaRun.stdout) {
|
|
153
|
+
const ipv6s = aaaaRun.stdout
|
|
154
|
+
.split('\n')
|
|
155
|
+
.map(line => line.trim())
|
|
156
|
+
.filter(line => /^[0-9a-f:]+$/i.test(line) && line.includes(':'));
|
|
157
|
+
ips.push(...ipv6s);
|
|
158
|
+
|
|
159
|
+
if (digResults) {
|
|
160
|
+
digResults.ipv6 = ipv6s;
|
|
161
|
+
if (this.config?.storage?.persistRawOutput) {
|
|
162
|
+
digResults.raw_ipv6 = aaaaRun.stdout;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [...new Set(ips)]; // Deduplicate
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Lookup ASN via iptoasn.com (100% free, unlimited)
|
|
172
|
+
* API: https://iptoasn.com/
|
|
173
|
+
*/
|
|
174
|
+
async lookupASNViaIPToASN(ip, options = {}) {
|
|
175
|
+
try {
|
|
176
|
+
const url = `https://api.iptoasn.com/v1/as/ip/${encodeURIComponent(ip)}`;
|
|
177
|
+
|
|
178
|
+
const response = await fetch(url, {
|
|
179
|
+
headers: {
|
|
180
|
+
'User-Agent': this.config.curl?.userAgent || 'ReconPlugin/1.0'
|
|
181
|
+
},
|
|
182
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const data = await response.json();
|
|
190
|
+
|
|
191
|
+
// iptoasn.com response format:
|
|
192
|
+
// {
|
|
193
|
+
// "announced": true,
|
|
194
|
+
// "as_number": 15169,
|
|
195
|
+
// "as_country_code": "US",
|
|
196
|
+
// "as_description": "GOOGLE",
|
|
197
|
+
// "first_ip": "8.8.8.0",
|
|
198
|
+
// "last_ip": "8.8.8.255",
|
|
199
|
+
// "as_name": "GOOGLE"
|
|
200
|
+
// }
|
|
201
|
+
|
|
202
|
+
if (!data.announced || !data.as_number) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
ip,
|
|
208
|
+
asn: `AS${data.as_number}`,
|
|
209
|
+
asnNumber: data.as_number,
|
|
210
|
+
organization: data.as_description || data.as_name,
|
|
211
|
+
country: data.as_country_code,
|
|
212
|
+
network: data.first_ip && data.last_ip
|
|
213
|
+
? `${data.first_ip} - ${data.last_ip}`
|
|
214
|
+
: null,
|
|
215
|
+
source: 'iptoasn.com'
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Lookup ASN via hackertarget.com (100 queries/day free)
|
|
225
|
+
* API: https://api.hackertarget.com/aslookup/
|
|
226
|
+
*/
|
|
227
|
+
async lookupASNViaHackerTarget(ip, options = {}) {
|
|
228
|
+
try {
|
|
229
|
+
const url = `https://api.hackertarget.com/aslookup/?q=${encodeURIComponent(ip)}`;
|
|
230
|
+
|
|
231
|
+
const response = await fetch(url, {
|
|
232
|
+
headers: {
|
|
233
|
+
'User-Agent': this.config.curl?.userAgent || 'ReconPlugin/1.0'
|
|
234
|
+
},
|
|
235
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(10000) : undefined
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const text = await response.text();
|
|
243
|
+
|
|
244
|
+
// hackertarget response format (plain text):
|
|
245
|
+
// "15169","8.8.8.0/24","US","arin","GOOGLE"
|
|
246
|
+
|
|
247
|
+
if (text.includes('error') || !text.includes(',')) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Parse CSV format
|
|
252
|
+
const parts = text.split(',').map(p => p.replace(/"/g, '').trim());
|
|
253
|
+
|
|
254
|
+
if (parts.length < 3) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const asnNumber = parseInt(parts[0]);
|
|
259
|
+
const network = parts[1] || null;
|
|
260
|
+
const country = parts[2] || null;
|
|
261
|
+
const organization = parts[4] || parts[3] || null;
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
ip,
|
|
265
|
+
asn: `AS${asnNumber}`,
|
|
266
|
+
asnNumber,
|
|
267
|
+
organization,
|
|
268
|
+
country,
|
|
269
|
+
network,
|
|
270
|
+
source: 'hackertarget.com'
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Deduplicate ASNs by ASN number
|
|
280
|
+
*/
|
|
281
|
+
deduplicateASNs(asns) {
|
|
282
|
+
const seen = new Map();
|
|
283
|
+
|
|
284
|
+
for (const asn of asns) {
|
|
285
|
+
const key = asn.asnNumber;
|
|
286
|
+
|
|
287
|
+
if (!seen.has(key)) {
|
|
288
|
+
seen.set(key, asn);
|
|
289
|
+
} else {
|
|
290
|
+
// Merge data from multiple sources
|
|
291
|
+
const existing = seen.get(key);
|
|
292
|
+
|
|
293
|
+
// Prefer more detailed data
|
|
294
|
+
if (asn.network && !existing.network) {
|
|
295
|
+
existing.network = asn.network;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (asn.organization && !existing.organization) {
|
|
299
|
+
existing.organization = asn.organization;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Track multiple sources
|
|
303
|
+
if (!existing.sources) {
|
|
304
|
+
existing.sources = [existing.source];
|
|
305
|
+
}
|
|
306
|
+
if (!existing.sources.includes(asn.source)) {
|
|
307
|
+
existing.sources.push(asn.source);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return Array.from(seen.values());
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CertificateStage
|
|
3
|
+
*
|
|
4
|
+
* TLS certificate inspection:
|
|
5
|
+
* - Subject and issuer details
|
|
6
|
+
* - Validity period
|
|
7
|
+
* - Fingerprint
|
|
8
|
+
* - Subject Alternative Names (SANs)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import tls from 'tls';
|
|
12
|
+
|
|
13
|
+
export class CertificateStage {
|
|
14
|
+
constructor(plugin) {
|
|
15
|
+
this.plugin = plugin;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async execute(target) {
|
|
19
|
+
const shouldCheckTls =
|
|
20
|
+
target.protocol === 'https' ||
|
|
21
|
+
(!target.protocol && (target.port === 443 || target.host.includes(':') === false));
|
|
22
|
+
|
|
23
|
+
if (!shouldCheckTls) {
|
|
24
|
+
return {
|
|
25
|
+
status: 'skipped',
|
|
26
|
+
message: 'TLS inspection skipped for non-HTTPS target'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const port = target.port || 443;
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const socket = tls.connect(
|
|
34
|
+
{
|
|
35
|
+
host: target.host,
|
|
36
|
+
port,
|
|
37
|
+
servername: target.host,
|
|
38
|
+
rejectUnauthorized: false,
|
|
39
|
+
timeout: 5000
|
|
40
|
+
},
|
|
41
|
+
() => {
|
|
42
|
+
const certificate = socket.getPeerCertificate(true);
|
|
43
|
+
socket.end();
|
|
44
|
+
|
|
45
|
+
if (!certificate || Object.keys(certificate).length === 0) {
|
|
46
|
+
resolve({
|
|
47
|
+
status: 'error',
|
|
48
|
+
message: 'No certificate information available'
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resolve({
|
|
54
|
+
status: 'ok',
|
|
55
|
+
subject: certificate.subject,
|
|
56
|
+
issuer: certificate.issuer,
|
|
57
|
+
validFrom: certificate.valid_from,
|
|
58
|
+
validTo: certificate.valid_to,
|
|
59
|
+
fingerprint: certificate.fingerprint256 || certificate.fingerprint,
|
|
60
|
+
subjectAltName: certificate.subjectaltname
|
|
61
|
+
? certificate.subjectaltname.split(',').map((entry) => entry.trim())
|
|
62
|
+
: [],
|
|
63
|
+
raw: certificate
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
socket.on('error', (error) => {
|
|
69
|
+
resolve({
|
|
70
|
+
status: 'error',
|
|
71
|
+
message: error?.message || 'Unable to retrieve certificate'
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
socket.setTimeout(6000, () => {
|
|
76
|
+
socket.destroy();
|
|
77
|
+
resolve({
|
|
78
|
+
status: 'timeout',
|
|
79
|
+
message: 'TLS handshake timed out'
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|