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,1332 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RateLimiter } from "../utils/rate-limiter.js";
|
|
3
|
+
import { TTLCache } from "../utils/cache.js";
|
|
4
|
+
import * as tls from "node:tls";
|
|
5
|
+
import * as net from "node:net";
|
|
6
|
+
import { X509Certificate, createHash } from "node:crypto";
|
|
7
|
+
import { json } from "../types/index.js";
|
|
8
|
+
// ─── Module-Level Singletons ───
|
|
9
|
+
const limiter = new RateLimiter(200);
|
|
10
|
+
const cache = new TTLCache(600_000); // 10min cache
|
|
11
|
+
// ─── Helper: TLS Connection ───
|
|
12
|
+
async function tlsConnect(host, port, options) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const opts = {
|
|
15
|
+
host,
|
|
16
|
+
port,
|
|
17
|
+
rejectUnauthorized: false,
|
|
18
|
+
servername: host,
|
|
19
|
+
timeout: 10000,
|
|
20
|
+
...options,
|
|
21
|
+
};
|
|
22
|
+
const socket = tls.connect(opts, () => {
|
|
23
|
+
const cert = socket.getPeerCertificate(true);
|
|
24
|
+
resolve({ socket, cert });
|
|
25
|
+
});
|
|
26
|
+
socket.on("error", reject);
|
|
27
|
+
socket.on("timeout", () => {
|
|
28
|
+
socket.destroy();
|
|
29
|
+
reject(new Error("TLS connection timeout"));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// ─── Helper: Safely close a TLS socket ───
|
|
34
|
+
function safeClose(socket) {
|
|
35
|
+
try {
|
|
36
|
+
socket.end();
|
|
37
|
+
socket.destroy();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// ─── Helper: Parse certificate subject/issuer fields ───
|
|
44
|
+
function parseDN(dn) {
|
|
45
|
+
if (!dn)
|
|
46
|
+
return {};
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const [key, value] of Object.entries(dn)) {
|
|
49
|
+
result[key] = Array.isArray(value) ? value.join(", ") : String(value ?? "");
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
// ─── Helper: Get SHA-256 fingerprint from raw DER cert ───
|
|
54
|
+
function sha256Fingerprint(raw) {
|
|
55
|
+
if (!raw)
|
|
56
|
+
return "N/A";
|
|
57
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
58
|
+
}
|
|
59
|
+
// ─── Helper: DNS resolution for cross-referencing ───
|
|
60
|
+
async function resolveHostname(hostname) {
|
|
61
|
+
const dns = await import("node:dns/promises");
|
|
62
|
+
try {
|
|
63
|
+
const result = await dns.resolve4(hostname);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ─── Helper: Extract SAN list from certificate ───
|
|
71
|
+
function extractSANs(cert) {
|
|
72
|
+
const dns = [];
|
|
73
|
+
const ips = [];
|
|
74
|
+
const subjectaltname = cert.subjectaltname ?? "";
|
|
75
|
+
if (!subjectaltname)
|
|
76
|
+
return { dns, ips };
|
|
77
|
+
for (const entry of subjectaltname.split(",")) {
|
|
78
|
+
const trimmed = entry.trim();
|
|
79
|
+
if (trimmed.startsWith("DNS:")) {
|
|
80
|
+
dns.push(trimmed.slice(4));
|
|
81
|
+
}
|
|
82
|
+
else if (trimmed.startsWith("IP Address:")) {
|
|
83
|
+
ips.push(trimmed.slice(11));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { dns, ips };
|
|
87
|
+
}
|
|
88
|
+
// ─── Helper: Detect malware certificate patterns ───
|
|
89
|
+
function detectMalwareCertPatterns(cert) {
|
|
90
|
+
const warnings = [];
|
|
91
|
+
const subject = parseDN(cert.subject);
|
|
92
|
+
const issuer = parseDN(cert.issuer);
|
|
93
|
+
// CN=localhost
|
|
94
|
+
if (subject.CN === "localhost") {
|
|
95
|
+
warnings.push("Subject CN is 'localhost' — common in malware/C2 certificates");
|
|
96
|
+
}
|
|
97
|
+
// Internet Widgits Pty Ltd (OpenSSL default)
|
|
98
|
+
if (subject.O === "Internet Widgits Pty Ltd") {
|
|
99
|
+
warnings.push("Subject O is 'Internet Widgits Pty Ltd' (OpenSSL default) — common in malware certs");
|
|
100
|
+
}
|
|
101
|
+
// Self-signed detection
|
|
102
|
+
const isSelfSigned = subject.CN === issuer.CN &&
|
|
103
|
+
subject.O === issuer.O &&
|
|
104
|
+
(subject.C ?? "") === (issuer.C ?? "");
|
|
105
|
+
if (isSelfSigned) {
|
|
106
|
+
warnings.push("Certificate is self-signed");
|
|
107
|
+
}
|
|
108
|
+
// Very long validity (>5 years)
|
|
109
|
+
if (cert.valid_from && cert.valid_to) {
|
|
110
|
+
const from = new Date(cert.valid_from).getTime();
|
|
111
|
+
const to = new Date(cert.valid_to).getTime();
|
|
112
|
+
const years = (to - from) / (1000 * 60 * 60 * 24 * 365.25);
|
|
113
|
+
if (years > 5) {
|
|
114
|
+
warnings.push(`Certificate validity period is ${years.toFixed(1)} years (>5 years) — suspicious`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Self-signed with no SAN
|
|
118
|
+
const sans = extractSANs(cert);
|
|
119
|
+
if (isSelfSigned && sans.dns.length === 0 && sans.ips.length === 0) {
|
|
120
|
+
warnings.push("Self-signed certificate with no SAN — strong malware indicator");
|
|
121
|
+
}
|
|
122
|
+
return warnings;
|
|
123
|
+
}
|
|
124
|
+
// ─── Helper: Walk certificate chain ───
|
|
125
|
+
function walkCertChain(cert) {
|
|
126
|
+
const chain = [];
|
|
127
|
+
const visited = new Set();
|
|
128
|
+
let current = cert;
|
|
129
|
+
while (current) {
|
|
130
|
+
const serial = current.serialNumber ?? "unknown";
|
|
131
|
+
if (visited.has(serial))
|
|
132
|
+
break;
|
|
133
|
+
visited.add(serial);
|
|
134
|
+
chain.push({
|
|
135
|
+
subject: parseDN(current.subject),
|
|
136
|
+
issuer: parseDN(current.issuer),
|
|
137
|
+
serialNumber: serial,
|
|
138
|
+
validFrom: current.valid_from ?? "N/A",
|
|
139
|
+
validTo: current.valid_to ?? "N/A",
|
|
140
|
+
fingerprint256: current.fingerprint256 ?? sha256Fingerprint(current.raw),
|
|
141
|
+
});
|
|
142
|
+
// Walk up the chain
|
|
143
|
+
const issuerCert = current.issuerCertificate;
|
|
144
|
+
if (!issuerCert || issuerCert === current)
|
|
145
|
+
break;
|
|
146
|
+
current = issuerCert;
|
|
147
|
+
}
|
|
148
|
+
return chain;
|
|
149
|
+
}
|
|
150
|
+
// ─── Helper: Compute RDN ordering string ───
|
|
151
|
+
function rdnOrder(dn) {
|
|
152
|
+
// Standard RDN field order as they appear in the certificate
|
|
153
|
+
const knownFields = ["CN", "O", "OU", "L", "ST", "C", "emailAddress", "serialNumber"];
|
|
154
|
+
const order = [];
|
|
155
|
+
for (const field of knownFields) {
|
|
156
|
+
if (dn[field] !== undefined && dn[field] !== "") {
|
|
157
|
+
order.push(field);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Include any non-standard fields
|
|
161
|
+
for (const key of Object.keys(dn)) {
|
|
162
|
+
if (!knownFields.includes(key) && dn[key] !== undefined && dn[key] !== "") {
|
|
163
|
+
order.push(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return order.join(",");
|
|
167
|
+
}
|
|
168
|
+
// ─── Helper: Extract extension OIDs from X509Certificate ───
|
|
169
|
+
function extractExtensionOIDs(cert) {
|
|
170
|
+
try {
|
|
171
|
+
if (!cert.raw)
|
|
172
|
+
return [];
|
|
173
|
+
const x509 = new X509Certificate(cert.raw);
|
|
174
|
+
const infoAccess = x509.infoAccess;
|
|
175
|
+
const keyUsage = x509.keyUsage;
|
|
176
|
+
const oids = [];
|
|
177
|
+
// Standard extensions we can detect
|
|
178
|
+
if (x509.subjectAltName)
|
|
179
|
+
oids.push("2.5.29.17"); // SAN
|
|
180
|
+
if (keyUsage && keyUsage.length > 0)
|
|
181
|
+
oids.push("2.5.29.15"); // Key Usage
|
|
182
|
+
if (infoAccess) {
|
|
183
|
+
oids.push("1.3.6.1.5.5.7.1.1"); // Authority Info Access
|
|
184
|
+
if (infoAccess.includes("OCSP"))
|
|
185
|
+
oids.push("1.3.6.1.5.5.7.48.1"); // OCSP
|
|
186
|
+
if (infoAccess.includes("CA Issuers"))
|
|
187
|
+
oids.push("1.3.6.1.5.5.7.48.2"); // CA Issuers
|
|
188
|
+
}
|
|
189
|
+
// Basic Constraints — detect via ca property
|
|
190
|
+
if (x509.ca !== undefined)
|
|
191
|
+
oids.push("2.5.29.19");
|
|
192
|
+
// Subject Key Identifier
|
|
193
|
+
if (cert.serialNumber)
|
|
194
|
+
oids.push("2.5.29.14");
|
|
195
|
+
// Authority Key Identifier
|
|
196
|
+
if (cert.issuerCertificate)
|
|
197
|
+
oids.push("2.5.29.35");
|
|
198
|
+
// Extended Key Usage — check via ext_key_usage
|
|
199
|
+
if (cert.ext_key_usage)
|
|
200
|
+
oids.push("2.5.29.37");
|
|
201
|
+
// CRL Distribution Points
|
|
202
|
+
oids.push("2.5.29.31");
|
|
203
|
+
// SCT (Certificate Transparency)
|
|
204
|
+
// OID: 1.3.6.1.4.1.11129.2.4.2
|
|
205
|
+
// We check the raw cert for this OID as a rough heuristic
|
|
206
|
+
if (cert.raw) {
|
|
207
|
+
const rawHex = cert.raw.toString("hex");
|
|
208
|
+
// SCT extension OID in DER: 060a2b06010401d679020402
|
|
209
|
+
if (rawHex.includes("060a2b06010401d679020402")) {
|
|
210
|
+
oids.push("1.3.6.1.4.1.11129.2.4.2");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return oids;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// ─── Helper: Detect key type and size from X509Certificate ───
|
|
220
|
+
function detectKeyInfo(cert) {
|
|
221
|
+
try {
|
|
222
|
+
if (!cert.raw)
|
|
223
|
+
return { type: "unknown", size: "unknown" };
|
|
224
|
+
const x509 = new X509Certificate(cert.raw);
|
|
225
|
+
const pubkey = x509.publicKey;
|
|
226
|
+
const keyType = pubkey.asymmetricKeyType ?? "unknown";
|
|
227
|
+
const keySize = pubkey.asymmetricKeySize ?? "unknown";
|
|
228
|
+
return { type: keyType.toUpperCase(), size: keySize };
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Fallback: try to infer from bits field
|
|
232
|
+
return {
|
|
233
|
+
type: cert.asn1Curve ? "ECDSA" : "RSA",
|
|
234
|
+
size: cert.bits ?? "unknown",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ─── Helper: Check for CT SCT extension ───
|
|
239
|
+
function hasSCTExtension(cert) {
|
|
240
|
+
if (!cert.raw)
|
|
241
|
+
return false;
|
|
242
|
+
const rawHex = cert.raw.toString("hex");
|
|
243
|
+
// SCT extension OID in DER encoding: 060a2b06010401d679020402
|
|
244
|
+
return rawHex.includes("060a2b06010401d679020402");
|
|
245
|
+
}
|
|
246
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
247
|
+
// Tool 1: tls_probe — TLS Handshake Analysis
|
|
248
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
const tlsProbe = {
|
|
250
|
+
name: "tls_probe",
|
|
251
|
+
description: "TLS handshake analysis. Connects via TLS and captures negotiated version, cipher suite, ALPN result, session ticket support, OCSP stapling, renegotiation info, and certificate validation status.",
|
|
252
|
+
schema: {
|
|
253
|
+
host: z.string().describe("Target hostname or IP"),
|
|
254
|
+
port: z.number().optional().default(443).describe("TLS port"),
|
|
255
|
+
},
|
|
256
|
+
async execute(args) {
|
|
257
|
+
const host = args.host;
|
|
258
|
+
const port = args.port ?? 443;
|
|
259
|
+
const cacheKey = `tls_probe:${host}:${port}`;
|
|
260
|
+
const cached = cache.get(cacheKey);
|
|
261
|
+
if (cached)
|
|
262
|
+
return json(cached);
|
|
263
|
+
await limiter.acquire();
|
|
264
|
+
const { socket, cert } = await tlsConnect(host, port);
|
|
265
|
+
try {
|
|
266
|
+
const cipher = socket.getCipher();
|
|
267
|
+
const protocol = socket.getProtocol();
|
|
268
|
+
const alpn = socket.alpnProtocol ?? null;
|
|
269
|
+
const authorized = socket.authorized;
|
|
270
|
+
const authError = socket.authorizationError;
|
|
271
|
+
const ephemeralKeyInfo = socket.getEphemeralKeyInfo?.() ?? null;
|
|
272
|
+
const peerFinished = socket.getPeerFinished();
|
|
273
|
+
const finished = socket.getFinished();
|
|
274
|
+
// Session info
|
|
275
|
+
const session = socket.getSession();
|
|
276
|
+
const hasSession = !!session;
|
|
277
|
+
// TLS version numerical check
|
|
278
|
+
const isTls13 = protocol === "TLSv1.3";
|
|
279
|
+
const result = {
|
|
280
|
+
host,
|
|
281
|
+
port,
|
|
282
|
+
tls: {
|
|
283
|
+
version: protocol,
|
|
284
|
+
cipher: {
|
|
285
|
+
name: cipher?.name ?? "unknown",
|
|
286
|
+
standardName: cipher?.standardName ?? cipher?.name ?? "unknown",
|
|
287
|
+
version: cipher?.version ?? "unknown",
|
|
288
|
+
bits: cipher?.bits ?? null,
|
|
289
|
+
},
|
|
290
|
+
alpn: alpn || null,
|
|
291
|
+
authorized,
|
|
292
|
+
authorizationError: authError || null,
|
|
293
|
+
ephemeralKey: ephemeralKeyInfo
|
|
294
|
+
? {
|
|
295
|
+
type: ephemeralKeyInfo.type ?? "unknown",
|
|
296
|
+
size: ephemeralKeyInfo.size ?? null,
|
|
297
|
+
name: ephemeralKeyInfo.name ?? null,
|
|
298
|
+
}
|
|
299
|
+
: null,
|
|
300
|
+
sessionTicket: hasSession,
|
|
301
|
+
isTls13,
|
|
302
|
+
peerCertificate: {
|
|
303
|
+
subject: parseDN(cert.subject),
|
|
304
|
+
issuer: parseDN(cert.issuer),
|
|
305
|
+
validFrom: cert.valid_from ?? "N/A",
|
|
306
|
+
validTo: cert.valid_to ?? "N/A",
|
|
307
|
+
serialNumber: cert.serialNumber ?? "N/A",
|
|
308
|
+
},
|
|
309
|
+
renegotiation: {
|
|
310
|
+
peerFinished: !!peerFinished,
|
|
311
|
+
finished: !!finished,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
cache.set(cacheKey, result);
|
|
316
|
+
return json(result);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
safeClose(socket);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
324
|
+
// Tool 2: tls_jarm — JARM Fingerprint
|
|
325
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
326
|
+
// JARM probe configurations: 10 different TLS connection parameters
|
|
327
|
+
const JARM_PROBES = [
|
|
328
|
+
// Probe 1: TLS 1.2 only, all ciphers
|
|
329
|
+
{ label: "tls12_all", minVersion: "TLSv1.2", maxVersion: "TLSv1.2" },
|
|
330
|
+
// Probe 2: TLS 1.3 only
|
|
331
|
+
{ label: "tls13_only", minVersion: "TLSv1.3", maxVersion: "TLSv1.3" },
|
|
332
|
+
// Probe 3: TLS 1.2, forward-secrecy only
|
|
333
|
+
{ label: "tls12_fs", minVersion: "TLSv1.2", maxVersion: "TLSv1.2", ciphers: "ECDHE:DHE" },
|
|
334
|
+
// Probe 4: TLS 1.2, RSA key exchange
|
|
335
|
+
{ label: "tls12_rsa", minVersion: "TLSv1.2", maxVersion: "TLSv1.2", ciphers: "RSA" },
|
|
336
|
+
// Probe 5: TLS 1.2, AES-GCM only
|
|
337
|
+
{
|
|
338
|
+
label: "tls12_aesgcm",
|
|
339
|
+
minVersion: "TLSv1.2",
|
|
340
|
+
maxVersion: "TLSv1.2",
|
|
341
|
+
ciphers: "AESGCM",
|
|
342
|
+
},
|
|
343
|
+
// Probe 6: TLS 1.2, ChaCha20 only
|
|
344
|
+
{
|
|
345
|
+
label: "tls12_chacha",
|
|
346
|
+
minVersion: "TLSv1.2",
|
|
347
|
+
maxVersion: "TLSv1.2",
|
|
348
|
+
ciphers: "CHACHA20",
|
|
349
|
+
},
|
|
350
|
+
// Probe 7: TLS 1.2 with ALPN h2
|
|
351
|
+
{ label: "tls12_h2", minVersion: "TLSv1.2", maxVersion: "TLSv1.2", alpn: ["h2"] },
|
|
352
|
+
// Probe 8: TLS 1.2 with ALPN http/1.1
|
|
353
|
+
{
|
|
354
|
+
label: "tls12_h1",
|
|
355
|
+
minVersion: "TLSv1.2",
|
|
356
|
+
maxVersion: "TLSv1.2",
|
|
357
|
+
alpn: ["http/1.1"],
|
|
358
|
+
},
|
|
359
|
+
// Probe 9: Widest range (TLS 1.2 - 1.3)
|
|
360
|
+
{ label: "tls_wide", minVersion: "TLSv1.2", maxVersion: "TLSv1.3" },
|
|
361
|
+
// Probe 10: TLS 1.3 with ALPN h2
|
|
362
|
+
{ label: "tls13_h2", minVersion: "TLSv1.3", maxVersion: "TLSv1.3", alpn: ["h2"] },
|
|
363
|
+
];
|
|
364
|
+
async function jarmProbe(host, port, probe) {
|
|
365
|
+
try {
|
|
366
|
+
const opts = {
|
|
367
|
+
minVersion: probe.minVersion,
|
|
368
|
+
maxVersion: probe.maxVersion,
|
|
369
|
+
};
|
|
370
|
+
if (probe.ciphers) {
|
|
371
|
+
opts.ciphers = probe.ciphers;
|
|
372
|
+
}
|
|
373
|
+
if (probe.alpn) {
|
|
374
|
+
opts.ALPNProtocols = probe.alpn;
|
|
375
|
+
}
|
|
376
|
+
const { socket } = await tlsConnect(host, port, opts);
|
|
377
|
+
try {
|
|
378
|
+
const cipher = socket.getCipher();
|
|
379
|
+
const protocol = socket.getProtocol();
|
|
380
|
+
if (!cipher || !protocol)
|
|
381
|
+
return "000";
|
|
382
|
+
// Extract first 3 hex chars from cipher name hash + TLS version indicator
|
|
383
|
+
const cipherHash = createHash("md5").update(cipher.name ?? "").digest("hex");
|
|
384
|
+
const versionByte = protocol === "TLSv1.3" ? "d" : protocol === "TLSv1.2" ? "c" : "0";
|
|
385
|
+
return cipherHash.slice(0, 3) + versionByte;
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
safeClose(socket);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return "0000";
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const tlsJarm = {
|
|
396
|
+
name: "tls_jarm",
|
|
397
|
+
description: "JARM TLS fingerprinting. Sends 10 TLS probes with varying protocol versions, cipher preferences, and ALPN settings to generate a server fingerprint. Useful for identifying C2 frameworks, web servers, and CDN infrastructure.",
|
|
398
|
+
schema: {
|
|
399
|
+
host: z.string().describe("Target hostname or IP"),
|
|
400
|
+
port: z.number().optional().default(443).describe("TLS port"),
|
|
401
|
+
},
|
|
402
|
+
async execute(args) {
|
|
403
|
+
const host = args.host;
|
|
404
|
+
const port = args.port ?? 443;
|
|
405
|
+
const cacheKey = `tls_jarm:${host}:${port}`;
|
|
406
|
+
const cached = cache.get(cacheKey);
|
|
407
|
+
if (cached)
|
|
408
|
+
return json(cached);
|
|
409
|
+
await limiter.acquire();
|
|
410
|
+
// Run all 10 probes
|
|
411
|
+
const probeResults = [];
|
|
412
|
+
const probeDetails = [];
|
|
413
|
+
for (const probe of JARM_PROBES) {
|
|
414
|
+
await limiter.acquire();
|
|
415
|
+
const response = await jarmProbe(host, port, probe);
|
|
416
|
+
probeResults.push(response);
|
|
417
|
+
probeDetails.push({ label: probe.label, response });
|
|
418
|
+
}
|
|
419
|
+
// Build raw fingerprint from concatenated probe results
|
|
420
|
+
const rawFingerprint = probeResults.join("");
|
|
421
|
+
// Hash the raw fingerprint to produce the JARM hash (62 chars)
|
|
422
|
+
const jarmHash = createHash("sha256").update(rawFingerprint).digest("hex").slice(0, 62);
|
|
423
|
+
// Try to match against known JARM signatures
|
|
424
|
+
let matchedSignature = null;
|
|
425
|
+
try {
|
|
426
|
+
const { JARM_SIGNATURES } = await import("../data/jarm-signatures.js");
|
|
427
|
+
const match = JARM_SIGNATURES.find((sig) => sig.hash === jarmHash);
|
|
428
|
+
if (match) {
|
|
429
|
+
matchedSignature = {
|
|
430
|
+
name: match.name,
|
|
431
|
+
category: match.category,
|
|
432
|
+
confidence: match.confidence,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Signature database not available
|
|
438
|
+
}
|
|
439
|
+
const result = {
|
|
440
|
+
host,
|
|
441
|
+
port,
|
|
442
|
+
jarm: {
|
|
443
|
+
hash: jarmHash,
|
|
444
|
+
rawFingerprint,
|
|
445
|
+
probeCount: probeResults.length,
|
|
446
|
+
probes: probeDetails,
|
|
447
|
+
match: matchedSignature,
|
|
448
|
+
note: "Simplified JARM implementation — probes TLS server responses with varying parameters. For full accuracy, use the official Salesforce JARM tool.",
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
cache.set(cacheKey, result);
|
|
452
|
+
return json(result);
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
456
|
+
// Tool 3: tls_cert — X.509 Certificate Deep Analysis
|
|
457
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
458
|
+
const tlsCert = {
|
|
459
|
+
name: "tls_cert",
|
|
460
|
+
description: "X.509 certificate deep analysis. Extracts subject, issuer, SAN list, validity, key type/size, signature algorithm, fingerprint, full chain, self-signed detection, malware cert patterns, CT/SCT compliance, and wildcard detection.",
|
|
461
|
+
schema: {
|
|
462
|
+
host: z.string().describe("Target hostname or IP"),
|
|
463
|
+
port: z.number().optional().default(443).describe("TLS port"),
|
|
464
|
+
},
|
|
465
|
+
async execute(args) {
|
|
466
|
+
const host = args.host;
|
|
467
|
+
const port = args.port ?? 443;
|
|
468
|
+
const cacheKey = `tls_cert:${host}:${port}`;
|
|
469
|
+
const cached = cache.get(cacheKey);
|
|
470
|
+
if (cached)
|
|
471
|
+
return json(cached);
|
|
472
|
+
await limiter.acquire();
|
|
473
|
+
const { socket, cert } = await tlsConnect(host, port);
|
|
474
|
+
try {
|
|
475
|
+
const subject = parseDN(cert.subject);
|
|
476
|
+
const issuer = parseDN(cert.issuer);
|
|
477
|
+
const sans = extractSANs(cert);
|
|
478
|
+
const keyInfo = detectKeyInfo(cert);
|
|
479
|
+
const malwareWarnings = detectMalwareCertPatterns(cert);
|
|
480
|
+
const chain = walkCertChain(cert);
|
|
481
|
+
const sctPresent = hasSCTExtension(cert);
|
|
482
|
+
// Self-signed detection
|
|
483
|
+
const isSelfSigned = subject.CN === issuer.CN &&
|
|
484
|
+
subject.O === issuer.O &&
|
|
485
|
+
(subject.C ?? "") === (issuer.C ?? "");
|
|
486
|
+
// Wildcard detection
|
|
487
|
+
const wildcardDomains = sans.dns.filter((d) => d.startsWith("*."));
|
|
488
|
+
const isWildcard = wildcardDomains.length > 0 || (subject.CN ?? "").startsWith("*.");
|
|
489
|
+
// Validity period
|
|
490
|
+
const validFrom = cert.valid_from ? new Date(cert.valid_from) : null;
|
|
491
|
+
const validTo = cert.valid_to ? new Date(cert.valid_to) : null;
|
|
492
|
+
const now = new Date();
|
|
493
|
+
const isExpired = validTo ? now > validTo : false;
|
|
494
|
+
const isNotYetValid = validFrom ? now < validFrom : false;
|
|
495
|
+
const daysRemaining = validTo
|
|
496
|
+
? Math.floor((validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
|
497
|
+
: null;
|
|
498
|
+
// Signature algorithm from X509Certificate
|
|
499
|
+
let signatureAlgorithm = "unknown";
|
|
500
|
+
try {
|
|
501
|
+
if (cert.raw) {
|
|
502
|
+
const x509 = new X509Certificate(cert.raw);
|
|
503
|
+
// The sigAlgName is not directly exposed; try fingerprint256 approach
|
|
504
|
+
// Node's X509Certificate doesn't expose sigAlg directly, but we can detect from raw
|
|
505
|
+
const rawHex = cert.raw.toString("hex");
|
|
506
|
+
if (rawHex.includes("2a864886f70d01010b"))
|
|
507
|
+
signatureAlgorithm = "sha256WithRSAEncryption";
|
|
508
|
+
else if (rawHex.includes("2a864886f70d010105"))
|
|
509
|
+
signatureAlgorithm = "sha1WithRSAEncryption";
|
|
510
|
+
else if (rawHex.includes("2a864886f70d01010c"))
|
|
511
|
+
signatureAlgorithm = "sha384WithRSAEncryption";
|
|
512
|
+
else if (rawHex.includes("2a864886f70d01010d"))
|
|
513
|
+
signatureAlgorithm = "sha512WithRSAEncryption";
|
|
514
|
+
else if (rawHex.includes("2a8648ce3d040302"))
|
|
515
|
+
signatureAlgorithm = "ecdsa-with-SHA256";
|
|
516
|
+
else if (rawHex.includes("2a8648ce3d040303"))
|
|
517
|
+
signatureAlgorithm = "ecdsa-with-SHA384";
|
|
518
|
+
else if (rawHex.includes("2b6570"))
|
|
519
|
+
signatureAlgorithm = "Ed25519";
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// ignore
|
|
524
|
+
}
|
|
525
|
+
const fingerprint256 = cert.fingerprint256 ?? sha256Fingerprint(cert.raw);
|
|
526
|
+
const result = {
|
|
527
|
+
host,
|
|
528
|
+
port,
|
|
529
|
+
certificate: {
|
|
530
|
+
subject,
|
|
531
|
+
issuer,
|
|
532
|
+
san: {
|
|
533
|
+
dns: sans.dns,
|
|
534
|
+
ips: sans.ips,
|
|
535
|
+
total: sans.dns.length + sans.ips.length,
|
|
536
|
+
},
|
|
537
|
+
validity: {
|
|
538
|
+
notBefore: cert.valid_from ?? "N/A",
|
|
539
|
+
notAfter: cert.valid_to ?? "N/A",
|
|
540
|
+
isExpired,
|
|
541
|
+
isNotYetValid,
|
|
542
|
+
daysRemaining,
|
|
543
|
+
},
|
|
544
|
+
serialNumber: cert.serialNumber ?? "N/A",
|
|
545
|
+
key: {
|
|
546
|
+
type: keyInfo.type,
|
|
547
|
+
size: keyInfo.size,
|
|
548
|
+
},
|
|
549
|
+
signatureAlgorithm,
|
|
550
|
+
fingerprint: {
|
|
551
|
+
sha256: fingerprint256,
|
|
552
|
+
},
|
|
553
|
+
chain: {
|
|
554
|
+
depth: chain.length,
|
|
555
|
+
certificates: chain,
|
|
556
|
+
},
|
|
557
|
+
analysis: {
|
|
558
|
+
isSelfSigned,
|
|
559
|
+
isWildcard,
|
|
560
|
+
wildcardDomains,
|
|
561
|
+
ctCompliance: sctPresent,
|
|
562
|
+
sctExtensionPresent: sctPresent,
|
|
563
|
+
malwareIndicators: malwareWarnings,
|
|
564
|
+
malwareScore: malwareWarnings.length === 0
|
|
565
|
+
? "clean"
|
|
566
|
+
: malwareWarnings.length <= 2
|
|
567
|
+
? "suspicious"
|
|
568
|
+
: "likely_malicious",
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
cache.set(cacheKey, result);
|
|
573
|
+
return json(result);
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
safeClose(socket);
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
581
|
+
// Tool 4: tls_ja4x — JA4X Certificate Generation Fingerprint
|
|
582
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
583
|
+
// Known JA4X patterns for certificate generation tools
|
|
584
|
+
const JA4X_KNOWN_TOOLS = {
|
|
585
|
+
"CN,O,L,ST,C": "OpenSSL (default)",
|
|
586
|
+
"CN,O,OU,L,ST,C": "OpenSSL (full subject)",
|
|
587
|
+
CN: "mkcert / simple tool",
|
|
588
|
+
"C,ST,L,O,OU,CN": "Java keytool",
|
|
589
|
+
"C,O,CN": "Let's Encrypt / ACME",
|
|
590
|
+
"O,CN": "Cobalt Strike (common pattern)",
|
|
591
|
+
"C,ST,O,CN": "Microsoft PKI / Active Directory CS",
|
|
592
|
+
};
|
|
593
|
+
const tlsJa4x = {
|
|
594
|
+
name: "tls_ja4x",
|
|
595
|
+
description: "JA4X certificate generation fingerprint. Fingerprints HOW a certificate was generated by analyzing issuer/subject RDN ordering and extension OID list. Identifies the tool that generated the cert (Cobalt Strike, mkcert, OpenSSL, Let's Encrypt, etc.).",
|
|
596
|
+
schema: {
|
|
597
|
+
host: z.string().describe("Target hostname or IP"),
|
|
598
|
+
port: z.number().optional().default(443).describe("TLS port"),
|
|
599
|
+
},
|
|
600
|
+
async execute(args) {
|
|
601
|
+
const host = args.host;
|
|
602
|
+
const port = args.port ?? 443;
|
|
603
|
+
const cacheKey = `tls_ja4x:${host}:${port}`;
|
|
604
|
+
const cached = cache.get(cacheKey);
|
|
605
|
+
if (cached)
|
|
606
|
+
return json(cached);
|
|
607
|
+
await limiter.acquire();
|
|
608
|
+
const { socket, cert } = await tlsConnect(host, port);
|
|
609
|
+
try {
|
|
610
|
+
const issuerDN = parseDN(cert.issuer);
|
|
611
|
+
const subjectDN = parseDN(cert.subject);
|
|
612
|
+
// RDN ordering
|
|
613
|
+
const issuerRdnOrder = rdnOrder(issuerDN);
|
|
614
|
+
const subjectRdnOrder = rdnOrder(subjectDN);
|
|
615
|
+
// Extension OIDs
|
|
616
|
+
const extensionOIDs = extractExtensionOIDs(cert);
|
|
617
|
+
const extensionStr = extensionOIDs.join(",");
|
|
618
|
+
// Hash each section
|
|
619
|
+
const issuerHash = createHash("sha256").update(issuerRdnOrder).digest("hex").slice(0, 12);
|
|
620
|
+
const subjectHash = createHash("sha256").update(subjectRdnOrder).digest("hex").slice(0, 12);
|
|
621
|
+
const extensionHash = createHash("sha256").update(extensionStr).digest("hex").slice(0, 12);
|
|
622
|
+
// JA4X format: {issuer_hash}_{subject_hash}_{extension_hash}
|
|
623
|
+
const ja4xFingerprint = `${issuerHash}_${subjectHash}_${extensionHash}`;
|
|
624
|
+
// Attempt tool identification
|
|
625
|
+
let toolGuess = null;
|
|
626
|
+
for (const [pattern, tool] of Object.entries(JA4X_KNOWN_TOOLS)) {
|
|
627
|
+
if (subjectRdnOrder === pattern || issuerRdnOrder === pattern) {
|
|
628
|
+
toolGuess = tool;
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const result = {
|
|
633
|
+
host,
|
|
634
|
+
port,
|
|
635
|
+
ja4x: {
|
|
636
|
+
fingerprint: ja4xFingerprint,
|
|
637
|
+
components: {
|
|
638
|
+
issuer: {
|
|
639
|
+
rdnOrder: issuerRdnOrder,
|
|
640
|
+
hash: issuerHash,
|
|
641
|
+
fields: issuerDN,
|
|
642
|
+
},
|
|
643
|
+
subject: {
|
|
644
|
+
rdnOrder: subjectRdnOrder,
|
|
645
|
+
hash: subjectHash,
|
|
646
|
+
fields: subjectDN,
|
|
647
|
+
},
|
|
648
|
+
extensions: {
|
|
649
|
+
oids: extensionOIDs,
|
|
650
|
+
hash: extensionHash,
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
toolIdentification: toolGuess
|
|
654
|
+
? {
|
|
655
|
+
tool: toolGuess,
|
|
656
|
+
matchedOn: subjectRdnOrder,
|
|
657
|
+
confidence: "medium",
|
|
658
|
+
note: "Based on RDN ordering pattern match against known certificate generation tools",
|
|
659
|
+
}
|
|
660
|
+
: {
|
|
661
|
+
tool: "unknown",
|
|
662
|
+
note: "RDN ordering does not match any known certificate generation tool pattern",
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
cache.set(cacheKey, result);
|
|
667
|
+
return json(result);
|
|
668
|
+
}
|
|
669
|
+
finally {
|
|
670
|
+
safeClose(socket);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
675
|
+
// Tool 5: tls_cert_cross_ref — Certificate Cross-Reference
|
|
676
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
677
|
+
const tlsCertCrossRef = {
|
|
678
|
+
name: "tls_cert_cross_ref",
|
|
679
|
+
description: "Cross-reference certificate SAN/fingerprint for infrastructure mapping. Extracts all SANs, resolves each domain to IP, and generates Shodan/Censys queries for the cert fingerprint to discover shared infrastructure.",
|
|
680
|
+
schema: {
|
|
681
|
+
host: z.string().describe("Target hostname or IP"),
|
|
682
|
+
port: z.number().optional().default(443).describe("TLS port"),
|
|
683
|
+
},
|
|
684
|
+
async execute(args) {
|
|
685
|
+
const host = args.host;
|
|
686
|
+
const port = args.port ?? 443;
|
|
687
|
+
const cacheKey = `tls_cert_cross_ref:${host}:${port}`;
|
|
688
|
+
const cached = cache.get(cacheKey);
|
|
689
|
+
if (cached)
|
|
690
|
+
return json(cached);
|
|
691
|
+
await limiter.acquire();
|
|
692
|
+
const { socket, cert } = await tlsConnect(host, port);
|
|
693
|
+
try {
|
|
694
|
+
const sans = extractSANs(cert);
|
|
695
|
+
const fingerprint256 = cert.fingerprint256 ?? sha256Fingerprint(cert.raw);
|
|
696
|
+
const serialNumber = cert.serialNumber ?? "N/A";
|
|
697
|
+
// Resolve each SAN domain to IP addresses
|
|
698
|
+
const domainResolutions = [];
|
|
699
|
+
const allIps = new Set();
|
|
700
|
+
// Add IPs from SAN directly
|
|
701
|
+
for (const ip of sans.ips) {
|
|
702
|
+
allIps.add(ip);
|
|
703
|
+
}
|
|
704
|
+
// Resolve DNS SANs (limit to avoid excessive DNS queries)
|
|
705
|
+
const domainsToResolve = sans.dns.slice(0, 50);
|
|
706
|
+
for (const domain of domainsToResolve) {
|
|
707
|
+
// Skip wildcard domains for resolution
|
|
708
|
+
if (domain.startsWith("*.")) {
|
|
709
|
+
domainResolutions.push({ domain, ips: ["(wildcard — not resolved)"] });
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const ips = await resolveHostname(domain);
|
|
714
|
+
domainResolutions.push({ domain, ips });
|
|
715
|
+
for (const ip of ips)
|
|
716
|
+
allIps.add(ip);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
domainResolutions.push({ domain, ips: ["(resolution failed)"] });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Identify interesting subdomains
|
|
723
|
+
const internalKeywords = [
|
|
724
|
+
"staging",
|
|
725
|
+
"stage",
|
|
726
|
+
"dev",
|
|
727
|
+
"test",
|
|
728
|
+
"internal",
|
|
729
|
+
"admin",
|
|
730
|
+
"jenkins",
|
|
731
|
+
"vault",
|
|
732
|
+
"gitlab",
|
|
733
|
+
"jira",
|
|
734
|
+
"confluence",
|
|
735
|
+
"grafana",
|
|
736
|
+
"kibana",
|
|
737
|
+
"prometheus",
|
|
738
|
+
"api",
|
|
739
|
+
"vpn",
|
|
740
|
+
"mail",
|
|
741
|
+
"mx",
|
|
742
|
+
"smtp",
|
|
743
|
+
"imap",
|
|
744
|
+
"pop",
|
|
745
|
+
"ftp",
|
|
746
|
+
"ssh",
|
|
747
|
+
"rdp",
|
|
748
|
+
"db",
|
|
749
|
+
"database",
|
|
750
|
+
"mongo",
|
|
751
|
+
"redis",
|
|
752
|
+
"elastic",
|
|
753
|
+
"kafka",
|
|
754
|
+
];
|
|
755
|
+
const interestingDomains = sans.dns.filter((d) => internalKeywords.some((kw) => d.toLowerCase().includes(kw)));
|
|
756
|
+
// Clean fingerprint for search queries (remove colons)
|
|
757
|
+
const cleanFingerprint = fingerprint256.replace(/:/g, "");
|
|
758
|
+
const result = {
|
|
759
|
+
host,
|
|
760
|
+
port,
|
|
761
|
+
crossReference: {
|
|
762
|
+
certificate: {
|
|
763
|
+
fingerprint256: cleanFingerprint,
|
|
764
|
+
serialNumber,
|
|
765
|
+
subject: parseDN(cert.subject),
|
|
766
|
+
},
|
|
767
|
+
san: {
|
|
768
|
+
totalDomains: sans.dns.length,
|
|
769
|
+
totalIPs: sans.ips.length,
|
|
770
|
+
domains: sans.dns,
|
|
771
|
+
ips: sans.ips,
|
|
772
|
+
},
|
|
773
|
+
dnsResolution: {
|
|
774
|
+
resolved: domainResolutions,
|
|
775
|
+
uniqueIPs: Array.from(allIps),
|
|
776
|
+
totalUniqueIPs: allIps.size,
|
|
777
|
+
},
|
|
778
|
+
interestingFindings: {
|
|
779
|
+
internalDomains: interestingDomains,
|
|
780
|
+
wildcardDomains: sans.dns.filter((d) => d.startsWith("*.")),
|
|
781
|
+
},
|
|
782
|
+
searchQueries: {
|
|
783
|
+
shodan: {
|
|
784
|
+
byFingerprint: `ssl.cert.fingerprint:"${cleanFingerprint}"`,
|
|
785
|
+
bySerial: `ssl.cert.serial:"${serialNumber}"`,
|
|
786
|
+
byIssuerOrg: cert.issuer?.O
|
|
787
|
+
? `ssl.cert.issuer.o:"${cert.issuer.O}"`
|
|
788
|
+
: null,
|
|
789
|
+
bySubjectCN: cert.subject?.CN
|
|
790
|
+
? `ssl.cert.subject.cn:"${cert.subject.CN}"`
|
|
791
|
+
: null,
|
|
792
|
+
},
|
|
793
|
+
censys: {
|
|
794
|
+
byFingerprint: `services.tls.certificates.leaf.fingerprint_sha256:"${cleanFingerprint}"`,
|
|
795
|
+
bySerial: `services.tls.certificates.leaf.tbs_certificate.serial_number:"${serialNumber}"`,
|
|
796
|
+
bySubjectCN: cert.subject?.CN
|
|
797
|
+
? `services.tls.certificates.leaf.tbs_certificate.subject.common_name:"${cert.subject.CN}"`
|
|
798
|
+
: null,
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
cache.set(cacheKey, result);
|
|
804
|
+
return json(result);
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
safeClose(socket);
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
812
|
+
// Tool 6: tls_ct_subdomains — Certificate Transparency Subdomain Discovery
|
|
813
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
814
|
+
const tlsCtSubdomains = {
|
|
815
|
+
name: "tls_ct_subdomains",
|
|
816
|
+
description: "Certificate Transparency subdomain discovery via crt.sh. Queries CT logs for all certificates issued for a domain, extracts and deduplicates subdomain names, and highlights internal/interesting hostnames (staging, jenkins, vault, etc.).",
|
|
817
|
+
schema: {
|
|
818
|
+
domain: z.string().describe("Domain for CT log subdomain discovery"),
|
|
819
|
+
},
|
|
820
|
+
async execute(args) {
|
|
821
|
+
const domain = args.domain;
|
|
822
|
+
const cacheKey = `tls_ct_subdomains:${domain}`;
|
|
823
|
+
const cached = cache.get(cacheKey);
|
|
824
|
+
if (cached)
|
|
825
|
+
return json(cached);
|
|
826
|
+
await limiter.acquire();
|
|
827
|
+
// Query crt.sh API
|
|
828
|
+
const url = `https://crt.sh/?q=%25.${encodeURIComponent(domain)}&output=json`;
|
|
829
|
+
const response = await fetch(url, {
|
|
830
|
+
headers: { "User-Agent": "fingerprint-mcp/0.1.0" },
|
|
831
|
+
signal: AbortSignal.timeout(30000),
|
|
832
|
+
});
|
|
833
|
+
if (!response.ok) {
|
|
834
|
+
throw new Error(`crt.sh API returned HTTP ${response.status}`);
|
|
835
|
+
}
|
|
836
|
+
const data = await response.json();
|
|
837
|
+
// Extract and deduplicate all subdomains
|
|
838
|
+
const subdomainSet = new Set();
|
|
839
|
+
for (const entry of data) {
|
|
840
|
+
// name_value can contain multiple domains separated by newlines
|
|
841
|
+
const names = (entry.name_value ?? "").split("\n");
|
|
842
|
+
for (const name of names) {
|
|
843
|
+
const cleaned = name.trim().toLowerCase();
|
|
844
|
+
if (cleaned && cleaned.endsWith(`.${domain.toLowerCase()}`)) {
|
|
845
|
+
// Remove leading wildcard if present
|
|
846
|
+
const normalized = cleaned.startsWith("*.") ? cleaned.slice(2) : cleaned;
|
|
847
|
+
subdomainSet.add(normalized);
|
|
848
|
+
}
|
|
849
|
+
// Also include exact domain match
|
|
850
|
+
if (cleaned === domain.toLowerCase()) {
|
|
851
|
+
subdomainSet.add(cleaned);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Also check common_name
|
|
855
|
+
const cn = (entry.common_name ?? "").trim().toLowerCase();
|
|
856
|
+
if (cn && (cn.endsWith(`.${domain.toLowerCase()}`) || cn === domain.toLowerCase())) {
|
|
857
|
+
const normalized = cn.startsWith("*.") ? cn.slice(2) : cn;
|
|
858
|
+
subdomainSet.add(normalized);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const subdomains = Array.from(subdomainSet).sort();
|
|
862
|
+
// Categorize interesting subdomains
|
|
863
|
+
const internalKeywords = {
|
|
864
|
+
staging: "Staging environment",
|
|
865
|
+
stage: "Staging environment",
|
|
866
|
+
dev: "Development environment",
|
|
867
|
+
test: "Test environment",
|
|
868
|
+
uat: "User acceptance testing",
|
|
869
|
+
qa: "Quality assurance",
|
|
870
|
+
sandbox: "Sandbox environment",
|
|
871
|
+
internal: "Internal service",
|
|
872
|
+
admin: "Admin panel",
|
|
873
|
+
jenkins: "CI/CD (Jenkins)",
|
|
874
|
+
gitlab: "GitLab instance",
|
|
875
|
+
github: "GitHub Enterprise",
|
|
876
|
+
jira: "Issue tracker (Jira)",
|
|
877
|
+
confluence: "Wiki/Docs (Confluence)",
|
|
878
|
+
vault: "Secret management (Vault)",
|
|
879
|
+
grafana: "Monitoring (Grafana)",
|
|
880
|
+
kibana: "Log analysis (Kibana)",
|
|
881
|
+
prometheus: "Monitoring (Prometheus)",
|
|
882
|
+
elastic: "Elasticsearch",
|
|
883
|
+
api: "API endpoint",
|
|
884
|
+
vpn: "VPN gateway",
|
|
885
|
+
mail: "Mail server",
|
|
886
|
+
smtp: "SMTP server",
|
|
887
|
+
imap: "IMAP server",
|
|
888
|
+
webmail: "Webmail interface",
|
|
889
|
+
ftp: "FTP server",
|
|
890
|
+
sftp: "SFTP server",
|
|
891
|
+
ssh: "SSH access",
|
|
892
|
+
rdp: "Remote desktop",
|
|
893
|
+
db: "Database server",
|
|
894
|
+
mysql: "MySQL database",
|
|
895
|
+
postgres: "PostgreSQL database",
|
|
896
|
+
mongo: "MongoDB database",
|
|
897
|
+
redis: "Redis cache",
|
|
898
|
+
kafka: "Message broker (Kafka)",
|
|
899
|
+
rabbitmq: "Message broker (RabbitMQ)",
|
|
900
|
+
docker: "Docker registry",
|
|
901
|
+
k8s: "Kubernetes",
|
|
902
|
+
kubernetes: "Kubernetes",
|
|
903
|
+
cdn: "CDN endpoint",
|
|
904
|
+
static: "Static assets",
|
|
905
|
+
assets: "Static assets",
|
|
906
|
+
s3: "S3/Object storage",
|
|
907
|
+
minio: "MinIO storage",
|
|
908
|
+
sentry: "Error tracking (Sentry)",
|
|
909
|
+
sonar: "Code quality (SonarQube)",
|
|
910
|
+
nexus: "Artifact repository",
|
|
911
|
+
artifactory: "Artifact repository",
|
|
912
|
+
backup: "Backup service",
|
|
913
|
+
};
|
|
914
|
+
const interestingSubdomains = [];
|
|
915
|
+
for (const sub of subdomains) {
|
|
916
|
+
const parts = sub.split(".");
|
|
917
|
+
for (const part of parts) {
|
|
918
|
+
for (const [keyword, classification] of Object.entries(internalKeywords)) {
|
|
919
|
+
if (part.includes(keyword)) {
|
|
920
|
+
interestingSubdomains.push({ subdomain: sub, classification });
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// Only match the first keyword per subdomain
|
|
925
|
+
if (interestingSubdomains.length > 0 && interestingSubdomains.at(-1)?.subdomain === sub) {
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Wildcard domains found in raw data
|
|
931
|
+
const wildcards = new Set();
|
|
932
|
+
for (const entry of data) {
|
|
933
|
+
const names = (entry.name_value ?? "").split("\n");
|
|
934
|
+
for (const name of names) {
|
|
935
|
+
const cleaned = name.trim().toLowerCase();
|
|
936
|
+
if (cleaned.startsWith("*.")) {
|
|
937
|
+
wildcards.add(cleaned);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// Unique issuers
|
|
942
|
+
const issuers = new Set();
|
|
943
|
+
for (const entry of data) {
|
|
944
|
+
if (entry.issuer_name)
|
|
945
|
+
issuers.add(entry.issuer_name);
|
|
946
|
+
}
|
|
947
|
+
const result = {
|
|
948
|
+
domain,
|
|
949
|
+
ctLogs: {
|
|
950
|
+
source: "crt.sh (Certificate Transparency)",
|
|
951
|
+
totalCertificatesFound: data.length,
|
|
952
|
+
subdomains: {
|
|
953
|
+
total: subdomains.length,
|
|
954
|
+
list: subdomains,
|
|
955
|
+
},
|
|
956
|
+
interestingSubdomains,
|
|
957
|
+
wildcardCertificates: Array.from(wildcards),
|
|
958
|
+
certificateIssuers: Array.from(issuers).slice(0, 20),
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
cache.set(cacheKey, result);
|
|
962
|
+
return json(result);
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
966
|
+
// Tool 7: tls_ciphers — Cipher Suite Enumeration
|
|
967
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
968
|
+
// Cipher suites to test, grouped by security level
|
|
969
|
+
const CIPHER_SUITES_TO_TEST = [
|
|
970
|
+
// Critical — should never be accepted
|
|
971
|
+
{ name: "NULL", openssl: "NULL", security: "critical" },
|
|
972
|
+
{ name: "EXPORT", openssl: "EXPORT", security: "critical" },
|
|
973
|
+
{ name: "eNULL", openssl: "eNULL", security: "critical" },
|
|
974
|
+
{ name: "aNULL", openssl: "aNULL", security: "critical" },
|
|
975
|
+
// Weak — should be disabled
|
|
976
|
+
{ name: "RC4", openssl: "RC4", security: "weak" },
|
|
977
|
+
{ name: "DES-CBC3-SHA", openssl: "DES-CBC3-SHA", security: "weak" },
|
|
978
|
+
{ name: "DES-CBC-SHA", openssl: "DES-CBC-SHA", security: "weak" },
|
|
979
|
+
{ name: "SEED-SHA", openssl: "SEED-SHA", security: "weak" },
|
|
980
|
+
{ name: "IDEA-CBC-SHA", openssl: "IDEA-CBC-SHA", security: "weak" },
|
|
981
|
+
// Deprecated — should be migrated away from
|
|
982
|
+
{ name: "AES128-SHA", openssl: "AES128-SHA", security: "deprecated" },
|
|
983
|
+
{ name: "AES256-SHA", openssl: "AES256-SHA", security: "deprecated" },
|
|
984
|
+
{ name: "AES128-SHA256", openssl: "AES128-SHA256", security: "deprecated" },
|
|
985
|
+
{ name: "AES256-SHA256", openssl: "AES256-SHA256", security: "deprecated" },
|
|
986
|
+
// Acceptable — forward-secrecy with CBC
|
|
987
|
+
{ name: "ECDHE-RSA-AES128-SHA", openssl: "ECDHE-RSA-AES128-SHA", security: "acceptable" },
|
|
988
|
+
{ name: "ECDHE-RSA-AES256-SHA", openssl: "ECDHE-RSA-AES256-SHA", security: "acceptable" },
|
|
989
|
+
{ name: "ECDHE-RSA-AES128-SHA256", openssl: "ECDHE-RSA-AES128-SHA256", security: "acceptable" },
|
|
990
|
+
{ name: "ECDHE-RSA-AES256-SHA384", openssl: "ECDHE-RSA-AES256-SHA384", security: "acceptable" },
|
|
991
|
+
{ name: "DHE-RSA-AES128-SHA", openssl: "DHE-RSA-AES128-SHA", security: "acceptable" },
|
|
992
|
+
{ name: "DHE-RSA-AES256-SHA", openssl: "DHE-RSA-AES256-SHA", security: "acceptable" },
|
|
993
|
+
{ name: "DHE-RSA-AES128-SHA256", openssl: "DHE-RSA-AES128-SHA256", security: "acceptable" },
|
|
994
|
+
{ name: "DHE-RSA-AES256-SHA256", openssl: "DHE-RSA-AES256-SHA256", security: "acceptable" },
|
|
995
|
+
// Strong — forward-secrecy with AEAD
|
|
996
|
+
{ name: "ECDHE-RSA-AES128-GCM-SHA256", openssl: "ECDHE-RSA-AES128-GCM-SHA256", security: "strong" },
|
|
997
|
+
{ name: "ECDHE-RSA-AES256-GCM-SHA384", openssl: "ECDHE-RSA-AES256-GCM-SHA384", security: "strong" },
|
|
998
|
+
{ name: "ECDHE-ECDSA-AES128-GCM-SHA256", openssl: "ECDHE-ECDSA-AES128-GCM-SHA256", security: "strong" },
|
|
999
|
+
{ name: "ECDHE-ECDSA-AES256-GCM-SHA384", openssl: "ECDHE-ECDSA-AES256-GCM-SHA384", security: "strong" },
|
|
1000
|
+
{ name: "ECDHE-RSA-CHACHA20-POLY1305", openssl: "ECDHE-RSA-CHACHA20-POLY1305", security: "strong" },
|
|
1001
|
+
{ name: "ECDHE-ECDSA-CHACHA20-POLY1305", openssl: "ECDHE-ECDSA-CHACHA20-POLY1305", security: "strong" },
|
|
1002
|
+
{ name: "DHE-RSA-AES128-GCM-SHA256", openssl: "DHE-RSA-AES128-GCM-SHA256", security: "strong" },
|
|
1003
|
+
{ name: "DHE-RSA-AES256-GCM-SHA384", openssl: "DHE-RSA-AES256-GCM-SHA384", security: "strong" },
|
|
1004
|
+
{ name: "DHE-RSA-CHACHA20-POLY1305", openssl: "DHE-RSA-CHACHA20-POLY1305", security: "strong" },
|
|
1005
|
+
// Modern — TLS 1.3 cipher suites
|
|
1006
|
+
{ name: "TLS_AES_128_GCM_SHA256", openssl: "TLS_AES_128_GCM_SHA256", security: "modern", tlsVersion: "tls13" },
|
|
1007
|
+
{ name: "TLS_AES_256_GCM_SHA384", openssl: "TLS_AES_256_GCM_SHA384", security: "modern", tlsVersion: "tls13" },
|
|
1008
|
+
{
|
|
1009
|
+
name: "TLS_CHACHA20_POLY1305_SHA256",
|
|
1010
|
+
openssl: "TLS_CHACHA20_POLY1305_SHA256",
|
|
1011
|
+
security: "modern",
|
|
1012
|
+
tlsVersion: "tls13",
|
|
1013
|
+
},
|
|
1014
|
+
];
|
|
1015
|
+
async function testCipher(host, port, cipher) {
|
|
1016
|
+
try {
|
|
1017
|
+
const opts = {};
|
|
1018
|
+
if (cipher.tlsVersion === "tls13") {
|
|
1019
|
+
opts.minVersion = "TLSv1.3";
|
|
1020
|
+
opts.maxVersion = "TLSv1.3";
|
|
1021
|
+
opts.ciphers = undefined; // TLS 1.3 ciphers are controlled differently
|
|
1022
|
+
// For TLS 1.3, we use cipherSuites instead of ciphers
|
|
1023
|
+
opts.cipherSuites = cipher.openssl;
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
opts.ciphers = cipher.openssl;
|
|
1027
|
+
opts.maxVersion = "TLSv1.2";
|
|
1028
|
+
}
|
|
1029
|
+
const { socket } = await tlsConnect(host, port, opts);
|
|
1030
|
+
const negotiated = socket.getCipher();
|
|
1031
|
+
safeClose(socket);
|
|
1032
|
+
return {
|
|
1033
|
+
name: cipher.name,
|
|
1034
|
+
accepted: true,
|
|
1035
|
+
security: cipher.security,
|
|
1036
|
+
negotiatedCipher: negotiated?.name,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
catch {
|
|
1040
|
+
return {
|
|
1041
|
+
name: cipher.name,
|
|
1042
|
+
accepted: false,
|
|
1043
|
+
security: cipher.security,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const tlsCiphers = {
|
|
1048
|
+
name: "tls_ciphers",
|
|
1049
|
+
description: "Enumerate accepted TLS cipher suites by testing each cipher individually. Identifies weak ciphers (RC4, DES, NULL, EXPORT), acceptable ciphers, strong ciphers (AES-GCM, ChaCha20), and TLS 1.3 suites. Provides a security grade.",
|
|
1050
|
+
schema: {
|
|
1051
|
+
host: z.string().describe("Target hostname or IP"),
|
|
1052
|
+
port: z.number().optional().default(443).describe("TLS port"),
|
|
1053
|
+
},
|
|
1054
|
+
async execute(args) {
|
|
1055
|
+
const host = args.host;
|
|
1056
|
+
const port = args.port ?? 443;
|
|
1057
|
+
const cacheKey = `tls_ciphers:${host}:${port}`;
|
|
1058
|
+
const cached = cache.get(cacheKey);
|
|
1059
|
+
if (cached)
|
|
1060
|
+
return json(cached);
|
|
1061
|
+
await limiter.acquire();
|
|
1062
|
+
// Test all cipher suites
|
|
1063
|
+
const results = [];
|
|
1064
|
+
// Test in small batches to avoid overwhelming the target
|
|
1065
|
+
for (const cipher of CIPHER_SUITES_TO_TEST) {
|
|
1066
|
+
await limiter.acquire();
|
|
1067
|
+
const result = await testCipher(host, port, cipher);
|
|
1068
|
+
results.push(result);
|
|
1069
|
+
}
|
|
1070
|
+
// Categorize results
|
|
1071
|
+
const accepted = results.filter((r) => r.accepted);
|
|
1072
|
+
const rejected = results.filter((r) => !r.accepted);
|
|
1073
|
+
const bySecurity = {
|
|
1074
|
+
critical: [],
|
|
1075
|
+
weak: [],
|
|
1076
|
+
deprecated: [],
|
|
1077
|
+
acceptable: [],
|
|
1078
|
+
strong: [],
|
|
1079
|
+
modern: [],
|
|
1080
|
+
};
|
|
1081
|
+
for (const r of accepted) {
|
|
1082
|
+
bySecurity[r.security]?.push(r.name);
|
|
1083
|
+
}
|
|
1084
|
+
// Calculate security grade
|
|
1085
|
+
let grade;
|
|
1086
|
+
if (bySecurity.critical.length > 0) {
|
|
1087
|
+
grade = "F";
|
|
1088
|
+
}
|
|
1089
|
+
else if (bySecurity.weak.length > 0) {
|
|
1090
|
+
grade = "C";
|
|
1091
|
+
}
|
|
1092
|
+
else if (bySecurity.deprecated.length > 0 && bySecurity.strong.length === 0) {
|
|
1093
|
+
grade = "D";
|
|
1094
|
+
}
|
|
1095
|
+
else if (bySecurity.deprecated.length > 0) {
|
|
1096
|
+
grade = "B-";
|
|
1097
|
+
}
|
|
1098
|
+
else if (bySecurity.strong.length > 0 || bySecurity.modern.length > 0) {
|
|
1099
|
+
if (bySecurity.acceptable.length === 0) {
|
|
1100
|
+
grade = "A+";
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
grade = "A";
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
else if (bySecurity.acceptable.length > 0) {
|
|
1107
|
+
grade = "B";
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
grade = "N/A";
|
|
1111
|
+
}
|
|
1112
|
+
const vulnerabilities = [];
|
|
1113
|
+
if (bySecurity.critical.length > 0) {
|
|
1114
|
+
vulnerabilities.push(`CRITICAL: Accepts NULL/EXPORT ciphers: ${bySecurity.critical.join(", ")}`);
|
|
1115
|
+
}
|
|
1116
|
+
if (bySecurity.weak.length > 0) {
|
|
1117
|
+
vulnerabilities.push(`WEAK: Accepts insecure ciphers: ${bySecurity.weak.join(", ")}`);
|
|
1118
|
+
}
|
|
1119
|
+
if (accepted.length === 0) {
|
|
1120
|
+
vulnerabilities.push("No cipher suites could be negotiated — server may be unreachable or using unusual configuration");
|
|
1121
|
+
}
|
|
1122
|
+
const result = {
|
|
1123
|
+
host,
|
|
1124
|
+
port,
|
|
1125
|
+
cipherEnumeration: {
|
|
1126
|
+
totalTested: CIPHER_SUITES_TO_TEST.length,
|
|
1127
|
+
totalAccepted: accepted.length,
|
|
1128
|
+
totalRejected: rejected.length,
|
|
1129
|
+
grade,
|
|
1130
|
+
accepted: accepted.map((r) => ({
|
|
1131
|
+
name: r.name,
|
|
1132
|
+
security: r.security,
|
|
1133
|
+
negotiated: r.negotiatedCipher,
|
|
1134
|
+
})),
|
|
1135
|
+
bySecurity: {
|
|
1136
|
+
critical: bySecurity.critical,
|
|
1137
|
+
weak: bySecurity.weak,
|
|
1138
|
+
deprecated: bySecurity.deprecated,
|
|
1139
|
+
acceptable: bySecurity.acceptable,
|
|
1140
|
+
strong: bySecurity.strong,
|
|
1141
|
+
modern: bySecurity.modern,
|
|
1142
|
+
},
|
|
1143
|
+
vulnerabilities,
|
|
1144
|
+
recommendations: grade === "A+" || grade === "A"
|
|
1145
|
+
? ["Cipher configuration looks good"]
|
|
1146
|
+
: [
|
|
1147
|
+
bySecurity.critical.length > 0
|
|
1148
|
+
? "URGENT: Disable NULL and EXPORT cipher suites immediately"
|
|
1149
|
+
: null,
|
|
1150
|
+
bySecurity.weak.length > 0 ? "Disable RC4, DES, and 3DES cipher suites" : null,
|
|
1151
|
+
bySecurity.deprecated.length > 0
|
|
1152
|
+
? "Migrate from non-FS cipher suites to ECDHE/DHE variants"
|
|
1153
|
+
: null,
|
|
1154
|
+
bySecurity.modern.length === 0 ? "Enable TLS 1.3 cipher suites for best performance" : null,
|
|
1155
|
+
].filter(Boolean),
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
cache.set(cacheKey, result);
|
|
1159
|
+
return json(result);
|
|
1160
|
+
},
|
|
1161
|
+
};
|
|
1162
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
1163
|
+
// Tool 8: tls_sni — SNI Probing
|
|
1164
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
1165
|
+
async function sniProbe(host, port, sniValue, label) {
|
|
1166
|
+
try {
|
|
1167
|
+
const opts = {};
|
|
1168
|
+
if (sniValue === undefined) {
|
|
1169
|
+
// No SNI — remove servername
|
|
1170
|
+
opts.servername = "";
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
opts.servername = sniValue;
|
|
1174
|
+
}
|
|
1175
|
+
const { socket, cert } = await tlsConnect(host, port, opts);
|
|
1176
|
+
try {
|
|
1177
|
+
const sans = extractSANs(cert);
|
|
1178
|
+
const subject = parseDN(cert.subject);
|
|
1179
|
+
const issuer = parseDN(cert.issuer);
|
|
1180
|
+
return {
|
|
1181
|
+
label,
|
|
1182
|
+
sni: sniValue ?? null,
|
|
1183
|
+
success: true,
|
|
1184
|
+
cert: {
|
|
1185
|
+
cn: subject.CN ?? "N/A",
|
|
1186
|
+
san: sans.dns,
|
|
1187
|
+
issuer: issuer.CN ?? issuer.O ?? "N/A",
|
|
1188
|
+
fingerprint256: cert.fingerprint256 ?? sha256Fingerprint(cert.raw),
|
|
1189
|
+
},
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
finally {
|
|
1193
|
+
safeClose(socket);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
catch (err) {
|
|
1197
|
+
return {
|
|
1198
|
+
label,
|
|
1199
|
+
sni: sniValue ?? null,
|
|
1200
|
+
success: false,
|
|
1201
|
+
error: err.message,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const tlsSni = {
|
|
1206
|
+
name: "tls_sni",
|
|
1207
|
+
description: "SNI (Server Name Indication) probing. Connects with various SNI values and compares returned certificates. Detects reverse proxies with multiple backends, reveals default/backend certificates, and analyzes wildcard scope.",
|
|
1208
|
+
schema: {
|
|
1209
|
+
host: z.string().describe("Target hostname or IP"),
|
|
1210
|
+
port: z.number().optional().default(443).describe("TLS port"),
|
|
1211
|
+
},
|
|
1212
|
+
async execute(args) {
|
|
1213
|
+
const host = args.host;
|
|
1214
|
+
const port = args.port ?? 443;
|
|
1215
|
+
const cacheKey = `tls_sni:${host}:${port}`;
|
|
1216
|
+
const cached = cache.get(cacheKey);
|
|
1217
|
+
if (cached)
|
|
1218
|
+
return json(cached);
|
|
1219
|
+
await limiter.acquire();
|
|
1220
|
+
// Define SNI values to test
|
|
1221
|
+
const sniTests = [
|
|
1222
|
+
{ sni: host, label: "target_domain" },
|
|
1223
|
+
{ sni: undefined, label: "no_sni" },
|
|
1224
|
+
{ sni: "", label: "empty_sni" },
|
|
1225
|
+
{ sni: "localhost", label: "localhost" },
|
|
1226
|
+
{ sni: "internal", label: "internal" },
|
|
1227
|
+
{ sni: `nonexistent-${Date.now()}.example.com`, label: "random_domain" },
|
|
1228
|
+
];
|
|
1229
|
+
// If host looks like an IP, also test it as SNI
|
|
1230
|
+
const isIP = net.isIP(host);
|
|
1231
|
+
if (isIP) {
|
|
1232
|
+
sniTests.push({ sni: host, label: "ip_as_sni" });
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
// Try the IP address as SNI too
|
|
1236
|
+
try {
|
|
1237
|
+
const dns = await import("node:dns/promises");
|
|
1238
|
+
const ips = await dns.resolve4(host);
|
|
1239
|
+
if (ips.length > 0) {
|
|
1240
|
+
sniTests.push({ sni: ips[0], label: "resolved_ip_as_sni" });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
// ignore DNS failure
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
// Run all SNI probes
|
|
1248
|
+
const probeResults = [];
|
|
1249
|
+
for (const test of sniTests) {
|
|
1250
|
+
await limiter.acquire();
|
|
1251
|
+
const result = await sniProbe(host, port, test.sni, test.label);
|
|
1252
|
+
probeResults.push(result);
|
|
1253
|
+
}
|
|
1254
|
+
// Analyze results
|
|
1255
|
+
const successfulProbes = probeResults.filter((p) => p.success);
|
|
1256
|
+
const uniqueFingerprints = new Set(successfulProbes
|
|
1257
|
+
.filter((p) => p.cert)
|
|
1258
|
+
.map((p) => p.cert.fingerprint256));
|
|
1259
|
+
// Compare target domain cert with no-SNI cert
|
|
1260
|
+
const targetProbe = probeResults.find((p) => p.label === "target_domain" && p.success);
|
|
1261
|
+
const noSniProbe = probeResults.find((p) => (p.label === "no_sni" || p.label === "empty_sni") && p.success);
|
|
1262
|
+
const analysis = [];
|
|
1263
|
+
if (uniqueFingerprints.size > 1) {
|
|
1264
|
+
analysis.push(`Multiple certificates detected (${uniqueFingerprints.size} unique) — indicates reverse proxy or multi-tenant hosting`);
|
|
1265
|
+
}
|
|
1266
|
+
else if (uniqueFingerprints.size === 1) {
|
|
1267
|
+
analysis.push("Same certificate returned for all SNI values — single-site or wildcard configuration");
|
|
1268
|
+
}
|
|
1269
|
+
if (targetProbe?.cert && noSniProbe?.cert) {
|
|
1270
|
+
if (targetProbe.cert.fingerprint256 !== noSniProbe.cert.fingerprint256) {
|
|
1271
|
+
analysis.push(`Different cert on no-SNI (CN=${noSniProbe.cert.cn}) vs target (CN=${targetProbe.cert.cn}) — reverse proxy with SNI routing`);
|
|
1272
|
+
analysis.push(`Default certificate (no-SNI) reveals: CN=${noSniProbe.cert.cn}, Issuer=${noSniProbe.cert.issuer}`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
// Wildcard analysis
|
|
1276
|
+
const wildcardCerts = successfulProbes.filter((p) => p.cert && p.cert.san.some((s) => s.startsWith("*.")));
|
|
1277
|
+
if (wildcardCerts.length > 0) {
|
|
1278
|
+
const wildcardSans = new Set(wildcardCerts.flatMap((p) => p.cert.san.filter((s) => s.startsWith("*."))));
|
|
1279
|
+
analysis.push(`Wildcard certificate scope: ${Array.from(wildcardSans).join(", ")}`);
|
|
1280
|
+
}
|
|
1281
|
+
// Random domain test
|
|
1282
|
+
const randomProbe = probeResults.find((p) => p.label === "random_domain");
|
|
1283
|
+
if (randomProbe?.success && targetProbe?.success) {
|
|
1284
|
+
if (randomProbe.cert?.fingerprint256 === targetProbe.cert?.fingerprint256) {
|
|
1285
|
+
analysis.push("Server returns same cert for unknown domains — catch-all/default vhost configuration");
|
|
1286
|
+
}
|
|
1287
|
+
else if (randomProbe.success) {
|
|
1288
|
+
analysis.push(`Server returns different cert for unknown domain (CN=${randomProbe.cert?.cn}) — may reveal default backend`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
const result = {
|
|
1292
|
+
host,
|
|
1293
|
+
port,
|
|
1294
|
+
sniAnalysis: {
|
|
1295
|
+
probeCount: probeResults.length,
|
|
1296
|
+
successfulProbes: successfulProbes.length,
|
|
1297
|
+
uniqueCertificates: uniqueFingerprints.size,
|
|
1298
|
+
probes: probeResults,
|
|
1299
|
+
analysis,
|
|
1300
|
+
findings: {
|
|
1301
|
+
multipleBackends: uniqueFingerprints.size > 1,
|
|
1302
|
+
defaultCertificate: noSniProbe?.cert
|
|
1303
|
+
? {
|
|
1304
|
+
cn: noSniProbe.cert.cn,
|
|
1305
|
+
issuer: noSniProbe.cert.issuer,
|
|
1306
|
+
san: noSniProbe.cert.san,
|
|
1307
|
+
}
|
|
1308
|
+
: null,
|
|
1309
|
+
wildcardScope: wildcardCerts.length > 0
|
|
1310
|
+
? Array.from(new Set(wildcardCerts.flatMap((p) => p.cert.san.filter((s) => s.startsWith("*.")))))
|
|
1311
|
+
: [],
|
|
1312
|
+
},
|
|
1313
|
+
},
|
|
1314
|
+
};
|
|
1315
|
+
cache.set(cacheKey, result);
|
|
1316
|
+
return json(result);
|
|
1317
|
+
},
|
|
1318
|
+
};
|
|
1319
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
1320
|
+
// Export
|
|
1321
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
1322
|
+
export const tlsTools = [
|
|
1323
|
+
tlsProbe,
|
|
1324
|
+
tlsJarm,
|
|
1325
|
+
tlsCert,
|
|
1326
|
+
tlsJa4x,
|
|
1327
|
+
tlsCertCrossRef,
|
|
1328
|
+
tlsCtSubdomains,
|
|
1329
|
+
tlsCiphers,
|
|
1330
|
+
tlsSni,
|
|
1331
|
+
];
|
|
1332
|
+
//# sourceMappingURL=index.js.map
|