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,676 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RateLimiter } from "../utils/rate-limiter.js";
|
|
3
|
+
import { TTLCache } from "../utils/cache.js";
|
|
4
|
+
import { json } from "../types/index.js";
|
|
5
|
+
import * as net from "node:net";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
const limiter = new RateLimiter(300);
|
|
8
|
+
const cache = new TTLCache(600_000); // 10min cache
|
|
9
|
+
// ─── Constants ───
|
|
10
|
+
const CONNECT_TIMEOUT = 10_000;
|
|
11
|
+
const BANNER_READ_TIMEOUT = 5_000;
|
|
12
|
+
const KEXINIT_READ_TIMEOUT = 8_000;
|
|
13
|
+
const SSH_MSG_KEXINIT = 20;
|
|
14
|
+
const CLIENT_BANNER = "SSH-2.0-fingerprint-mcp\r\n";
|
|
15
|
+
// ─── Deprecated / Weak Algorithm Sets ───
|
|
16
|
+
const WEAK_KEX = new Set([
|
|
17
|
+
"diffie-hellman-group1-sha1",
|
|
18
|
+
"diffie-hellman-group-exchange-sha1",
|
|
19
|
+
"diffie-hellman-group14-sha1",
|
|
20
|
+
"ecdh-sha2-nistp256", // not weak per se, but NIST concerns
|
|
21
|
+
]);
|
|
22
|
+
const DEPRECATED_KEX = new Set([
|
|
23
|
+
"diffie-hellman-group1-sha1",
|
|
24
|
+
"diffie-hellman-group-exchange-sha1",
|
|
25
|
+
]);
|
|
26
|
+
const WEAK_CIPHERS = new Set([
|
|
27
|
+
"3des-cbc",
|
|
28
|
+
"arcfour",
|
|
29
|
+
"arcfour128",
|
|
30
|
+
"arcfour256",
|
|
31
|
+
"blowfish-cbc",
|
|
32
|
+
"cast128-cbc",
|
|
33
|
+
"aes128-cbc",
|
|
34
|
+
"aes192-cbc",
|
|
35
|
+
"aes256-cbc",
|
|
36
|
+
"rijndael-cbc@lysator.liu.se",
|
|
37
|
+
]);
|
|
38
|
+
const DEPRECATED_CIPHERS = new Set([
|
|
39
|
+
"3des-cbc",
|
|
40
|
+
"arcfour",
|
|
41
|
+
"arcfour128",
|
|
42
|
+
"arcfour256",
|
|
43
|
+
"blowfish-cbc",
|
|
44
|
+
"cast128-cbc",
|
|
45
|
+
]);
|
|
46
|
+
const WEAK_MACS = new Set([
|
|
47
|
+
"hmac-sha1",
|
|
48
|
+
"hmac-sha1-96",
|
|
49
|
+
"hmac-md5",
|
|
50
|
+
"hmac-md5-96",
|
|
51
|
+
"umac-64@openssh.com",
|
|
52
|
+
]);
|
|
53
|
+
const DEPRECATED_MACS = new Set([
|
|
54
|
+
"hmac-md5",
|
|
55
|
+
"hmac-md5-96",
|
|
56
|
+
]);
|
|
57
|
+
const WEAK_HOST_KEY_TYPES = new Set([
|
|
58
|
+
"ssh-rsa",
|
|
59
|
+
"ssh-dss",
|
|
60
|
+
]);
|
|
61
|
+
// ─── OpenSSH Version to Algorithm Mapping (for version pinpointing) ───
|
|
62
|
+
const OPENSSH_VERSION_HINTS = [
|
|
63
|
+
{
|
|
64
|
+
version: "9.x+",
|
|
65
|
+
indicators: ["sntrup761x25519-sha512@openssh.com"],
|
|
66
|
+
missing: ["ssh-rsa"],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
version: "8.5+",
|
|
70
|
+
indicators: ["sk-ssh-ed25519@openssh.com"],
|
|
71
|
+
missing: [],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
version: "8.x",
|
|
75
|
+
indicators: ["curve25519-sha256", "chacha20-poly1305@openssh.com"],
|
|
76
|
+
missing: ["diffie-hellman-group1-sha1"],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
version: "7.x",
|
|
80
|
+
indicators: ["curve25519-sha256@libssh.org", "diffie-hellman-group14-sha256"],
|
|
81
|
+
missing: [],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
version: "6.x",
|
|
85
|
+
indicators: ["ecdh-sha2-nistp256"],
|
|
86
|
+
missing: ["curve25519-sha256"],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
// ─── Dropbear Algorithm Signatures ───
|
|
90
|
+
const DROPBEAR_INDICATORS = [
|
|
91
|
+
"diffie-hellman-group14-sha256",
|
|
92
|
+
"ecdh-sha2-nistp521",
|
|
93
|
+
"ecdh-sha2-nistp384",
|
|
94
|
+
];
|
|
95
|
+
const DROPBEAR_MISSING = ["curve25519-sha256@libssh.org"];
|
|
96
|
+
function parseSSHBanner(banner) {
|
|
97
|
+
const raw = banner.trim();
|
|
98
|
+
const result = {
|
|
99
|
+
raw,
|
|
100
|
+
protocolVersion: "",
|
|
101
|
+
softwareName: "",
|
|
102
|
+
softwareVersion: "",
|
|
103
|
+
};
|
|
104
|
+
// SSH-<protoversion>-<softwareversion> <comments>
|
|
105
|
+
const match = raw.match(/^SSH-([\d.]+)-(\S+?)(?:\s+(.+))?$/);
|
|
106
|
+
if (!match) {
|
|
107
|
+
result.protocolVersion = "unknown";
|
|
108
|
+
result.softwareName = raw;
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
result.protocolVersion = match[1];
|
|
112
|
+
const softwarePart = match[2];
|
|
113
|
+
result.comments = match[3];
|
|
114
|
+
// Parse software name and version: "OpenSSH_9.7", "dropbear_2022.83"
|
|
115
|
+
const swMatch = softwarePart.match(/^(.+?)[-_]([\d][\d._p]+.*)$/);
|
|
116
|
+
if (swMatch) {
|
|
117
|
+
result.softwareName = swMatch[1];
|
|
118
|
+
result.softwareVersion = swMatch[2];
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
result.softwareName = softwarePart;
|
|
122
|
+
}
|
|
123
|
+
// OS detection from comments or software string
|
|
124
|
+
if (result.comments) {
|
|
125
|
+
const osPatterns = [
|
|
126
|
+
{ pattern: /Ubuntu/i, os: "Ubuntu Linux" },
|
|
127
|
+
{ pattern: /Debian/i, os: "Debian Linux" },
|
|
128
|
+
{ pattern: /FreeBSD/i, os: "FreeBSD" },
|
|
129
|
+
{ pattern: /CentOS/i, os: "CentOS Linux" },
|
|
130
|
+
{ pattern: /Red Hat/i, os: "Red Hat Enterprise Linux" },
|
|
131
|
+
{ pattern: /Fedora/i, os: "Fedora Linux" },
|
|
132
|
+
{ pattern: /SUSE/i, os: "SUSE Linux" },
|
|
133
|
+
{ pattern: /Raspbian/i, os: "Raspbian (Raspberry Pi)" },
|
|
134
|
+
{ pattern: /Arch/i, os: "Arch Linux" },
|
|
135
|
+
{ pattern: /Gentoo/i, os: "Gentoo Linux" },
|
|
136
|
+
{ pattern: /Alpine/i, os: "Alpine Linux" },
|
|
137
|
+
];
|
|
138
|
+
for (const { pattern, os } of osPatterns) {
|
|
139
|
+
if (pattern.test(result.comments)) {
|
|
140
|
+
result.os = os;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
function parseKexInit(data) {
|
|
148
|
+
try {
|
|
149
|
+
// SSH packet: 4 bytes length + 1 byte padding length + payload
|
|
150
|
+
if (data.length < 6)
|
|
151
|
+
return null;
|
|
152
|
+
let offset = 0;
|
|
153
|
+
// Read packet length
|
|
154
|
+
const packetLength = data.readUInt32BE(offset);
|
|
155
|
+
offset += 4;
|
|
156
|
+
// Read padding length
|
|
157
|
+
const paddingLength = data.readUInt8(offset);
|
|
158
|
+
offset += 1;
|
|
159
|
+
// Read message type
|
|
160
|
+
const msgType = data.readUInt8(offset);
|
|
161
|
+
offset += 1;
|
|
162
|
+
if (msgType !== SSH_MSG_KEXINIT) {
|
|
163
|
+
// Maybe the data doesn't start with the length prefix (some servers)
|
|
164
|
+
// Try interpreting from byte 0 as the message type
|
|
165
|
+
offset = 0;
|
|
166
|
+
const altMsgType = data.readUInt8(offset);
|
|
167
|
+
offset += 1;
|
|
168
|
+
if (altMsgType !== SSH_MSG_KEXINIT)
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// 16 bytes cookie
|
|
172
|
+
if (offset + 16 > data.length)
|
|
173
|
+
return null;
|
|
174
|
+
const cookie = data.subarray(offset, offset + 16).toString("hex");
|
|
175
|
+
offset += 16;
|
|
176
|
+
// Read 10 name-lists
|
|
177
|
+
const nameLists = [];
|
|
178
|
+
for (let i = 0; i < 10; i++) {
|
|
179
|
+
if (offset + 4 > data.length)
|
|
180
|
+
return null;
|
|
181
|
+
const listLength = data.readUInt32BE(offset);
|
|
182
|
+
offset += 4;
|
|
183
|
+
if (offset + listLength > data.length)
|
|
184
|
+
return null;
|
|
185
|
+
const listStr = data.subarray(offset, offset + listLength).toString("utf-8");
|
|
186
|
+
offset += listLength;
|
|
187
|
+
nameLists.push(listStr.length > 0 ? listStr.split(",") : []);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
cookie,
|
|
191
|
+
kexAlgorithms: nameLists[0],
|
|
192
|
+
serverHostKeyAlgorithms: nameLists[1],
|
|
193
|
+
encryptionClientToServer: nameLists[2],
|
|
194
|
+
encryptionServerToClient: nameLists[3],
|
|
195
|
+
macClientToServer: nameLists[4],
|
|
196
|
+
macServerToClient: nameLists[5],
|
|
197
|
+
compressionClientToServer: nameLists[6],
|
|
198
|
+
compressionServerToClient: nameLists[7],
|
|
199
|
+
languagesClientToServer: nameLists[8],
|
|
200
|
+
languagesServerToClient: nameLists[9],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ─── HASSH Fingerprint ───
|
|
208
|
+
function computeHASSH(kex) {
|
|
209
|
+
// HASSH = MD5(kex_algorithms;encryption_algorithms_client_to_server;mac_algorithms_client_to_server;compression_algorithms_client_to_server)
|
|
210
|
+
const hasshRaw = [
|
|
211
|
+
kex.kexAlgorithms.join(","),
|
|
212
|
+
kex.encryptionClientToServer.join(","),
|
|
213
|
+
kex.macClientToServer.join(","),
|
|
214
|
+
kex.compressionClientToServer.join(","),
|
|
215
|
+
].join(";");
|
|
216
|
+
const hassh = createHash("md5").update(hasshRaw).digest("hex");
|
|
217
|
+
return { hassh, hasshRaw };
|
|
218
|
+
}
|
|
219
|
+
function performSSHProbe(host, port) {
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const result = {
|
|
222
|
+
banner: null,
|
|
223
|
+
kexInit: null,
|
|
224
|
+
hassh: null,
|
|
225
|
+
hasshRaw: null,
|
|
226
|
+
};
|
|
227
|
+
let finished = false;
|
|
228
|
+
const finish = (socket) => {
|
|
229
|
+
if (finished)
|
|
230
|
+
return;
|
|
231
|
+
finished = true;
|
|
232
|
+
socket.destroy();
|
|
233
|
+
resolve(result);
|
|
234
|
+
};
|
|
235
|
+
const connectTimer = setTimeout(() => {
|
|
236
|
+
result.error = "connection timeout";
|
|
237
|
+
resolve(result);
|
|
238
|
+
}, CONNECT_TIMEOUT);
|
|
239
|
+
const socket = net.connect({ host, port }, () => {
|
|
240
|
+
clearTimeout(connectTimer);
|
|
241
|
+
let buffer = Buffer.alloc(0);
|
|
242
|
+
let bannerRead = false;
|
|
243
|
+
let bannerStr = "";
|
|
244
|
+
let kexPhaseTimer = null;
|
|
245
|
+
socket.on("data", (chunk) => {
|
|
246
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
247
|
+
// Phase 1: Read banner (text line ending with \n)
|
|
248
|
+
if (!bannerRead) {
|
|
249
|
+
const newlineIdx = buffer.indexOf(0x0a); // \n
|
|
250
|
+
if (newlineIdx !== -1) {
|
|
251
|
+
bannerStr = buffer.subarray(0, newlineIdx).toString("utf-8").replace(/\r$/, "");
|
|
252
|
+
result.banner = parseSSHBanner(bannerStr);
|
|
253
|
+
buffer = buffer.subarray(newlineIdx + 1);
|
|
254
|
+
bannerRead = true;
|
|
255
|
+
// Send our client banner to trigger KEXINIT exchange
|
|
256
|
+
try {
|
|
257
|
+
socket.write(CLIENT_BANNER);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
finish(socket);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Set timeout for KEXINIT phase
|
|
264
|
+
kexPhaseTimer = setTimeout(() => {
|
|
265
|
+
// We got the banner but no KEXINIT — still useful
|
|
266
|
+
finish(socket);
|
|
267
|
+
}, KEXINIT_READ_TIMEOUT);
|
|
268
|
+
}
|
|
269
|
+
else if (buffer.length > 1024) {
|
|
270
|
+
// Banner too long — not a real SSH server
|
|
271
|
+
result.error = "banner exceeded 1024 bytes";
|
|
272
|
+
finish(socket);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// If no newline yet, wait for more data
|
|
276
|
+
if (!bannerRead)
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Phase 2: Read KEXINIT binary packet
|
|
280
|
+
// Need at least 4 bytes for packet length
|
|
281
|
+
if (buffer.length >= 4) {
|
|
282
|
+
const packetLength = buffer.readUInt32BE(0);
|
|
283
|
+
// Sanity check: packet shouldn't be > 64KB for KEXINIT
|
|
284
|
+
if (packetLength > 65536) {
|
|
285
|
+
if (kexPhaseTimer)
|
|
286
|
+
clearTimeout(kexPhaseTimer);
|
|
287
|
+
finish(socket);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Wait for full packet: 4 (length field) + packetLength
|
|
291
|
+
if (buffer.length >= 4 + packetLength) {
|
|
292
|
+
const kexPacket = buffer.subarray(0, 4 + packetLength);
|
|
293
|
+
const parsed = parseKexInit(kexPacket);
|
|
294
|
+
if (parsed) {
|
|
295
|
+
result.kexInit = parsed;
|
|
296
|
+
const { hassh, hasshRaw } = computeHASSH(parsed);
|
|
297
|
+
result.hassh = hassh;
|
|
298
|
+
result.hasshRaw = hasshRaw;
|
|
299
|
+
}
|
|
300
|
+
if (kexPhaseTimer)
|
|
301
|
+
clearTimeout(kexPhaseTimer);
|
|
302
|
+
finish(socket);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
// Timeout for initial banner read
|
|
308
|
+
setTimeout(() => {
|
|
309
|
+
if (!bannerRead) {
|
|
310
|
+
result.error = "banner read timeout";
|
|
311
|
+
finish(socket);
|
|
312
|
+
}
|
|
313
|
+
}, BANNER_READ_TIMEOUT);
|
|
314
|
+
});
|
|
315
|
+
socket.on("error", (err) => {
|
|
316
|
+
clearTimeout(connectTimer);
|
|
317
|
+
result.error = err.message;
|
|
318
|
+
resolve(result);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function detectBannerSpoofing(banner, kex) {
|
|
323
|
+
const bannerSw = banner.softwareName.toLowerCase();
|
|
324
|
+
// Check if banner says OpenSSH but algorithms suggest Dropbear
|
|
325
|
+
if (bannerSw.includes("openssh")) {
|
|
326
|
+
const hasDropbearSign = DROPBEAR_INDICATORS.some((a) => kex.kexAlgorithms.includes(a)) &&
|
|
327
|
+
DROPBEAR_MISSING.every((a) => !kex.kexAlgorithms.includes(a));
|
|
328
|
+
if (hasDropbearSign) {
|
|
329
|
+
return {
|
|
330
|
+
detected: true,
|
|
331
|
+
reason: "Banner claims OpenSSH but algorithm set matches Dropbear profile (missing curve25519-sha256@libssh.org, has dropbear-specific kex)",
|
|
332
|
+
bannerClaims: "OpenSSH",
|
|
333
|
+
algorithmsSuggest: "Dropbear",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Check if banner says Dropbear but algorithms suggest OpenSSH
|
|
338
|
+
if (bannerSw.includes("dropbear")) {
|
|
339
|
+
const hasOpenSSHSign = kex.kexAlgorithms.includes("curve25519-sha256@libssh.org") ||
|
|
340
|
+
kex.kexAlgorithms.includes("sntrup761x25519-sha512@openssh.com");
|
|
341
|
+
if (hasOpenSSHSign) {
|
|
342
|
+
return {
|
|
343
|
+
detected: true,
|
|
344
|
+
reason: "Banner claims Dropbear but algorithm set includes OpenSSH-specific extensions (curve25519-sha256@libssh.org or sntrup761)",
|
|
345
|
+
bannerClaims: "Dropbear",
|
|
346
|
+
algorithmsSuggest: "OpenSSH",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Check for libssh
|
|
351
|
+
if (bannerSw.includes("openssh")) {
|
|
352
|
+
if (kex.encryptionClientToServer.includes("chacha20-poly1305") &&
|
|
353
|
+
!kex.encryptionClientToServer.includes("chacha20-poly1305@openssh.com")) {
|
|
354
|
+
return {
|
|
355
|
+
detected: true,
|
|
356
|
+
reason: "Banner claims OpenSSH but cipher names lack @openssh.com suffix — possible libssh or custom implementation",
|
|
357
|
+
bannerClaims: "OpenSSH",
|
|
358
|
+
algorithmsSuggest: "libssh or custom",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
detected: false,
|
|
364
|
+
bannerClaims: banner.softwareName,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// ─── Version Pinpointing from Algorithms ───
|
|
368
|
+
function pinpointVersion(kex) {
|
|
369
|
+
for (const hint of OPENSSH_VERSION_HINTS) {
|
|
370
|
+
const hasIndicators = hint.indicators.every((a) => kex.kexAlgorithms.includes(a) ||
|
|
371
|
+
kex.encryptionClientToServer.includes(a) ||
|
|
372
|
+
kex.serverHostKeyAlgorithms.includes(a));
|
|
373
|
+
const missingCorrectly = hint.missing.every((a) => !kex.kexAlgorithms.includes(a) &&
|
|
374
|
+
!kex.encryptionClientToServer.includes(a) &&
|
|
375
|
+
!kex.serverHostKeyAlgorithms.includes(a));
|
|
376
|
+
if (hasIndicators && missingCorrectly) {
|
|
377
|
+
return `OpenSSH ${hint.version} (estimated from algorithm set)`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
// ─── Tool 1: ssh_probe ───
|
|
383
|
+
async function sshProbe(args, _ctx) {
|
|
384
|
+
const host = args.host;
|
|
385
|
+
const port = args.port ?? 22;
|
|
386
|
+
const cacheKey = `ssh_probe:${host}:${port}`;
|
|
387
|
+
const cached = cache.get(cacheKey);
|
|
388
|
+
if (cached)
|
|
389
|
+
return json(cached);
|
|
390
|
+
await limiter.acquire();
|
|
391
|
+
const probe = await performSSHProbe(host, port);
|
|
392
|
+
if (probe.error && !probe.banner) {
|
|
393
|
+
const errorResult = {
|
|
394
|
+
host,
|
|
395
|
+
port,
|
|
396
|
+
error: probe.error,
|
|
397
|
+
banner: null,
|
|
398
|
+
kexInit: null,
|
|
399
|
+
hassh: null,
|
|
400
|
+
};
|
|
401
|
+
return json(errorResult);
|
|
402
|
+
}
|
|
403
|
+
const spoofing = probe.banner && probe.kexInit
|
|
404
|
+
? detectBannerSpoofing(probe.banner, probe.kexInit)
|
|
405
|
+
: null;
|
|
406
|
+
const versionEstimate = probe.kexInit ? pinpointVersion(probe.kexInit) : null;
|
|
407
|
+
const result = {
|
|
408
|
+
host,
|
|
409
|
+
port,
|
|
410
|
+
banner: probe.banner,
|
|
411
|
+
kexInit: probe.kexInit
|
|
412
|
+
? {
|
|
413
|
+
cookie: probe.kexInit.cookie,
|
|
414
|
+
kexAlgorithms: probe.kexInit.kexAlgorithms,
|
|
415
|
+
serverHostKeyAlgorithms: probe.kexInit.serverHostKeyAlgorithms,
|
|
416
|
+
encryptionClientToServer: probe.kexInit.encryptionClientToServer,
|
|
417
|
+
encryptionServerToClient: probe.kexInit.encryptionServerToClient,
|
|
418
|
+
macClientToServer: probe.kexInit.macClientToServer,
|
|
419
|
+
macServerToClient: probe.kexInit.macServerToClient,
|
|
420
|
+
compressionClientToServer: probe.kexInit.compressionClientToServer,
|
|
421
|
+
compressionServerToClient: probe.kexInit.compressionServerToClient,
|
|
422
|
+
}
|
|
423
|
+
: null,
|
|
424
|
+
hassh: probe.hassh,
|
|
425
|
+
hasshRaw: probe.hasshRaw,
|
|
426
|
+
bannerSpoofing: spoofing,
|
|
427
|
+
versionEstimate,
|
|
428
|
+
};
|
|
429
|
+
cache.set(cacheKey, result);
|
|
430
|
+
return json(result);
|
|
431
|
+
}
|
|
432
|
+
// ─── Tool 2: ssh_hostkey_lookup ───
|
|
433
|
+
async function sshHostkeyLookup(args, ctx) {
|
|
434
|
+
const host = args.host;
|
|
435
|
+
const port = args.port ?? 22;
|
|
436
|
+
await limiter.acquire();
|
|
437
|
+
// First, get the SSH host key fingerprint via probe
|
|
438
|
+
const probe = await performSSHProbe(host, port);
|
|
439
|
+
if (!probe.kexInit) {
|
|
440
|
+
return json({
|
|
441
|
+
host,
|
|
442
|
+
port,
|
|
443
|
+
error: probe.error ?? "Could not retrieve SSH KEXINIT — cannot extract host key fingerprint",
|
|
444
|
+
fingerprint: null,
|
|
445
|
+
shodanResults: null,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
// We compute the HASSH as the "fingerprint" for cross-referencing.
|
|
449
|
+
// For a true host key fingerprint, we would need to complete the key exchange,
|
|
450
|
+
// but HASSH (algorithm fingerprint) is more useful for finding cloned configs.
|
|
451
|
+
const { hassh } = computeHASSH(probe.kexInit);
|
|
452
|
+
const result = {
|
|
453
|
+
host,
|
|
454
|
+
port,
|
|
455
|
+
banner: probe.banner,
|
|
456
|
+
hassh,
|
|
457
|
+
serverHostKeyAlgorithms: probe.kexInit.serverHostKeyAlgorithms,
|
|
458
|
+
shodanResults: null,
|
|
459
|
+
};
|
|
460
|
+
// Query Shodan if API key is available
|
|
461
|
+
const shodanKey = ctx.config.shodanApiKey;
|
|
462
|
+
if (shodanKey) {
|
|
463
|
+
try {
|
|
464
|
+
const query = encodeURIComponent(`ssh.hassh:${hassh}`);
|
|
465
|
+
const url = `https://api.shodan.io/shodan/host/search?key=${shodanKey}&query=${query}`;
|
|
466
|
+
const controller = new AbortController();
|
|
467
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
468
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
469
|
+
clearTimeout(timeout);
|
|
470
|
+
if (response.ok) {
|
|
471
|
+
const data = (await response.json());
|
|
472
|
+
result.shodanResults = {
|
|
473
|
+
total: data.total,
|
|
474
|
+
matches: (data.matches ?? []).slice(0, 25).map((m) => ({
|
|
475
|
+
ip: m.ip_str,
|
|
476
|
+
port: m.port,
|
|
477
|
+
org: m.org,
|
|
478
|
+
isp: m.isp,
|
|
479
|
+
os: m.os,
|
|
480
|
+
})),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
const errText = await response.text().catch(() => "");
|
|
485
|
+
result.shodanError = `Shodan API returned ${response.status}: ${errText.slice(0, 200)}`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
result.shodanError = `Shodan API error: ${err.message}`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
result.shodanError =
|
|
494
|
+
"SHODAN_API_KEY not set — returning fingerprint only. Set the key to cross-reference with Shodan.";
|
|
495
|
+
}
|
|
496
|
+
return json(result);
|
|
497
|
+
}
|
|
498
|
+
function assessKex(alg) {
|
|
499
|
+
if (DEPRECATED_KEX.has(alg)) {
|
|
500
|
+
return { name: alg, status: "deprecated", note: "Known vulnerable, should be disabled" };
|
|
501
|
+
}
|
|
502
|
+
if (WEAK_KEX.has(alg)) {
|
|
503
|
+
return { name: alg, status: "weak", note: "Uses SHA-1 or has known weaknesses" };
|
|
504
|
+
}
|
|
505
|
+
return { name: alg, status: "secure" };
|
|
506
|
+
}
|
|
507
|
+
function assessCipher(alg) {
|
|
508
|
+
if (DEPRECATED_CIPHERS.has(alg)) {
|
|
509
|
+
return { name: alg, status: "deprecated", note: "Broken or obsolete cipher, must be removed" };
|
|
510
|
+
}
|
|
511
|
+
if (WEAK_CIPHERS.has(alg)) {
|
|
512
|
+
return { name: alg, status: "weak", note: "CBC mode vulnerable to padding oracle attacks" };
|
|
513
|
+
}
|
|
514
|
+
return { name: alg, status: "secure" };
|
|
515
|
+
}
|
|
516
|
+
function assessMac(alg) {
|
|
517
|
+
if (DEPRECATED_MACS.has(alg)) {
|
|
518
|
+
return { name: alg, status: "deprecated", note: "MD5-based MAC, cryptographically broken" };
|
|
519
|
+
}
|
|
520
|
+
if (WEAK_MACS.has(alg)) {
|
|
521
|
+
return { name: alg, status: "weak", note: "SHA-1 based or short tag length" };
|
|
522
|
+
}
|
|
523
|
+
return { name: alg, status: "secure" };
|
|
524
|
+
}
|
|
525
|
+
function assessHostKey(alg) {
|
|
526
|
+
if (WEAK_HOST_KEY_TYPES.has(alg)) {
|
|
527
|
+
const note = alg === "ssh-dss"
|
|
528
|
+
? "DSA keys are limited to 1024 bits, considered insecure"
|
|
529
|
+
: "SHA-1 based signature, deprecated in OpenSSH 8.8+";
|
|
530
|
+
return { name: alg, status: "deprecated", note };
|
|
531
|
+
}
|
|
532
|
+
return { name: alg, status: "secure" };
|
|
533
|
+
}
|
|
534
|
+
async function sshAudit(args, _ctx) {
|
|
535
|
+
const host = args.host;
|
|
536
|
+
const port = args.port ?? 22;
|
|
537
|
+
const cacheKey = `ssh_audit:${host}:${port}`;
|
|
538
|
+
const cached = cache.get(cacheKey);
|
|
539
|
+
if (cached)
|
|
540
|
+
return json(cached);
|
|
541
|
+
await limiter.acquire();
|
|
542
|
+
const probe = await performSSHProbe(host, port);
|
|
543
|
+
if (!probe.kexInit) {
|
|
544
|
+
return json({
|
|
545
|
+
host,
|
|
546
|
+
port,
|
|
547
|
+
error: probe.error ?? "Could not retrieve KEXINIT for audit",
|
|
548
|
+
banner: probe.banner,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
// Assess all algorithm categories
|
|
552
|
+
const kexAssessment = probe.kexInit.kexAlgorithms.map(assessKex);
|
|
553
|
+
const cipherAssessment = probe.kexInit.encryptionClientToServer.map(assessCipher);
|
|
554
|
+
const macAssessment = probe.kexInit.macClientToServer.map(assessMac);
|
|
555
|
+
const hostKeyAssessment = probe.kexInit.serverHostKeyAlgorithms.map(assessHostKey);
|
|
556
|
+
const compressionList = probe.kexInit.compressionClientToServer;
|
|
557
|
+
// Collect all weak and deprecated
|
|
558
|
+
const allAssessments = [...kexAssessment, ...cipherAssessment, ...macAssessment, ...hostKeyAssessment];
|
|
559
|
+
const deprecated = allAssessments.filter((a) => a.status === "deprecated");
|
|
560
|
+
const weak = allAssessments.filter((a) => a.status === "weak");
|
|
561
|
+
const secure = allAssessments.filter((a) => a.status === "secure");
|
|
562
|
+
// Security rating
|
|
563
|
+
let rating;
|
|
564
|
+
let ratingDescription;
|
|
565
|
+
if (deprecated.length === 0 && weak.length === 0) {
|
|
566
|
+
rating = "A";
|
|
567
|
+
ratingDescription = "Excellent — no weak or deprecated algorithms found";
|
|
568
|
+
}
|
|
569
|
+
else if (deprecated.length === 0 && weak.length <= 2) {
|
|
570
|
+
rating = "B";
|
|
571
|
+
ratingDescription = `Good — ${weak.length} weak algorithm(s) found but no deprecated ones`;
|
|
572
|
+
}
|
|
573
|
+
else if (deprecated.length <= 2) {
|
|
574
|
+
rating = "C";
|
|
575
|
+
ratingDescription = `Fair — ${deprecated.length} deprecated and ${weak.length} weak algorithm(s) found`;
|
|
576
|
+
}
|
|
577
|
+
else if (deprecated.length <= 5) {
|
|
578
|
+
rating = "D";
|
|
579
|
+
ratingDescription = `Poor — ${deprecated.length} deprecated and ${weak.length} weak algorithm(s) found`;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
rating = "F";
|
|
583
|
+
ratingDescription = `Critical — ${deprecated.length} deprecated and ${weak.length} weak algorithm(s) found`;
|
|
584
|
+
}
|
|
585
|
+
// Spoofing detection
|
|
586
|
+
const spoofing = probe.banner
|
|
587
|
+
? detectBannerSpoofing(probe.banner, probe.kexInit)
|
|
588
|
+
: null;
|
|
589
|
+
// Version pinpointing
|
|
590
|
+
const versionEstimate = pinpointVersion(probe.kexInit);
|
|
591
|
+
// Recommendations
|
|
592
|
+
const recommendations = [];
|
|
593
|
+
if (deprecated.length > 0) {
|
|
594
|
+
recommendations.push(`Remove deprecated algorithms: ${deprecated.map((a) => a.name).join(", ")}`);
|
|
595
|
+
}
|
|
596
|
+
if (weak.length > 0) {
|
|
597
|
+
recommendations.push(`Consider replacing weak algorithms: ${weak.map((a) => a.name).join(", ")}`);
|
|
598
|
+
}
|
|
599
|
+
if (WEAK_HOST_KEY_TYPES.has("ssh-rsa") && probe.kexInit.serverHostKeyAlgorithms.includes("ssh-rsa")) {
|
|
600
|
+
recommendations.push("Disable ssh-rsa host key type — use rsa-sha2-256 or rsa-sha2-512 instead");
|
|
601
|
+
}
|
|
602
|
+
if (probe.kexInit.serverHostKeyAlgorithms.includes("ssh-dss")) {
|
|
603
|
+
recommendations.push("Remove ssh-dss (DSA) host key — DSA is limited to 1024 bits");
|
|
604
|
+
}
|
|
605
|
+
if (!probe.kexInit.kexAlgorithms.includes("curve25519-sha256") &&
|
|
606
|
+
!probe.kexInit.kexAlgorithms.includes("curve25519-sha256@libssh.org")) {
|
|
607
|
+
recommendations.push("Enable curve25519-sha256 key exchange for modern security");
|
|
608
|
+
}
|
|
609
|
+
if (!probe.kexInit.encryptionClientToServer.includes("chacha20-poly1305@openssh.com") &&
|
|
610
|
+
!probe.kexInit.encryptionClientToServer.includes("aes256-gcm@openssh.com")) {
|
|
611
|
+
recommendations.push("Enable chacha20-poly1305@openssh.com or aes256-gcm@openssh.com cipher");
|
|
612
|
+
}
|
|
613
|
+
if (compressionList.includes("zlib") || compressionList.includes("zlib@openssh.com")) {
|
|
614
|
+
recommendations.push("Consider disabling compression to mitigate CRIME/BREACH style attacks");
|
|
615
|
+
}
|
|
616
|
+
const result = {
|
|
617
|
+
host,
|
|
618
|
+
port,
|
|
619
|
+
banner: probe.banner,
|
|
620
|
+
hassh: probe.hassh,
|
|
621
|
+
hasshRaw: probe.hasshRaw,
|
|
622
|
+
versionEstimate,
|
|
623
|
+
bannerSpoofing: spoofing,
|
|
624
|
+
algorithms: {
|
|
625
|
+
kex: kexAssessment,
|
|
626
|
+
ciphers: cipherAssessment,
|
|
627
|
+
macs: macAssessment,
|
|
628
|
+
hostKeys: hostKeyAssessment,
|
|
629
|
+
compression: compressionList,
|
|
630
|
+
},
|
|
631
|
+
security: {
|
|
632
|
+
rating,
|
|
633
|
+
description: ratingDescription,
|
|
634
|
+
totalAlgorithms: allAssessments.length,
|
|
635
|
+
secure: secure.length,
|
|
636
|
+
weak: weak.length,
|
|
637
|
+
deprecated: deprecated.length,
|
|
638
|
+
weakList: weak.map((a) => ({ name: a.name, note: a.note })),
|
|
639
|
+
deprecatedList: deprecated.map((a) => ({ name: a.name, note: a.note })),
|
|
640
|
+
},
|
|
641
|
+
recommendations,
|
|
642
|
+
};
|
|
643
|
+
cache.set(cacheKey, result);
|
|
644
|
+
return json(result);
|
|
645
|
+
}
|
|
646
|
+
// ─── Tool Definitions ───
|
|
647
|
+
export const sshTools = [
|
|
648
|
+
{
|
|
649
|
+
name: "ssh_probe",
|
|
650
|
+
description: "SSH banner and key exchange analysis. Connects to SSH port, reads the server banner (protocol version, software, OS), triggers KEXINIT exchange to extract supported algorithms (kex, ciphers, MACs, compression, host key types), computes HASSH fingerprint, and detects banner spoofing (e.g., OpenSSH banner with Dropbear algorithms).",
|
|
651
|
+
schema: {
|
|
652
|
+
host: z.string().describe("Target host"),
|
|
653
|
+
port: z.number().optional().default(22).describe("SSH port"),
|
|
654
|
+
},
|
|
655
|
+
execute: sshProbe,
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: "ssh_hostkey_lookup",
|
|
659
|
+
description: "Cross-reference SSH host key fingerprint against Shodan. Connects to target SSH, extracts HASSH fingerprint, then queries Shodan API to find other IPs with the same algorithm set. Identifies cloned servers, same organization infrastructure, or lateral movement targets. Requires SHODAN_API_KEY for cross-referencing; without it, returns the fingerprint only.",
|
|
660
|
+
schema: {
|
|
661
|
+
host: z.string().describe("Target host to get SSH fingerprint"),
|
|
662
|
+
port: z.number().optional().default(22).describe("SSH port"),
|
|
663
|
+
},
|
|
664
|
+
execute: sshHostkeyLookup,
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: "ssh_audit",
|
|
668
|
+
description: "Full SSH algorithm enumeration with security assessment. Connects and extracts all supported KEX algorithms, ciphers, MACs, compression, and host key types. Flags deprecated (3des-cbc, arcfour, ssh-dss, diffie-hellman-group1-sha1) and weak algorithms. Calculates HASSH fingerprint, estimates OpenSSH version from algorithm set, detects banner-algorithm mismatches, and provides a security rating (A-F) with hardening recommendations.",
|
|
669
|
+
schema: {
|
|
670
|
+
host: z.string().describe("Target host"),
|
|
671
|
+
port: z.number().optional().default(22).describe("SSH port"),
|
|
672
|
+
},
|
|
673
|
+
execute: sshAudit,
|
|
674
|
+
},
|
|
675
|
+
];
|
|
676
|
+
//# sourceMappingURL=index.js.map
|