fingerprint-mcp 0.1.0
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/LICENSE +663 -0
- package/README.ar.md +767 -0
- package/README.bn.md +767 -0
- package/README.bs.md +767 -0
- package/README.da.md +767 -0
- package/README.de.md +770 -0
- package/README.el.md +767 -0
- package/README.es.md +769 -0
- package/README.fr.md +769 -0
- package/README.hi.md +767 -0
- package/README.it.md +767 -0
- package/README.ja.md +767 -0
- package/README.ko.md +772 -0
- package/README.md +767 -0
- package/README.no.md +767 -0
- package/README.pl.md +767 -0
- package/README.pt-BR.md +767 -0
- package/README.ru.md +767 -0
- package/README.th.md +767 -0
- package/README.tr.md +767 -0
- package/README.uk.md +767 -0
- package/README.vi.md +767 -0
- package/README.zh-TW.md +768 -0
- package/README.zh.md +768 -0
- package/dist/app/index.d.ts +3 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +614 -0
- package/dist/app/index.js.map +1 -0
- package/dist/composite/analyze.d.ts +3 -0
- package/dist/composite/analyze.d.ts.map +1 -0
- package/dist/composite/analyze.js +59 -0
- package/dist/composite/analyze.js.map +1 -0
- package/dist/composite/correlate.d.ts +3 -0
- package/dist/composite/correlate.d.ts.map +1 -0
- package/dist/composite/correlate.js +184 -0
- package/dist/composite/correlate.js.map +1 -0
- package/dist/composite/enumerate.d.ts +3 -0
- package/dist/composite/enumerate.d.ts.map +1 -0
- package/dist/composite/enumerate.js +94 -0
- package/dist/composite/enumerate.js.map +1 -0
- package/dist/composite/helpers.d.ts +22 -0
- package/dist/composite/helpers.d.ts.map +1 -0
- package/dist/composite/helpers.js +111 -0
- package/dist/composite/helpers.js.map +1 -0
- package/dist/composite/index.d.ts +3 -0
- package/dist/composite/index.d.ts.map +1 -0
- package/dist/composite/index.js +29 -0
- package/dist/composite/index.js.map +1 -0
- package/dist/composite/meta.d.ts +3 -0
- package/dist/composite/meta.d.ts.map +1 -0
- package/dist/composite/meta.js +23 -0
- package/dist/composite/meta.js.map +1 -0
- package/dist/composite/osint.d.ts +3 -0
- package/dist/composite/osint.d.ts.map +1 -0
- package/dist/composite/osint.js +40 -0
- package/dist/composite/osint.js.map +1 -0
- package/dist/composite/recon.d.ts +3 -0
- package/dist/composite/recon.d.ts.map +1 -0
- package/dist/composite/recon.js +131 -0
- package/dist/composite/recon.js.map +1 -0
- package/dist/composite/scan-dns.d.ts +3 -0
- package/dist/composite/scan-dns.d.ts.map +1 -0
- package/dist/composite/scan-dns.js +44 -0
- package/dist/composite/scan-dns.js.map +1 -0
- package/dist/composite/scan-http.d.ts +3 -0
- package/dist/composite/scan-http.d.ts.map +1 -0
- package/dist/composite/scan-http.js +68 -0
- package/dist/composite/scan-http.js.map +1 -0
- package/dist/composite/scan-paths.d.ts +3 -0
- package/dist/composite/scan-paths.d.ts.map +1 -0
- package/dist/composite/scan-paths.js +32 -0
- package/dist/composite/scan-paths.js.map +1 -0
- package/dist/composite/scan-ports.d.ts +3 -0
- package/dist/composite/scan-ports.d.ts.map +1 -0
- package/dist/composite/scan-ports.js +79 -0
- package/dist/composite/scan-ports.js.map +1 -0
- package/dist/composite/scan-services.d.ts +3 -0
- package/dist/composite/scan-services.d.ts.map +1 -0
- package/dist/composite/scan-services.js +111 -0
- package/dist/composite/scan-services.js.map +1 -0
- package/dist/composite/scan-tls.d.ts +3 -0
- package/dist/composite/scan-tls.d.ts.map +1 -0
- package/dist/composite/scan-tls.js +32 -0
- package/dist/composite/scan-tls.js.map +1 -0
- package/dist/composite/scan-waf.d.ts +3 -0
- package/dist/composite/scan-waf.d.ts.map +1 -0
- package/dist/composite/scan-waf.js +35 -0
- package/dist/composite/scan-waf.js.map +1 -0
- package/dist/correlation/index.d.ts +3 -0
- package/dist/correlation/index.d.ts.map +1 -0
- package/dist/correlation/index.js +1044 -0
- package/dist/correlation/index.js.map +1 -0
- package/dist/data/analytics-patterns.d.ts +21 -0
- package/dist/data/analytics-patterns.d.ts.map +1 -0
- package/dist/data/analytics-patterns.js +449 -0
- package/dist/data/analytics-patterns.js.map +1 -0
- package/dist/data/app-signatures.d.ts +52 -0
- package/dist/data/app-signatures.d.ts.map +1 -0
- package/dist/data/app-signatures.js +1143 -0
- package/dist/data/app-signatures.js.map +1 -0
- package/dist/data/c2-signatures.d.ts +54 -0
- package/dist/data/c2-signatures.d.ts.map +1 -0
- package/dist/data/c2-signatures.js +256 -0
- package/dist/data/c2-signatures.js.map +1 -0
- package/dist/data/cloud-ranges.d.ts +34 -0
- package/dist/data/cloud-ranges.d.ts.map +1 -0
- package/dist/data/cloud-ranges.js +628 -0
- package/dist/data/cloud-ranges.js.map +1 -0
- package/dist/data/cookie-patterns.d.ts +28 -0
- package/dist/data/cookie-patterns.d.ts.map +1 -0
- package/dist/data/cookie-patterns.js +225 -0
- package/dist/data/cookie-patterns.js.map +1 -0
- package/dist/data/error-signatures.d.ts +19 -0
- package/dist/data/error-signatures.d.ts.map +1 -0
- package/dist/data/error-signatures.js +321 -0
- package/dist/data/error-signatures.js.map +1 -0
- package/dist/data/favicon-hashes.d.ts +25 -0
- package/dist/data/favicon-hashes.d.ts.map +1 -0
- package/dist/data/favicon-hashes.js +249 -0
- package/dist/data/favicon-hashes.js.map +1 -0
- package/dist/data/h2-signatures.d.ts +50 -0
- package/dist/data/h2-signatures.d.ts.map +1 -0
- package/dist/data/h2-signatures.js +211 -0
- package/dist/data/h2-signatures.js.map +1 -0
- package/dist/data/header-order.d.ts +38 -0
- package/dist/data/header-order.d.ts.map +1 -0
- package/dist/data/header-order.js +185 -0
- package/dist/data/header-order.js.map +1 -0
- package/dist/data/jarm-signatures.d.ts +25 -0
- package/dist/data/jarm-signatures.d.ts.map +1 -0
- package/dist/data/jarm-signatures.js +213 -0
- package/dist/data/jarm-signatures.js.map +1 -0
- package/dist/data/saas-patterns.d.ts +23 -0
- package/dist/data/saas-patterns.d.ts.map +1 -0
- package/dist/data/saas-patterns.js +139 -0
- package/dist/data/saas-patterns.js.map +1 -0
- package/dist/data/sensitive-paths.d.ts +12 -0
- package/dist/data/sensitive-paths.d.ts.map +1 -0
- package/dist/data/sensitive-paths.js +1539 -0
- package/dist/data/sensitive-paths.js.map +1 -0
- package/dist/data/service-banners.d.ts +59 -0
- package/dist/data/service-banners.d.ts.map +1 -0
- package/dist/data/service-banners.js +323 -0
- package/dist/data/service-banners.js.map +1 -0
- package/dist/data/smtp-signatures.d.ts +12 -0
- package/dist/data/smtp-signatures.d.ts.map +1 -0
- package/dist/data/smtp-signatures.js +350 -0
- package/dist/data/smtp-signatures.js.map +1 -0
- package/dist/data/takeover-patterns.d.ts +37 -0
- package/dist/data/takeover-patterns.d.ts.map +1 -0
- package/dist/data/takeover-patterns.js +249 -0
- package/dist/data/takeover-patterns.js.map +1 -0
- package/dist/data/tech-patterns.d.ts +44 -0
- package/dist/data/tech-patterns.d.ts.map +1 -0
- package/dist/data/tech-patterns.js +690 -0
- package/dist/data/tech-patterns.js.map +1 -0
- package/dist/data/waf-signatures.d.ts +21 -0
- package/dist/data/waf-signatures.d.ts.map +1 -0
- package/dist/data/waf-signatures.js +318 -0
- package/dist/data/waf-signatures.js.map +1 -0
- package/dist/dns/index.d.ts +3 -0
- package/dist/dns/index.d.ts.map +1 -0
- package/dist/dns/index.js +1067 -0
- package/dist/dns/index.js.map +1 -0
- package/dist/enum/index.d.ts +3 -0
- package/dist/enum/index.d.ts.map +1 -0
- package/dist/enum/index.js +818 -0
- package/dist/enum/index.js.map +1 -0
- package/dist/h2/index.d.ts +3 -0
- package/dist/h2/index.d.ts.map +1 -0
- package/dist/h2/index.js +414 -0
- package/dist/h2/index.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2444 -0
- package/dist/http/index.js.map +1 -0
- package/dist/identify/index.d.ts +3 -0
- package/dist/identify/index.d.ts.map +1 -0
- package/dist/identify/index.js +447 -0
- package/dist/identify/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/index.d.ts +3 -0
- package/dist/infra/index.d.ts.map +1 -0
- package/dist/infra/index.js +989 -0
- package/dist/infra/index.js.map +1 -0
- package/dist/iot/index.d.ts +3 -0
- package/dist/iot/index.d.ts.map +1 -0
- package/dist/iot/index.js +610 -0
- package/dist/iot/index.js.map +1 -0
- package/dist/meta/index.d.ts +3 -0
- package/dist/meta/index.d.ts.map +1 -0
- package/dist/meta/index.js +442 -0
- package/dist/meta/index.js.map +1 -0
- package/dist/osint/index.d.ts +3 -0
- package/dist/osint/index.d.ts.map +1 -0
- package/dist/osint/index.js +687 -0
- package/dist/osint/index.js.map +1 -0
- package/dist/passive/index.d.ts +3 -0
- package/dist/passive/index.d.ts.map +1 -0
- package/dist/passive/index.js +944 -0
- package/dist/passive/index.js.map +1 -0
- package/dist/path/index.d.ts +3 -0
- package/dist/path/index.d.ts.map +1 -0
- package/dist/path/index.js +878 -0
- package/dist/path/index.js.map +1 -0
- package/dist/protocol/mcp-server.d.ts +4 -0
- package/dist/protocol/mcp-server.d.ts.map +1 -0
- package/dist/protocol/mcp-server.js +32 -0
- package/dist/protocol/mcp-server.js.map +1 -0
- package/dist/protocol/tools.d.ts +3 -0
- package/dist/protocol/tools.d.ts.map +1 -0
- package/dist/protocol/tools.js +5 -0
- package/dist/protocol/tools.js.map +1 -0
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +1053 -0
- package/dist/service/index.js.map +1 -0
- package/dist/smtp/index.d.ts +3 -0
- package/dist/smtp/index.d.ts.map +1 -0
- package/dist/smtp/index.js +437 -0
- package/dist/smtp/index.js.map +1 -0
- package/dist/ssh/index.d.ts +3 -0
- package/dist/ssh/index.d.ts.map +1 -0
- package/dist/ssh/index.js +676 -0
- package/dist/ssh/index.js.map +1 -0
- package/dist/tcp/index.d.ts +3 -0
- package/dist/tcp/index.d.ts.map +1 -0
- package/dist/tcp/index.js +369 -0
- package/dist/tcp/index.js.map +1 -0
- package/dist/timing/index.d.ts +3 -0
- package/dist/timing/index.d.ts.map +1 -0
- package/dist/timing/index.js +425 -0
- package/dist/timing/index.js.map +1 -0
- package/dist/tls/index.d.ts +3 -0
- package/dist/tls/index.d.ts.map +1 -0
- package/dist/tls/index.js +1332 -0
- package/dist/tls/index.js.map +1 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cache.d.ts +11 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +35 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/murmurhash3.d.ts +3 -0
- package/dist/utils/murmurhash3.d.ts.map +1 -0
- package/dist/utils/murmurhash3.js +57 -0
- package/dist/utils/murmurhash3.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +10 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +35 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/require-key.d.ts +2 -0
- package/dist/utils/require-key.d.ts.map +1 -0
- package/dist/utils/require-key.js +8 -0
- package/dist/utils/require-key.js.map +1 -0
- package/dist/waf/index.d.ts +3 -0
- package/dist/waf/index.d.ts.map +1 -0
- package/dist/waf/index.js +767 -0
- package/dist/waf/index.js.map +1 -0
- package/dist/web/index.d.ts +3 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +1366 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RateLimiter } from "../utils/rate-limiter.js";
|
|
3
|
+
import { TTLCache } from "../utils/cache.js";
|
|
4
|
+
import { requireApiKey } from "../utils/require-key.js";
|
|
5
|
+
import { json } from "../types/index.js";
|
|
6
|
+
import * as dns from "node:dns/promises";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
const limiter = new RateLimiter(500);
|
|
9
|
+
const cache = new TTLCache(600_000);
|
|
10
|
+
// ─── Helpers ───
|
|
11
|
+
const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
12
|
+
function categorizeHostname(hostname) {
|
|
13
|
+
const h = hostname.toLowerCase();
|
|
14
|
+
if (/^(dev|development)[\.-]/.test(h) || /[\.-](dev|development)\./.test(h))
|
|
15
|
+
return "development";
|
|
16
|
+
if (/^(staging|stage|stg)[\.-]/.test(h) || /[\.-](staging|stage|stg)\./.test(h))
|
|
17
|
+
return "staging";
|
|
18
|
+
if (/^(admin|administrator|panel)[\.-]/.test(h))
|
|
19
|
+
return "admin";
|
|
20
|
+
if (/^(api|rest|graphql)[\.-]/.test(h))
|
|
21
|
+
return "api";
|
|
22
|
+
if (/^(vpn|gateway|gw|tunnel)[\.-]/.test(h))
|
|
23
|
+
return "vpn";
|
|
24
|
+
if (/^(mail|smtp|pop|imap|mx|webmail)[\.-]/.test(h))
|
|
25
|
+
return "mail";
|
|
26
|
+
if (/^(internal|intranet|corp)[\.-]/.test(h))
|
|
27
|
+
return "internal";
|
|
28
|
+
if (/^(test|testing|qa|uat)[\.-]/.test(h))
|
|
29
|
+
return "testing";
|
|
30
|
+
if (/^(cdn|static|assets|media|img|images)[\.-]/.test(h))
|
|
31
|
+
return "cdn";
|
|
32
|
+
if (/^(ci|cd|jenkins|gitlab|github|build|deploy)[\.-]/.test(h))
|
|
33
|
+
return "ci-cd";
|
|
34
|
+
if (/^(db|database|mysql|postgres|redis|mongo|elastic)[\.-]/.test(h))
|
|
35
|
+
return "database";
|
|
36
|
+
if (/^(ns\d*|dns\d*)\./.test(h))
|
|
37
|
+
return "nameserver";
|
|
38
|
+
if (/^(www|web)\./.test(h))
|
|
39
|
+
return "web";
|
|
40
|
+
return "other";
|
|
41
|
+
}
|
|
42
|
+
// ─── Tool 1: enum_subdomains ───
|
|
43
|
+
async function enumSubdomains(args, ctx) {
|
|
44
|
+
const domain = args.domain;
|
|
45
|
+
const source = args.source ?? "auto";
|
|
46
|
+
const cacheKey = `enum_subdomains:${domain}:${source}`;
|
|
47
|
+
const cached = cache.get(cacheKey);
|
|
48
|
+
if (cached)
|
|
49
|
+
return json(cached);
|
|
50
|
+
let subdomainList = [];
|
|
51
|
+
let usedSource;
|
|
52
|
+
// Determine which source to use
|
|
53
|
+
const useSecurityTrails = source === "securitytrails" || (source === "auto" && ctx.config.securitytrailsApiKey);
|
|
54
|
+
if (useSecurityTrails) {
|
|
55
|
+
const apiKey = requireApiKey(ctx.config.securitytrailsApiKey, "SecurityTrails", "SECURITYTRAILS_API_KEY");
|
|
56
|
+
usedSource = "securitytrails";
|
|
57
|
+
await limiter.acquire();
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(`https://api.securitytrails.com/v1/domain/${encodeURIComponent(domain)}/subdomains`, {
|
|
60
|
+
headers: { APIKEY: apiKey },
|
|
61
|
+
signal: AbortSignal.timeout(15_000),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const errText = await response.text();
|
|
65
|
+
return json({
|
|
66
|
+
error: `SecurityTrails API error: ${response.status}`,
|
|
67
|
+
details: errText,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const data = await response.json();
|
|
71
|
+
const subs = (data.subdomains ?? []);
|
|
72
|
+
subdomainList = subs.map((sub) => `${sub}.${domain}`);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return json({
|
|
76
|
+
error: `SecurityTrails request failed: ${err.message}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// HackerTarget fallback
|
|
82
|
+
usedSource = "hackertarget";
|
|
83
|
+
await limiter.acquire();
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(`https://api.hackertarget.com/hostsearch/?q=${encodeURIComponent(domain)}`, {
|
|
86
|
+
signal: AbortSignal.timeout(15_000),
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
return json({
|
|
90
|
+
error: `HackerTarget API error: ${response.status}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
if (text.startsWith("error") || text.startsWith("API count exceeded")) {
|
|
95
|
+
return json({
|
|
96
|
+
domain,
|
|
97
|
+
error: text.trim(),
|
|
98
|
+
subdomains: [],
|
|
99
|
+
total: 0,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const parts = line.split(",");
|
|
105
|
+
if (parts.length >= 1 && parts[0]) {
|
|
106
|
+
subdomainList.push(parts[0].trim());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return json({
|
|
112
|
+
error: `HackerTarget request failed: ${err.message}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Resolve IPs and categorize each subdomain
|
|
117
|
+
const subdomains = [];
|
|
118
|
+
const categories = {};
|
|
119
|
+
for (const hostname of subdomainList) {
|
|
120
|
+
let ips = [];
|
|
121
|
+
try {
|
|
122
|
+
await limiter.acquire();
|
|
123
|
+
ips = await dns.resolve4(hostname);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// DNS resolution failed — no IPs
|
|
127
|
+
}
|
|
128
|
+
const category = categorizeHostname(hostname);
|
|
129
|
+
categories[category] = (categories[category] ?? 0) + 1;
|
|
130
|
+
subdomains.push({ hostname, ips, category });
|
|
131
|
+
}
|
|
132
|
+
const result = {
|
|
133
|
+
domain,
|
|
134
|
+
source: usedSource,
|
|
135
|
+
subdomains,
|
|
136
|
+
total: subdomains.length,
|
|
137
|
+
categories,
|
|
138
|
+
intelligence: {
|
|
139
|
+
note: `Discovered ${subdomains.length} subdomains via ${usedSource}.`,
|
|
140
|
+
interestingCategories: Object.entries(categories)
|
|
141
|
+
.filter(([cat]) => ["admin", "staging", "development", "internal", "database", "vpn", "ci-cd", "testing"].includes(cat))
|
|
142
|
+
.map(([cat, count]) => `${cat}: ${count}`),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
cache.set(cacheKey, result);
|
|
146
|
+
return json(result);
|
|
147
|
+
}
|
|
148
|
+
// ─── Tool 2: enum_asn_neighbors ───
|
|
149
|
+
async function enumAsnNeighbors(args) {
|
|
150
|
+
const target = args.target;
|
|
151
|
+
const limit = args.limit ?? 100;
|
|
152
|
+
const cacheKey = `enum_asn_neighbors:${target}:${limit}`;
|
|
153
|
+
const cached = cache.get(cacheKey);
|
|
154
|
+
if (cached)
|
|
155
|
+
return json(cached);
|
|
156
|
+
// Resolve domain to IP if needed
|
|
157
|
+
let ip;
|
|
158
|
+
if (IP_REGEX.test(target)) {
|
|
159
|
+
ip = target;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
try {
|
|
163
|
+
await limiter.acquire();
|
|
164
|
+
const ips = await dns.resolve4(target);
|
|
165
|
+
if (!ips || ips.length === 0) {
|
|
166
|
+
return json({ error: `Could not resolve ${target} to an IP address` });
|
|
167
|
+
}
|
|
168
|
+
ip = ips[0];
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
return json({ error: `DNS resolution failed for ${target}: ${err.message}` });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Get ASN for this IP
|
|
175
|
+
await limiter.acquire();
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(`https://api.hackertarget.com/aslookup/?q=${encodeURIComponent(ip)}`, {
|
|
178
|
+
signal: AbortSignal.timeout(15_000),
|
|
179
|
+
});
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
return json({ error: `HackerTarget ASN lookup error: ${response.status}` });
|
|
182
|
+
}
|
|
183
|
+
const text = await response.text();
|
|
184
|
+
if (text.startsWith("error") || text.startsWith("API count exceeded")) {
|
|
185
|
+
return json({ target, ip, error: text.trim() });
|
|
186
|
+
}
|
|
187
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
188
|
+
if (lines.length === 0) {
|
|
189
|
+
return json({ target, ip, error: "No ASN data returned" });
|
|
190
|
+
}
|
|
191
|
+
// First line: "ip,asn_number,org_name" or similar
|
|
192
|
+
const firstLineParts = lines[0].split(",");
|
|
193
|
+
let asnNumber = null;
|
|
194
|
+
let orgName = null;
|
|
195
|
+
if (firstLineParts.length >= 2) {
|
|
196
|
+
// Extract ASN number — might be like "AS12345" or just "12345"
|
|
197
|
+
const rawAsn = firstLineParts[1].trim().replace(/^AS/i, "");
|
|
198
|
+
asnNumber = rawAsn;
|
|
199
|
+
orgName = firstLineParts.slice(2).join(",").trim() || null;
|
|
200
|
+
}
|
|
201
|
+
if (!asnNumber) {
|
|
202
|
+
return json({ target, ip, error: "Could not parse ASN number from response" });
|
|
203
|
+
}
|
|
204
|
+
// Get all prefixes for this ASN
|
|
205
|
+
await limiter.acquire();
|
|
206
|
+
const asnResponse = await fetch(`https://api.hackertarget.com/aslookup/?q=AS${asnNumber}`, {
|
|
207
|
+
signal: AbortSignal.timeout(15_000),
|
|
208
|
+
});
|
|
209
|
+
if (!asnResponse.ok) {
|
|
210
|
+
return json({ error: `HackerTarget ASN prefix lookup error: ${asnResponse.status}` });
|
|
211
|
+
}
|
|
212
|
+
const asnText = await asnResponse.text();
|
|
213
|
+
const asnLines = asnText.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
214
|
+
// Parse CIDR ranges from the response
|
|
215
|
+
const prefixes = [];
|
|
216
|
+
for (const line of asnLines) {
|
|
217
|
+
// CIDR pattern: x.x.x.x/xx or x:x:x::/xx
|
|
218
|
+
const cidrMatch = line.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2})/);
|
|
219
|
+
if (cidrMatch) {
|
|
220
|
+
prefixes.push(cidrMatch[1]);
|
|
221
|
+
}
|
|
222
|
+
// Also check for IPv6 CIDR
|
|
223
|
+
const ipv6CidrMatch = line.match(/([0-9a-fA-F:]+\/\d{1,3})/);
|
|
224
|
+
if (ipv6CidrMatch && !cidrMatch) {
|
|
225
|
+
prefixes.push(ipv6CidrMatch[1]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const limitedPrefixes = prefixes.slice(0, limit);
|
|
229
|
+
const result = {
|
|
230
|
+
target,
|
|
231
|
+
ip,
|
|
232
|
+
asn: {
|
|
233
|
+
number: `AS${asnNumber}`,
|
|
234
|
+
org: orgName,
|
|
235
|
+
},
|
|
236
|
+
prefixes: limitedPrefixes,
|
|
237
|
+
totalPrefixes: prefixes.length,
|
|
238
|
+
intelligence: {
|
|
239
|
+
note: `ASN AS${asnNumber} (${orgName ?? "unknown"}) announces ${prefixes.length} prefix(es).`,
|
|
240
|
+
networkSize: prefixes.length > 50
|
|
241
|
+
? "Large network — likely a hosting provider, ISP, or major organization."
|
|
242
|
+
: prefixes.length > 10
|
|
243
|
+
? "Medium-sized network — could be a mid-size company or regional ISP."
|
|
244
|
+
: "Small network — likely a single organization with limited IP space.",
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
cache.set(cacheKey, result);
|
|
248
|
+
return json(result);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
return json({ error: `ASN neighbor lookup failed: ${err.message}` });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ─── Tool 3: enum_passive_dns ───
|
|
255
|
+
async function enumPassiveDns(args, ctx) {
|
|
256
|
+
const domain = args.domain;
|
|
257
|
+
const type = args.type ?? "a";
|
|
258
|
+
const cacheKey = `enum_passive_dns:${domain}:${type}`;
|
|
259
|
+
const cached = cache.get(cacheKey);
|
|
260
|
+
if (cached)
|
|
261
|
+
return json(cached);
|
|
262
|
+
let apiKey;
|
|
263
|
+
try {
|
|
264
|
+
apiKey = requireApiKey(ctx.config.securitytrailsApiKey, "SecurityTrails", "SECURITYTRAILS_API_KEY");
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return json({
|
|
268
|
+
error: "SecurityTrails API key required for passive DNS history.",
|
|
269
|
+
hint: "Set SECURITYTRAILS_API_KEY environment variable",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
await limiter.acquire();
|
|
273
|
+
try {
|
|
274
|
+
const response = await fetch(`https://api.securitytrails.com/v1/history/${encodeURIComponent(domain)}/dns/${type}`, {
|
|
275
|
+
headers: { APIKEY: apiKey },
|
|
276
|
+
signal: AbortSignal.timeout(15_000),
|
|
277
|
+
});
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
const errText = await response.text();
|
|
280
|
+
return json({
|
|
281
|
+
error: `SecurityTrails API error: ${response.status}`,
|
|
282
|
+
details: errText,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const data = await response.json();
|
|
286
|
+
const records = [];
|
|
287
|
+
for (const record of data.records ?? []) {
|
|
288
|
+
records.push({
|
|
289
|
+
first_seen: record.first_seen ?? "unknown",
|
|
290
|
+
last_seen: record.last_seen ?? "unknown",
|
|
291
|
+
values: (record.values ?? []).map((v) => ({
|
|
292
|
+
ip: v.ip ?? v.host ?? v.ip_count ?? null,
|
|
293
|
+
ip_count: v.ip_count ?? null,
|
|
294
|
+
host: v.host ?? null,
|
|
295
|
+
})),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// Detect infrastructure changes
|
|
299
|
+
const changes = [];
|
|
300
|
+
if (records.length > 1) {
|
|
301
|
+
changes.push(`${records.length} historical DNS record sets found — infrastructure has changed over time.`);
|
|
302
|
+
// Check for IP changes in A records
|
|
303
|
+
if (type === "a") {
|
|
304
|
+
const uniqueIps = new Set();
|
|
305
|
+
for (const rec of records) {
|
|
306
|
+
for (const v of rec.values) {
|
|
307
|
+
if (v.ip)
|
|
308
|
+
uniqueIps.add(v.ip);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (uniqueIps.size > 1) {
|
|
312
|
+
changes.push(`${uniqueIps.size} unique IP addresses observed — domain has migrated between hosts.`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const result = {
|
|
317
|
+
domain,
|
|
318
|
+
type,
|
|
319
|
+
records,
|
|
320
|
+
total_records: records.length,
|
|
321
|
+
intelligence: {
|
|
322
|
+
note: records.length > 0
|
|
323
|
+
? `Found ${records.length} historical ${type.toUpperCase()} record(s) for ${domain}.`
|
|
324
|
+
: `No historical ${type.toUpperCase()} records found for ${domain}.`,
|
|
325
|
+
changes,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
cache.set(cacheKey, result);
|
|
329
|
+
return json(result);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
return json({ error: `Passive DNS lookup failed: ${err.message}` });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// ─── Tool 4: enum_tld_expansion ───
|
|
336
|
+
async function enumTldExpansion(args) {
|
|
337
|
+
const basename = args.basename;
|
|
338
|
+
const tlds = args.tlds ?? [
|
|
339
|
+
"com", "net", "org", "io", "dev", "co", "app", "ai", "xyz", "info",
|
|
340
|
+
"biz", "us", "uk", "de", "fr", "eu", "ca", "au", "nl", "ru",
|
|
341
|
+
"cn", "jp", "in", "com.tr", "me",
|
|
342
|
+
];
|
|
343
|
+
const cacheKey = `enum_tld_expansion:${basename}:${tlds.join(",")}`;
|
|
344
|
+
const cached = cache.get(cacheKey);
|
|
345
|
+
if (cached)
|
|
346
|
+
return json(cached);
|
|
347
|
+
const results = [];
|
|
348
|
+
for (const tld of tlds) {
|
|
349
|
+
const domain = `${basename}.${tld}`;
|
|
350
|
+
await limiter.acquire();
|
|
351
|
+
let ips = [];
|
|
352
|
+
let active = false;
|
|
353
|
+
try {
|
|
354
|
+
ips = await dns.resolve4(domain);
|
|
355
|
+
active = ips.length > 0;
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// Domain does not resolve — inactive
|
|
359
|
+
}
|
|
360
|
+
let nameservers = [];
|
|
361
|
+
if (active) {
|
|
362
|
+
try {
|
|
363
|
+
await limiter.acquire();
|
|
364
|
+
nameservers = await dns.resolveNs(domain);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// NS resolution failed
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
results.push({ domain, tld, active, ips, nameservers });
|
|
371
|
+
}
|
|
372
|
+
const activeResults = results.filter((r) => r.active);
|
|
373
|
+
const inactiveResults = results.filter((r) => !r.active);
|
|
374
|
+
// Group by shared NS
|
|
375
|
+
const nsGroups = {};
|
|
376
|
+
for (const r of activeResults) {
|
|
377
|
+
if (r.nameservers.length > 0) {
|
|
378
|
+
const nsKey = [...r.nameservers].sort().join(",");
|
|
379
|
+
if (!nsGroups[nsKey])
|
|
380
|
+
nsGroups[nsKey] = [];
|
|
381
|
+
nsGroups[nsKey].push(r.domain);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Filter groups with more than one domain (same owner signals)
|
|
385
|
+
const sameNsGroups = {};
|
|
386
|
+
for (const [ns, domains] of Object.entries(nsGroups)) {
|
|
387
|
+
if (domains.length > 1) {
|
|
388
|
+
sameNsGroups[ns] = domains;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const result = {
|
|
392
|
+
basename,
|
|
393
|
+
results,
|
|
394
|
+
active_count: activeResults.length,
|
|
395
|
+
inactive_count: inactiveResults.length,
|
|
396
|
+
same_ns_groups: sameNsGroups,
|
|
397
|
+
intelligence: {
|
|
398
|
+
note: `${activeResults.length} of ${tlds.length} TLD variations are active for "${basename}".`,
|
|
399
|
+
sameOwnerHint: Object.keys(sameNsGroups).length > 0
|
|
400
|
+
? "Domains sharing the same nameservers likely belong to the same organization."
|
|
401
|
+
: "No domains share the same nameservers — ownership may differ.",
|
|
402
|
+
activeTlds: activeResults.map((r) => r.tld),
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
cache.set(cacheKey, result);
|
|
406
|
+
return json(result);
|
|
407
|
+
}
|
|
408
|
+
// ─── Tool 5: enum_related_domains ───
|
|
409
|
+
async function enumRelatedDomains(args) {
|
|
410
|
+
const domain = args.domain;
|
|
411
|
+
const analyticsId = args.analyticsId;
|
|
412
|
+
const nameserversParam = args.nameservers;
|
|
413
|
+
const cacheKey = `enum_related_domains:${domain}:${analyticsId ?? ""}:${(nameserversParam ?? []).join(",")}`;
|
|
414
|
+
const cached = cache.get(cacheKey);
|
|
415
|
+
if (cached)
|
|
416
|
+
return json(cached);
|
|
417
|
+
await limiter.acquire();
|
|
418
|
+
// Query crt.sh for certificate SANs
|
|
419
|
+
let certSANs = [];
|
|
420
|
+
let relatedRootDomains = [];
|
|
421
|
+
try {
|
|
422
|
+
const response = await fetch(`https://crt.sh/?q=${encodeURIComponent(domain)}&output=json`, {
|
|
423
|
+
signal: AbortSignal.timeout(20_000),
|
|
424
|
+
});
|
|
425
|
+
if (response.ok) {
|
|
426
|
+
const data = await response.json();
|
|
427
|
+
const allNames = new Set();
|
|
428
|
+
for (const entry of data) {
|
|
429
|
+
const nameValue = entry.name_value ?? "";
|
|
430
|
+
const names = nameValue.split("\n").map((n) => n.trim().toLowerCase()).filter(Boolean);
|
|
431
|
+
for (const name of names) {
|
|
432
|
+
// Remove wildcard prefix
|
|
433
|
+
const cleanName = name.replace(/^\*\./, "");
|
|
434
|
+
allNames.add(cleanName);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
certSANs = [...allNames];
|
|
438
|
+
// Extract unique root domains
|
|
439
|
+
const rootDomains = new Set();
|
|
440
|
+
for (const name of allNames) {
|
|
441
|
+
const parts = name.split(".");
|
|
442
|
+
if (parts.length >= 2) {
|
|
443
|
+
// Handle multi-level TLDs like co.uk, com.tr
|
|
444
|
+
const possibleRoots = [];
|
|
445
|
+
if (parts.length >= 3) {
|
|
446
|
+
// Try 2-level root: domain.tld
|
|
447
|
+
possibleRoots.push(parts.slice(-2).join("."));
|
|
448
|
+
// Try 3-level root: domain.co.uk
|
|
449
|
+
possibleRoots.push(parts.slice(-3).join("."));
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
possibleRoots.push(parts.join("."));
|
|
453
|
+
}
|
|
454
|
+
// Use the 2-level root domain as default
|
|
455
|
+
rootDomains.add(parts.slice(-2).join("."));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
relatedRootDomains = [...rootDomains].filter((d) => d !== domain);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// crt.sh query failed
|
|
463
|
+
}
|
|
464
|
+
// For each unique root domain, get NS records
|
|
465
|
+
const relatedDomains = [];
|
|
466
|
+
// Get NS for primary domain
|
|
467
|
+
let primaryNs = [];
|
|
468
|
+
try {
|
|
469
|
+
await limiter.acquire();
|
|
470
|
+
primaryNs = await dns.resolveNs(domain);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// NS resolution failed
|
|
474
|
+
}
|
|
475
|
+
const nsToMatch = nameserversParam ?? primaryNs;
|
|
476
|
+
for (const relDomain of relatedRootDomains) {
|
|
477
|
+
await limiter.acquire();
|
|
478
|
+
let relNs = [];
|
|
479
|
+
try {
|
|
480
|
+
relNs = await dns.resolveNs(relDomain);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// NS resolution failed
|
|
484
|
+
}
|
|
485
|
+
const sharedNs = nsToMatch.length > 0 &&
|
|
486
|
+
relNs.length > 0 &&
|
|
487
|
+
relNs.some((ns) => nsToMatch.includes(ns));
|
|
488
|
+
relatedDomains.push({
|
|
489
|
+
domain: relDomain,
|
|
490
|
+
source: "certificate_transparency",
|
|
491
|
+
sharedNs,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const intelligenceNotes = [];
|
|
495
|
+
intelligenceNotes.push(`Found ${certSANs.length} unique names in certificate transparency logs.`);
|
|
496
|
+
intelligenceNotes.push(`${relatedRootDomains.length} unique root domain(s) discovered beyond ${domain}.`);
|
|
497
|
+
if (analyticsId) {
|
|
498
|
+
intelligenceNotes.push(`Analytics ID "${analyticsId}" provided — cross-referencing requires fetching page content (not performed here). Use tools like BuiltWith or PublicWWW for verification.`);
|
|
499
|
+
}
|
|
500
|
+
const sharedNsDomains = relatedDomains.filter((d) => d.sharedNs);
|
|
501
|
+
if (sharedNsDomains.length > 0) {
|
|
502
|
+
intelligenceNotes.push(`${sharedNsDomains.length} related domain(s) share nameservers with ${domain} — likely same owner.`);
|
|
503
|
+
}
|
|
504
|
+
const result = {
|
|
505
|
+
domain,
|
|
506
|
+
relatedDomains,
|
|
507
|
+
totalFound: relatedDomains.length,
|
|
508
|
+
certSANs,
|
|
509
|
+
intelligence: {
|
|
510
|
+
notes: intelligenceNotes,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
cache.set(cacheKey, result);
|
|
514
|
+
return json(result);
|
|
515
|
+
}
|
|
516
|
+
// ─── Tool 6: enum_wildcard_detect ───
|
|
517
|
+
async function enumWildcardDetect(args) {
|
|
518
|
+
const domain = args.domain;
|
|
519
|
+
const cacheKey = `enum_wildcard_detect:${domain}`;
|
|
520
|
+
const cached = cache.get(cacheKey);
|
|
521
|
+
if (cached)
|
|
522
|
+
return json(cached);
|
|
523
|
+
// Generate 5 random subdomain names
|
|
524
|
+
const randomSubs = Array.from({ length: 5 }, () => `${randomUUID().slice(0, 8)}.${domain}`);
|
|
525
|
+
const resolvedResults = [];
|
|
526
|
+
for (const sub of randomSubs) {
|
|
527
|
+
await limiter.acquire();
|
|
528
|
+
try {
|
|
529
|
+
const ips = await dns.resolve4(sub);
|
|
530
|
+
resolvedResults.push({ subdomain: sub, ips });
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
resolvedResults.push({ subdomain: sub, ips: null });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Analyze results
|
|
537
|
+
const resolvedCount = resolvedResults.filter((r) => r.ips !== null && r.ips.length > 0).length;
|
|
538
|
+
const unresolvedCount = resolvedResults.filter((r) => r.ips === null || r.ips.length === 0).length;
|
|
539
|
+
let wildcard = false;
|
|
540
|
+
let wildcardIps = [];
|
|
541
|
+
if (resolvedCount === randomSubs.length) {
|
|
542
|
+
// All resolved — check if they all point to the same IP(s)
|
|
543
|
+
const allIps = resolvedResults
|
|
544
|
+
.filter((r) => r.ips !== null)
|
|
545
|
+
.map((r) => r.ips.sort().join(","));
|
|
546
|
+
const uniqueIpSets = new Set(allIps);
|
|
547
|
+
if (uniqueIpSets.size === 1) {
|
|
548
|
+
wildcard = true;
|
|
549
|
+
wildcardIps = resolvedResults[0].ips ?? [];
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
wildcard = "partial";
|
|
553
|
+
const ipSet = new Set();
|
|
554
|
+
for (const r of resolvedResults) {
|
|
555
|
+
if (r.ips) {
|
|
556
|
+
for (const ip of r.ips)
|
|
557
|
+
ipSet.add(ip);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
wildcardIps = [...ipSet];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else if (resolvedCount > 0 && unresolvedCount > 0) {
|
|
564
|
+
wildcard = "partial";
|
|
565
|
+
const ipSet = new Set();
|
|
566
|
+
for (const r of resolvedResults) {
|
|
567
|
+
if (r.ips) {
|
|
568
|
+
for (const ip of r.ips)
|
|
569
|
+
ipSet.add(ip);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
wildcardIps = [...ipSet];
|
|
573
|
+
}
|
|
574
|
+
// Check for wildcard CNAME
|
|
575
|
+
let wildcardCname = null;
|
|
576
|
+
try {
|
|
577
|
+
await limiter.acquire();
|
|
578
|
+
const cnames = await dns.resolveCname(`*.${domain}`);
|
|
579
|
+
if (cnames && cnames.length > 0) {
|
|
580
|
+
wildcardCname = cnames[0];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// No wildcard CNAME
|
|
585
|
+
}
|
|
586
|
+
const result = {
|
|
587
|
+
domain,
|
|
588
|
+
wildcard,
|
|
589
|
+
wildcardIps,
|
|
590
|
+
wildcardCname,
|
|
591
|
+
testedSubdomains: resolvedResults.map((r) => ({
|
|
592
|
+
subdomain: r.subdomain,
|
|
593
|
+
resolved: r.ips !== null && r.ips.length > 0,
|
|
594
|
+
ips: r.ips ?? [],
|
|
595
|
+
})),
|
|
596
|
+
intelligence: {
|
|
597
|
+
note: wildcard === true
|
|
598
|
+
? `Wildcard DNS confirmed for ${domain} — all random subdomains resolve to ${wildcardIps.join(", ")}. Subdomain brute-forcing will produce false positives unless wildcard IPs are filtered.`
|
|
599
|
+
: wildcard === "partial"
|
|
600
|
+
? `Partial wildcard DNS detected for ${domain} — some random subdomains resolve. Results may be inconsistent; consider filtering known wildcard IPs during enumeration.`
|
|
601
|
+
: `No wildcard DNS detected for ${domain} — subdomain enumeration results should be reliable.`,
|
|
602
|
+
wildcardCname: wildcardCname
|
|
603
|
+
? `Wildcard CNAME record found pointing to ${wildcardCname}.`
|
|
604
|
+
: null,
|
|
605
|
+
recommendation: wildcard
|
|
606
|
+
? "Filter wildcard IPs from subdomain enumeration results to avoid false positives."
|
|
607
|
+
: "No filtering needed — proceed with standard subdomain enumeration.",
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
cache.set(cacheKey, result);
|
|
611
|
+
return json(result);
|
|
612
|
+
}
|
|
613
|
+
// ─── Tool 7: enum_scope_summary ───
|
|
614
|
+
async function enumScopeSummary(args) {
|
|
615
|
+
const domain = args.domain;
|
|
616
|
+
const subdomains = args.subdomains ?? [];
|
|
617
|
+
const ips = args.ips ?? [];
|
|
618
|
+
const asns = args.asns ?? [];
|
|
619
|
+
const wildcardZones = args.wildcardZones ?? [];
|
|
620
|
+
// Categorize subdomains
|
|
621
|
+
const categories = {};
|
|
622
|
+
for (const sub of subdomains) {
|
|
623
|
+
const category = categorizeHostname(sub);
|
|
624
|
+
if (!categories[category])
|
|
625
|
+
categories[category] = [];
|
|
626
|
+
categories[category].push(sub);
|
|
627
|
+
}
|
|
628
|
+
// Group IPs into /24 ranges
|
|
629
|
+
const ipRanges = {};
|
|
630
|
+
for (const ip of ips) {
|
|
631
|
+
if (IP_REGEX.test(ip)) {
|
|
632
|
+
const octets = ip.split(".");
|
|
633
|
+
const range = `${octets[0]}.${octets[1]}.${octets[2]}.0/24`;
|
|
634
|
+
if (!ipRanges[range])
|
|
635
|
+
ipRanges[range] = [];
|
|
636
|
+
ipRanges[range].push(ip);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Identify interesting findings
|
|
640
|
+
const interestingCategories = [
|
|
641
|
+
"admin", "staging", "development", "internal", "database", "vpn", "ci-cd", "testing",
|
|
642
|
+
];
|
|
643
|
+
const interestingFindings = [];
|
|
644
|
+
for (const cat of interestingCategories) {
|
|
645
|
+
if (categories[cat] && categories[cat].length > 0) {
|
|
646
|
+
interestingFindings.push({
|
|
647
|
+
category: cat,
|
|
648
|
+
hosts: categories[cat],
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Generate recommended actions
|
|
653
|
+
const recommendedActions = [];
|
|
654
|
+
if (interestingFindings.length > 0) {
|
|
655
|
+
recommendedActions.push(`Investigate ${interestingFindings.length} interesting subdomain categories: ${interestingFindings.map((f) => f.category).join(", ")}.`);
|
|
656
|
+
}
|
|
657
|
+
if (categories["admin"]) {
|
|
658
|
+
recommendedActions.push("Admin panels found — test for default credentials, brute-force protection, and authentication bypasses.");
|
|
659
|
+
}
|
|
660
|
+
if (categories["staging"] || categories["development"]) {
|
|
661
|
+
recommendedActions.push("Staging/dev environments found — these often have weaker security controls. Check for debug modes, exposed source, and unpatched software.");
|
|
662
|
+
}
|
|
663
|
+
if (categories["internal"]) {
|
|
664
|
+
recommendedActions.push("Internal/intranet subdomains found — verify they are not publicly accessible and test for information leakage.");
|
|
665
|
+
}
|
|
666
|
+
if (categories["database"]) {
|
|
667
|
+
recommendedActions.push("Database-related subdomains found — check for exposed database management interfaces (phpMyAdmin, Adminer, etc.).");
|
|
668
|
+
}
|
|
669
|
+
if (categories["vpn"]) {
|
|
670
|
+
recommendedActions.push("VPN endpoints found — test for known VPN vulnerabilities (CVEs for Fortinet, Pulse Secure, etc.).");
|
|
671
|
+
}
|
|
672
|
+
if (categories["ci-cd"]) {
|
|
673
|
+
recommendedActions.push("CI/CD systems found — check for unauthenticated access to Jenkins, GitLab, or build pipelines.");
|
|
674
|
+
}
|
|
675
|
+
if (wildcardZones.length > 0) {
|
|
676
|
+
recommendedActions.push(`${wildcardZones.length} wildcard DNS zone(s) detected — filter wildcard IPs during enumeration to avoid false positives.`);
|
|
677
|
+
}
|
|
678
|
+
if (Object.keys(ipRanges).length > 3) {
|
|
679
|
+
recommendedActions.push(`Infrastructure spans ${Object.keys(ipRanges).length} /24 ranges — consider port scanning key ranges for additional service discovery.`);
|
|
680
|
+
}
|
|
681
|
+
if (asns.length > 1) {
|
|
682
|
+
recommendedActions.push(`${asns.length} ASNs discovered — multi-ASN presence suggests cloud/CDN usage or geographically distributed infrastructure.`);
|
|
683
|
+
}
|
|
684
|
+
if (recommendedActions.length === 0) {
|
|
685
|
+
recommendedActions.push("Run enum_subdomains, enum_tld_expansion, and enum_asn_neighbors to expand scope before summarizing.");
|
|
686
|
+
}
|
|
687
|
+
const result = {
|
|
688
|
+
domain,
|
|
689
|
+
summary: {
|
|
690
|
+
totalSubdomains: subdomains.length,
|
|
691
|
+
totalIps: ips.length,
|
|
692
|
+
totalAsns: asns.length,
|
|
693
|
+
wildcardZones: wildcardZones.length,
|
|
694
|
+
},
|
|
695
|
+
categories,
|
|
696
|
+
ipRanges,
|
|
697
|
+
interestingFindings,
|
|
698
|
+
recommendedActions,
|
|
699
|
+
intelligence: {
|
|
700
|
+
attackSurface: {
|
|
701
|
+
uniqueHosts: subdomains.length,
|
|
702
|
+
ipDiversity: Object.keys(ipRanges).length,
|
|
703
|
+
asnDiversity: asns.length,
|
|
704
|
+
interestingCategoryCount: interestingFindings.length,
|
|
705
|
+
},
|
|
706
|
+
riskLevel: interestingFindings.length > 3
|
|
707
|
+
? "high"
|
|
708
|
+
: interestingFindings.length > 0
|
|
709
|
+
? "medium"
|
|
710
|
+
: "low",
|
|
711
|
+
note: `Attack surface includes ${subdomains.length} hosts across ${Object.keys(ipRanges).length} /24 range(s) and ${asns.length} ASN(s). ${interestingFindings.length} interesting subdomain categories identified for further investigation.`,
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
return json(result);
|
|
715
|
+
}
|
|
716
|
+
// ─── Tool Definitions ───
|
|
717
|
+
export const enumTools = [
|
|
718
|
+
{
|
|
719
|
+
name: "enum_subdomains",
|
|
720
|
+
description: "Subdomain enumeration via SecurityTrails or HackerTarget. Discovers subdomains, resolves their current IPs, and categorizes each hostname (admin, staging, api, vpn, database, ci-cd, etc.). Uses SecurityTrails when API key is available, falls back to HackerTarget (free, no key required).",
|
|
721
|
+
schema: {
|
|
722
|
+
domain: z.string().describe("Target domain for subdomain enumeration"),
|
|
723
|
+
source: z
|
|
724
|
+
.enum(["securitytrails", "hackertarget", "auto"])
|
|
725
|
+
.optional()
|
|
726
|
+
.default("auto")
|
|
727
|
+
.describe("Enumeration source (auto = SecurityTrails if key available, else HackerTarget)"),
|
|
728
|
+
},
|
|
729
|
+
execute: enumSubdomains,
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: "enum_asn_neighbors",
|
|
733
|
+
description: "ASN neighbor discovery. Finds the ASN for a given IP or domain, then enumerates all IP prefixes (CIDR ranges) announced by that ASN. Reveals the full network footprint of an organization and helps identify other IP ranges that may host related services.",
|
|
734
|
+
schema: {
|
|
735
|
+
target: z.string().describe("IP address or domain to find ASN neighbors"),
|
|
736
|
+
limit: z
|
|
737
|
+
.number()
|
|
738
|
+
.optional()
|
|
739
|
+
.default(100)
|
|
740
|
+
.describe("Max results to return (default: 100)"),
|
|
741
|
+
},
|
|
742
|
+
execute: (args) => enumAsnNeighbors(args),
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: "enum_passive_dns",
|
|
746
|
+
description: "Passive DNS history via SecurityTrails. Retrieves historical DNS records showing infrastructure changes over time — IP migrations, hosting provider switches, and DNS configuration evolution. Requires SecurityTrails API key.",
|
|
747
|
+
schema: {
|
|
748
|
+
domain: z.string().describe("Domain for passive DNS history"),
|
|
749
|
+
type: z
|
|
750
|
+
.enum(["a", "aaaa", "mx", "ns", "soa"])
|
|
751
|
+
.optional()
|
|
752
|
+
.default("a")
|
|
753
|
+
.describe("Record type (default: a)"),
|
|
754
|
+
},
|
|
755
|
+
execute: enumPassiveDns,
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: "enum_tld_expansion",
|
|
759
|
+
description: "TLD/ccTLD expansion check. Tests whether the same base domain name exists on other TLDs (.com, .net, .org, .io, .dev, .ai, etc.). Identifies active variations, compares nameservers to detect same-owner domains, and flags domains that may be related or brand-squatted.",
|
|
760
|
+
schema: {
|
|
761
|
+
basename: z.string().describe("Base domain name without TLD (e.g., 'example')"),
|
|
762
|
+
tlds: z
|
|
763
|
+
.array(z.string())
|
|
764
|
+
.optional()
|
|
765
|
+
.describe("TLDs to check (default: 25 common TLDs including com, net, org, io, dev, co, app, ai, xyz, info, biz, us, uk, de, fr, eu, ca, au, nl, ru, cn, jp, in, com.tr, me)"),
|
|
766
|
+
},
|
|
767
|
+
execute: (args) => enumTldExpansion(args),
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: "enum_related_domains",
|
|
771
|
+
description: "Find domains related by shared infrastructure signals. Queries certificate transparency logs (crt.sh) for certificate SANs, extracts unique root domains, and compares nameservers to identify domains likely owned by the same organization. Optionally accepts analytics IDs for cross-reference hints.",
|
|
772
|
+
schema: {
|
|
773
|
+
domain: z.string().describe("Primary domain to find related domains"),
|
|
774
|
+
analyticsId: z
|
|
775
|
+
.string()
|
|
776
|
+
.optional()
|
|
777
|
+
.describe("Google Analytics/GTM ID for cross-reference"),
|
|
778
|
+
nameservers: z
|
|
779
|
+
.array(z.string())
|
|
780
|
+
.optional()
|
|
781
|
+
.describe("Known NS records to match against"),
|
|
782
|
+
},
|
|
783
|
+
execute: (args) => enumRelatedDomains(args),
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: "enum_wildcard_detect",
|
|
787
|
+
description: "Detect wildcard DNS records. Tests random non-existent subdomains to determine if a domain has wildcard DNS configured. Reports whether wildcard is confirmed, partial, or absent, along with wildcard IPs and CNAME targets. Essential before subdomain brute-forcing to avoid false positives.",
|
|
788
|
+
schema: {
|
|
789
|
+
domain: z.string().describe("Domain to check for wildcard DNS"),
|
|
790
|
+
},
|
|
791
|
+
execute: (args) => enumWildcardDetect(args),
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
name: "enum_scope_summary",
|
|
795
|
+
description: "Aggregate scope expansion data into an attack surface summary. Categorizes subdomains, groups IPs into /24 ranges, identifies interesting targets (admin panels, staging/dev environments, databases, VPNs, CI/CD), and generates prioritized recommended actions. Pure local computation, no network requests.",
|
|
796
|
+
schema: {
|
|
797
|
+
domain: z.string().describe("Primary target domain"),
|
|
798
|
+
subdomains: z
|
|
799
|
+
.array(z.string())
|
|
800
|
+
.optional()
|
|
801
|
+
.describe("Discovered subdomains"),
|
|
802
|
+
ips: z
|
|
803
|
+
.array(z.string())
|
|
804
|
+
.optional()
|
|
805
|
+
.describe("Discovered IP addresses"),
|
|
806
|
+
asns: z
|
|
807
|
+
.array(z.string())
|
|
808
|
+
.optional()
|
|
809
|
+
.describe("Discovered ASN numbers"),
|
|
810
|
+
wildcardZones: z
|
|
811
|
+
.array(z.string())
|
|
812
|
+
.optional()
|
|
813
|
+
.describe("Zones with wildcard DNS"),
|
|
814
|
+
},
|
|
815
|
+
execute: (args) => enumScopeSummary(args),
|
|
816
|
+
},
|
|
817
|
+
];
|
|
818
|
+
//# sourceMappingURL=index.js.map
|