batman
1#!/usr/bin/env node
2
3/**
4 * JA4+ Fingerprint Analysis MCP Server
5 *
6 * Provides tools for analyzing JA4+ network fingerprints with human-readable
7 * explanations, pattern detection, and threat intelligence integration.
8 */
9
10import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12import {
13 CallToolRequestSchema,
14 ListToolsRequestSchema,
15 ListPromptsRequestSchema,
16 GetPromptRequestSchema,
17 ListResourcesRequestSchema,
18 ReadResourceRequestSchema,
19} from "@modelcontextprotocol/sdk/types.js";
20import fs from "fs/promises";
21import path from "path";
22import https from "https";
23import crypto from "crypto";
24import os from "os";
25
26const DB_URL = "https://ja4db.com/api/read/";
27
28// Get XDG cache directory or fallback to default locations
29function getXDGCacheDir() {
30 if (process.env.XDG_CACHE_HOME) {
31 return process.env.XDG_CACHE_HOME;
32 }
33
34 const homeDir = os.homedir();
35
36 switch (process.platform) {
37 case "win32":
38 return process.env.LOCALAPPDATA || path.join(homeDir, "AppData", "Local");
39 case "darwin":
40 return path.join(homeDir, "Library", "Caches");
41 default:
42 return path.join(homeDir, ".cache");
43 }
44}
45
46const CACHE_DIR = path.join(getXDGCacheDir(), "ja4-mcp");
47const DB_FILE = path.join(CACHE_DIR, "ja4db.json");
48const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
49
50class JA4Database {
51 constructor() {
52 this.data = [];
53 this.lastUpdate = null;
54 }
55
56 async ensureCacheDir() {
57 try {
58 await fs.mkdir(CACHE_DIR, { recursive: true });
59 } catch (error) {
60 // Directory might already exist, ignore error
61 }
62 }
63
64 async downloadDatabase() {
65 // Ensure cache directory exists before downloading
66 await this.ensureCacheDir();
67
68 return new Promise((resolve, reject) => {
69 console.error("Downloading JA4 database from ja4db.com...");
70 https
71 .get(DB_URL, (res) => {
72 let data = "";
73 res.on("data", (chunk) => (data += chunk));
74 res.on("end", async () => {
75 try {
76 await fs.writeFile(DB_FILE, data);
77 console.error("Database downloaded successfully");
78 resolve(JSON.parse(data));
79 } catch (error) {
80 reject(error);
81 }
82 });
83 })
84 .on("error", reject);
85 });
86 }
87
88 async loadDatabase() {
89 try {
90 const stats = await fs.stat(DB_FILE);
91 const age = Date.now() - stats.mtimeMs;
92
93 if (age > CACHE_DURATION) {
94 console.error("Database cache expired, downloading fresh copy...");
95 this.data = await this.downloadDatabase();
96 } else {
97 const content = await fs.readFile(DB_FILE, "utf-8");
98 this.data = JSON.parse(content);
99 console.error(`Loaded ${this.data.length} fingerprint records from cache`);
100 }
101 } catch (err) {
102 if (err.code === "ENOENT") {
103 console.error("Database not found, downloading...");
104 this.data = await this.downloadDatabase();
105 } else {
106 throw err;
107 }
108 }
109 this.lastUpdate = new Date();
110 }
111
112 lookupJA4(fingerprint) {
113 if (!this.data || this.data.length === 0) {
114 return [];
115 }
116 return this.data.filter((entry) => entry.ja4_fingerprint === fingerprint);
117 }
118
119 lookupJA4S(fingerprint) {
120 if (!this.data || this.data.length === 0) {
121 return [];
122 }
123 return this.data.filter((entry) => entry.ja4s_fingerprint === fingerprint);
124 }
125
126 lookupJA4H(fingerprint) {
127 if (!this.data || this.data.length === 0) {
128 return [];
129 }
130 return this.data.filter((entry) => entry.ja4h_fingerprint === fingerprint);
131 }
132
133 lookupJA4X(fingerprint) {
134 if (!this.data || this.data.length === 0) {
135 return [];
136 }
137 return this.data.filter((entry) => entry.ja4x_fingerprint === fingerprint);
138 }
139
140 lookupJA4T(fingerprint) {
141 if (!this.data || this.data.length === 0) {
142 return [];
143 }
144 return this.data.filter((entry) => entry.ja4t_fingerprint === fingerprint);
145 }
146
147 searchByApplication(appName) {
148 if (!this.data || this.data.length === 0) {
149 return [];
150 }
151 const searchTerm = appName.toLowerCase();
152 return this.data.filter((entry) => entry.application?.toLowerCase().includes(searchTerm));
153 }
154
155 searchByOS(osName) {
156 if (!this.data || this.data.length === 0) {
157 return [];
158 }
159 const searchTerm = osName.toLowerCase();
160 return this.data.filter((entry) => entry.os?.toLowerCase().includes(searchTerm));
161 }
162
163 getStatistics() {
164 if (!this.data || this.data.length === 0) {
165 return {
166 total_records: 0,
167 ja4_count: 0,
168 ja4s_count: 0,
169 ja4h_count: 0,
170 ja4x_count: 0,
171 ja4t_count: 0,
172 verified_count: 0,
173 applications: 0,
174 operating_systems: 0,
175 last_update: this.lastUpdate?.toISOString() || null,
176 status: "Database not loaded yet",
177 };
178 }
179 const stats = {
180 total_records: this.data.length,
181 ja4_count: this.data.filter((e) => e.ja4_fingerprint).length,
182 ja4s_count: this.data.filter((e) => e.ja4s_fingerprint).length,
183 ja4h_count: this.data.filter((e) => e.ja4h_fingerprint).length,
184 ja4x_count: this.data.filter((e) => e.ja4x_fingerprint).length,
185 ja4t_count: this.data.filter((e) => e.ja4t_fingerprint).length,
186 verified_count: this.data.filter((e) => e.verified).length,
187 applications: [...new Set(this.data.map((e) => e.application).filter(Boolean))].length,
188 operating_systems: [...new Set(this.data.map((e) => e.os).filter(Boolean))].length,
189 last_update: this.lastUpdate?.toISOString() || null,
190 status: "Database loaded",
191 };
192 return stats;
193 }
194}
195
196class JA4Analyzer {
197 /**
198 * Parse and explain JA4 (TLS Client) fingerprint
199 */
200 static parseJA4(fingerprint) {
201 const parts = fingerprint.split("_");
202 if (parts.length !== 3) {
203 throw new Error("Invalid JA4 fingerprint format. Expected format: a_b_c");
204 }
205
206 const [partA, partB, partC] = parts;
207
208 // Parse Part A: Protocol + Version + SNI + Cipher Count + Extension Count + ALPN
209 const protocol = partA[0];
210 const version = partA.substring(1, 3);
211 const sni = partA[3];
212 const cipherCount = partA.substring(4, 6);
213 const extensionCount = partA.substring(6, 8);
214 const alpn = partA.substring(8);
215
216 const analysis = {
217 fingerprint,
218 format: "JA4 (TLS Client Fingerprint)",
219 breakdown: {
220 part_a: {
221 raw: partA,
222 protocol: this.explainProtocol(protocol),
223 tls_version: this.explainTLSVersion(version),
224 sni_presence: this.explainSNI(sni),
225 cipher_count: parseInt(cipherCount, 10),
226 extension_count: parseInt(extensionCount, 10),
227 alpn: this.explainALPN(alpn),
228 },
229 part_b: {
230 raw: partB,
231 description: "SHA256 hash (truncated to 12 chars) of sorted cipher suites",
232 note: "Sorted to prevent evasion via cipher stunting",
233 },
234 part_c: {
235 raw: partC,
236 description: "SHA256 hash (truncated to 12 chars) of sorted extensions + signature algorithms",
237 note: "Extensions are sorted; SNI and ALPN removed for consistency across domains",
238 },
239 },
240 human_readable: this.generateHumanReadable(partA, protocol, version, sni, alpn),
241 };
242
243 return analysis;
244 }
245
246 /**
247 * Parse and explain JA4S (TLS Server) fingerprint
248 */
249 static parseJA4S(fingerprint) {
250 const parts = fingerprint.split("_");
251 if (parts.length !== 3) {
252 throw new Error("Invalid JA4S fingerprint format. Expected format: a_b_c");
253 }
254
255 const [partA, partB, partC] = parts;
256
257 const protocol = partA[0];
258 const version = partA.substring(1, 3);
259 const cipherCount = partA.substring(3, 5);
260 const extensionCount = partA.substring(5, 7);
261 const alpn = partA.substring(7);
262
263 return {
264 fingerprint,
265 format: "JA4S (TLS Server Response Fingerprint)",
266 breakdown: {
267 part_a: {
268 raw: partA,
269 protocol: this.explainProtocol(protocol),
270 tls_version: this.explainTLSVersion(version),
271 cipher_count: parseInt(cipherCount, 10),
272 extension_count: parseInt(extensionCount, 10),
273 alpn: this.explainALPN(alpn),
274 },
275 part_b: {
276 raw: partB,
277 description: "Selected cipher suite (4-char hex)",
278 note: "Server chooses one cipher from client offer",
279 },
280 part_c: {
281 raw: partC,
282 description: "SHA256 hash of sorted server extensions",
283 note: "Server extensions in response",
284 },
285 },
286 human_readable: `Server responding via ${this.explainProtocol(protocol).description}, using ${this.explainTLSVersion(version).description}, negotiated ${this.explainALPN(alpn).description}`,
287 };
288 }
289
290 /**
291 * Parse and explain JA4H (HTTP Client) fingerprint
292 */
293 static parseJA4H(fingerprint) {
294 const parts = fingerprint.split("_");
295 if (parts.length !== 4) {
296 throw new Error("Invalid JA4H fingerprint format. Expected format: a_b_c_d");
297 }
298
299 const [partA, partB, partC, partD] = parts;
300
301 const method = partA.substring(0, 2);
302 const version = partA.substring(2, 4);
303 const cookiePresence = partA[4];
304 const refererPresence = partA[5];
305 const headerCount = partA.substring(6, 8);
306 const languageHeader = partA.substring(8);
307
308 return {
309 fingerprint,
310 format: "JA4H (HTTP Client Fingerprint)",
311 breakdown: {
312 part_a: {
313 raw: partA,
314 method: this.explainHTTPMethod(method),
315 version: this.explainHTTPVersion(version),
316 has_cookies: cookiePresence === "c",
317 has_referer: refererPresence === "r",
318 header_count: parseInt(headerCount, 10),
319 language: languageHeader,
320 },
321 part_b: {
322 raw: partB,
323 description: "SHA256 hash of sorted HTTP header names",
324 note: "Identifies HTTP client library/browser",
325 },
326 part_c: {
327 raw: partC,
328 description: "SHA256 hash of sorted cookie field names",
329 note: "Server-controlled, consistent per application",
330 },
331 part_d: {
332 raw: partD,
333 description: "SHA256 hash of sorted cookie values",
334 note: "User-specific, can track individuals (GDPR compliant)",
335 },
336 },
337 use_cases: [
338 "JA4H_ab: Identify specific client applications/browsers",
339 "JA4H_c: Hunt for anomalies (should be consistent server-side)",
340 "JA4H_d: Track users without logging PII",
341 "Combined with JA4/JA4S: High-fidelity malware detection",
342 ],
343 };
344 }
345
346 /**
347 * Parse and explain JA4X (X.509 Certificate) fingerprint
348 */
349 static parseJA4X(fingerprint) {
350 const parts = fingerprint.split("_");
351 if (parts.length !== 3) {
352 throw new Error("Invalid JA4X fingerprint format. Expected format: a_b_c");
353 }
354
355 return {
356 fingerprint,
357 format: "JA4X (X.509 TLS Certificate Fingerprint)",
358 breakdown: {
359 part_a: {
360 raw: parts[0],
361 description: "SHA256 hash of issuer fields",
362 note: "Identifies Certificate Authority or self-signed pattern",
363 },
364 part_b: {
365 raw: parts[1],
366 description: "SHA256 hash of subject fields",
367 note: "Identifies certificate owner/server",
368 },
369 part_c: {
370 raw: parts[2],
371 description: "SHA256 hash of certificate extensions",
372 note: "Additional certificate properties and usage constraints",
373 },
374 },
375 use_cases: [
376 "Track malware C2 infrastructure",
377 "Identify certificate reuse across threat actors",
378 "Detect self-signed or suspicious certificates",
379 "Group related malicious servers",
380 ],
381 };
382 }
383
384 /**
385 * Parse and explain JA4T (TCP Client) fingerprint
386 */
387 static parseJA4T(fingerprint) {
388 const parts = fingerprint.split("_");
389 if (parts.length !== 4) {
390 throw new Error("Invalid JA4T fingerprint format. Expected format: window_size_tcp_options_mss_window_scale");
391 }
392
393 const [windowSize, tcpOptions, mss, windowScale] = parts;
394
395 // Parse TCP options
396 const optionsList = tcpOptions.split("-").map(Number);
397 const optionsExplained = optionsList.map((opt) => this.explainTCPOption(opt));
398
399 return {
400 fingerprint,
401 format: "JA4T (TCP Client Fingerprint)",
402 breakdown: {
403 window_size: {
404 raw: windowSize,
405 value: parseInt(windowSize, 10),
406 description: "TCP window size - maximum amount of data transmitted before ACK needed",
407 note: "Limited to 2 bytes (0-65535), determined by OS netcode",
408 },
409 tcp_options: {
410 raw: tcpOptions,
411 options: optionsExplained,
412 description: "Ordered list of TCP option kinds",
413 note: "TCP options are not required but used by every modern OS",
414 },
415 mss: {
416 raw: mss,
417 value: parseInt(mss, 10),
418 description: "Maximum Segment Size - largest data payload accepted per packet",
419 note: "Dependent on network overhead (1460 = standard Ethernet MTU 1500)",
420 },
421 window_scale: {
422 raw: windowScale,
423 value: parseInt(windowScale, 10),
424 description: "Window scale factor - multiplier for actual window size",
425 note: "Allows window size larger than 65535 (actual = window_size * 2^scale)",
426 },
427 },
428 analysis: {
429 actual_window_size: parseInt(windowSize, 10) * Math.pow(2, parseInt(windowScale, 10)),
430 network_overhead: this.analyzeNetworkOverhead(parseInt(mss, 10)),
431 os_indicators: this.analyzeTCPOSIndicators(optionsList, parseInt(windowSize, 10)),
432 tunnel_vpn_indicators: this.analyzeTunnelIndicators(parseInt(mss, 10)),
433 },
434 use_cases: [
435 "Operating system fingerprinting",
436 "Device type identification",
437 "Botnet traffic detection",
438 "VPN/Tunnel detection",
439 "NAT/Proxy identification",
440 "Network topology analysis",
441 ],
442 };
443 }
444
445 static explainTCPOption(option) {
446 const tcpOptions = {
447 0: { kind: 0, name: "End of Options List", description: "Marks the end of the option list" },
448 1: { kind: 1, name: "No Operation (NOP)", description: "Used for option alignment padding" },
449 2: { kind: 2, name: "Maximum Segment Size (MSS)", description: "Specifies maximum segment size" },
450 3: { kind: 3, name: "Window Scale", description: "Allows window sizes larger than 65535" },
451 4: { kind: 4, name: "SACK Permitted", description: "Selective Acknowledgment permitted" },
452 5: { kind: 5, name: "SACK", description: "Selective Acknowledgment data" },
453 6: { kind: 6, name: "Echo", description: "TCP echo option (obsolete)" },
454 7: { kind: 7, name: "Echo Reply", description: "TCP echo reply option (obsolete)" },
455 8: { kind: 8, name: "Timestamp", description: "TCP timestamps for RTT measurement" },
456 9: { kind: 9, name: "Partial Order Connection Permitted", description: "Partial order service" },
457 10: { kind: 10, name: "Partial Order Service Profile", description: "Partial order service profile" },
458 11: { kind: 11, name: "CC", description: "Connection Count option" },
459 12: { kind: 12, name: "CC.NEW", description: "Connection Count new option" },
460 13: { kind: 13, name: "CC.ECHO", description: "Connection Count echo option" },
461 14: { kind: 14, name: "TCP Alternate Checksum Request", description: "Alternative checksum request" },
462 15: { kind: 15, name: "TCP Alternate Checksum Data", description: "Alternative checksum data" },
463 };
464 return tcpOptions[option] || { kind: option, name: `Unknown Option ${option}`, description: "Unknown TCP option" };
465 }
466
467 static analyzeNetworkOverhead(mss) {
468 if (mss === 1460) {
469 return { type: "Standard Ethernet", overhead: 40, description: "Standard Ethernet MTU (1500)" };
470 } else if (mss === 1380) {
471 return { type: "Tunnel/VPN", overhead: 120, description: "Likely tunnel or VPN with encryption overhead" };
472 } else if (mss < 1380) {
473 return { type: "High Overhead", overhead: 1500 - mss - 40, description: "Significant network overhead detected" };
474 } else if (mss > 1460) {
475 return { type: "Jumbo Frames", overhead: 0, description: "Jumbo frames or custom MTU" };
476 }
477 return { type: "Custom", overhead: 1500 - mss - 40, description: "Custom MTU configuration" };
478 }
479
480 static analyzeTCPOSIndicators(options, windowSize) {
481 const indicators = [];
482
483 // Check for timestamp option (option 8)
484 if (!options.includes(8)) {
485 indicators.push("No timestamps - possible Windows or embedded system");
486 }
487
488 // Check for SACK (option 4)
489 if (options.includes(4)) {
490 indicators.push("SACK supported - modern TCP stack");
491 }
492
493 // Check window size patterns
494 if (windowSize === 65535) {
495 indicators.push("Max window size - common in Windows/Linux");
496 } else if (windowSize === 8192) {
497 indicators.push("8K window - possible older or embedded system");
498 }
499
500 // Check for specific option patterns
501 const optionString = options.join("-");
502 if (optionString === "2-4-8-1-3") {
503 indicators.push("Unix-like system pattern");
504 } else if (optionString === "2-4-1-3") {
505 indicators.push("Windows-like system pattern (no timestamps)");
506 }
507
508 return indicators;
509 }
510
511 static analyzeTunnelIndicators(mss) {
512 const indicators = [];
513
514 if (mss === 1424) {
515 indicators.push("36 bytes overhead - possible unencrypted tunnel or proxy");
516 } else if (mss === 1380) {
517 indicators.push("120 bytes overhead - likely VPN or encrypted tunnel");
518 } else if (mss < 1400) {
519 indicators.push("High overhead - multiple layers of encapsulation possible");
520 }
521
522 return indicators;
523 }
524
525 static explainProtocol(code) {
526 const protocols = {
527 t: { code: "t", description: "TLS over TCP", full_name: "Transport Layer Security over TCP" },
528 q: { code: "q", description: "QUIC", full_name: "QUIC (HTTP/3 over UDP)" },
529 d: { code: "d", description: "DTLS", full_name: "Datagram Transport Layer Security" },
530 };
531 return protocols[code] || { code, description: "Unknown protocol", full_name: "Unknown" };
532 }
533
534 static explainTLSVersion(version) {
535 const versions = {
536 10: { version: "1.0", description: "TLS 1.0 (deprecated, insecure)", security: "CRITICAL" },
537 11: { version: "1.1", description: "TLS 1.1 (deprecated, insecure)", security: "HIGH" },
538 12: { version: "1.2", description: "TLS 1.2 (secure, widely used)", security: "MEDIUM" },
539 13: { version: "1.3", description: "TLS 1.3 (modern, most secure)", security: "LOW" },
540 };
541 return versions[version] || { version, description: "Unknown TLS version", security: "UNKNOWN" };
542 }
543
544 static explainSNI(code) {
545 const sni = {
546 d: { code: "d", description: "SNI present (connecting to domain)", note: "Server Name Indication exists" },
547 i: {
548 code: "i",
549 description: "SNI absent (connecting to IP)",
550 note: "May indicate direct IP connection or certain tools",
551 },
552 };
553 return sni[code] || { code, description: "Unknown SNI status", note: "" };
554 }
555
556 static explainALPN(alpn) {
557 if (alpn === "00") {
558 return { code: "00", description: "No ALPN", note: "May not be a web browser" };
559 }
560
561 const alpns = {
562 h1: { code: "h1", description: "HTTP/1.1", protocol: "HTTP/1.1" },
563 h2: { code: "h2", description: "HTTP/2", protocol: "HTTP/2" },
564 h3: { code: "h3", description: "HTTP/3", protocol: "HTTP/3 (over QUIC)" },
565 dt: { code: "dt", description: "DNS-over-TLS", protocol: "DNS-over-TLS" },
566 dq: { code: "dq", description: "DNS-over-QUIC", protocol: "DNS-over-QUIC" },
567 };
568
569 return alpns[alpn] || { code: alpn, description: `ALPN: ${alpn}`, protocol: "Custom protocol" };
570 }
571
572 static explainHTTPMethod(code) {
573 const methods = {
574 ge: { code: "ge", method: "GET", description: "HTTP GET request" },
575 po: { code: "po", method: "POST", description: "HTTP POST request" },
576 he: { code: "he", method: "HEAD", description: "HTTP HEAD request" },
577 pu: { code: "pu", method: "PUT", description: "HTTP PUT request" },
578 de: { code: "de", method: "DELETE", description: "HTTP DELETE request" },
579 op: { code: "op", method: "OPTIONS", description: "HTTP OPTIONS request" },
580 pa: { code: "pa", method: "PATCH", description: "HTTP PATCH request" },
581 };
582 return methods[code] || { code, method: "UNKNOWN", description: "Unknown HTTP method" };
583 }
584
585 static explainHTTPVersion(version) {
586 const versions = {
587 10: { version: "1.0", description: "HTTP/1.0 (legacy)" },
588 11: { version: "1.1", description: "HTTP/1.1 (standard)" },
589 20: { version: "2.0", description: "HTTP/2 (modern, multiplexed)" },
590 30: { version: "3.0", description: "HTTP/3 (over QUIC)" },
591 };
592 return versions[version] || { version, description: "Unknown HTTP version" };
593 }
594
595 static generateHumanReadable(partA, protocol, version, sni, alpn) {
596 const proto = this.explainProtocol(protocol);
597 const ver = this.explainTLSVersion(version);
598 const sniInfo = this.explainSNI(sni);
599 const alpnInfo = this.explainALPN(alpn);
600
601 return `Client using ${proto.description} with ${ver.description}, ${sniInfo.description}, negotiating ${alpnInfo.description}`;
602 }
603
604 /**
605 * Compare two fingerprints and identify similarities
606 */
607 static compareFingerprints(fp1, fp2) {
608 const parts1 = fp1.split("_");
609 const parts2 = fp2.split("_");
610
611 if (parts1.length !== parts2.length) {
612 return { error: "Fingerprints must be of the same type and format" };
613 }
614
615 const comparison = {
616 fingerprint_1: fp1,
617 fingerprint_2: fp2,
618 identical: fp1 === fp2,
619 similarities: {},
620 differences: {},
621 };
622
623 // Compare each part
624 parts1.forEach((part, idx) => {
625 const letter = String.fromCharCode(97 + idx); // a, b, c, d
626 if (part === parts2[idx]) {
627 comparison.similarities[`part_${letter}`] = {
628 value: part,
629 note: "Identical",
630 };
631 } else {
632 comparison.differences[`part_${letter}`] = {
633 fp1: part,
634 fp2: parts2[idx],
635 note: this.getDifferenceNote(idx, parts1.length),
636 };
637 }
638 });
639
640 comparison.analysis = this.generateComparisonAnalysis(comparison);
641 return comparison;
642 }
643
644 static getDifferenceNote(partIndex, totalParts) {
645 if (totalParts === 3) {
646 // JA4/JA4S/JA4X format
647 const notes = [
648 "Different protocol details (version, counts, ALPN)",
649 "Different cipher/extension selection",
650 "Different extension/signature algorithms",
651 ];
652 return notes[partIndex] || "Different component";
653 } else if (totalParts === 4) {
654 // JA4H format
655 const notes = [
656 "Different HTTP method/version/headers",
657 "Different header names/order",
658 "Different cookie fields (server-side)",
659 "Different cookie values (user-specific)",
660 ];
661 return notes[partIndex] || "Different component";
662 }
663 return "Different component";
664 }
665
666 static generateComparisonAnalysis(comparison) {
667 const simCount = Object.keys(comparison.similarities).length;
668 const diffCount = Object.keys(comparison.differences).length;
669 const total = simCount + diffCount;
670
671 let analysis = `${simCount} of ${total} parts match. `;
672
673 if (comparison.identical) {
674 analysis += "Fingerprints are identical - same client configuration.";
675 } else if (simCount === total - 1) {
676 const diffPart = Object.keys(comparison.differences)[0];
677 analysis += `Only ${diffPart} differs - likely similar clients with minor variation. `;
678
679 if (diffPart === "part_b") {
680 analysis +=
681 "Different cipher selection or ordering. Could be same client family with configuration changes or fingerprint evasion attempt.";
682 }
683 } else if (simCount >= total / 2) {
684 analysis +=
685 "Partial match - clients share some characteristics but have notable differences. May be related applications or same library with different configurations.";
686 } else {
687 analysis += "Significant differences - these are likely different clients or applications.";
688 }
689
690 return analysis;
691 }
692
693 /**
694 * Detect patterns across multiple fingerprints
695 */
696 static detectPatterns(fingerprints) {
697 if (!Array.isArray(fingerprints) || fingerprints.length < 2) {
698 throw new Error("Provide at least 2 fingerprints for pattern detection");
699 }
700
701 const patterns = {
702 total_analyzed: fingerprints.length,
703 common_patterns: {},
704 outliers: [],
705 groupings: {},
706 };
707
708 // Analyze part A patterns (protocol, version, etc.)
709 const partAValues = {};
710 fingerprints.forEach((fp) => {
711 const parts = fp.split("_");
712 const partA = parts[0];
713 partAValues[partA] = (partAValues[partA] || 0) + 1;
714 });
715
716 // Find most common part A
717 const sortedPartA = Object.entries(partAValues).sort((a, b) => b[1] - a[1]);
718 patterns.common_patterns.protocol_config = {
719 most_common: sortedPartA[0][0],
720 occurrences: sortedPartA[0][1],
721 percentage: ((sortedPartA[0][1] / fingerprints.length) * 100).toFixed(1) + "%",
722 };
723
724 // Identify outliers (fingerprints with unique part A)
725 fingerprints.forEach((fp) => {
726 const partA = fp.split("_")[0];
727 if (partAValues[partA] === 1) {
728 patterns.outliers.push({
729 fingerprint: fp,
730 reason: "Unique protocol configuration",
731 });
732 }
733 });
734
735 // Group by part B (cipher patterns)
736 const partBGroups = {};
737 fingerprints.forEach((fp) => {
738 const parts = fp.split("_");
739 const partB = parts[1];
740 if (!partBGroups[partB]) {
741 partBGroups[partB] = [];
742 }
743 partBGroups[partB].push(fp);
744 });
745
746 patterns.groupings.by_cipher = Object.entries(partBGroups)
747 .map(([cipher, fps]) => ({
748 cipher_hash: cipher,
749 fingerprints: fps,
750 count: fps.length,
751 }))
752 .sort((a, b) => b.count - a.count);
753
754 // Analysis summary
755 patterns.analysis = this.generatePatternAnalysis(patterns, fingerprints.length);
756
757 return patterns;
758 }
759
760 static generatePatternAnalysis(patterns, total) {
761 let analysis = "";
762
763 const uniqueProtocolConfigs = Object.keys(patterns.common_patterns).length;
764 const outliersCount = patterns.outliers.length;
765 const cipherGroups = patterns.groupings.by_cipher.length;
766
767 analysis += `Analyzed ${total} fingerprints. `;
768
769 if (outliersCount === 0) {
770 analysis += "No outliers detected - all fingerprints share common characteristics. ";
771 } else {
772 analysis += `${outliersCount} outlier(s) detected with unique configurations. `;
773 }
774
775 if (cipherGroups === 1) {
776 analysis += "All fingerprints use the same cipher set - likely same application or library family.";
777 } else if (cipherGroups < total / 2) {
778 analysis += `Fingerprints cluster into ${cipherGroups} distinct cipher groups - suggests ${cipherGroups} different client families or configurations.`;
779 } else {
780 analysis += `High cipher diversity (${cipherGroups} unique cipher sets) - diverse client population or potential evasion techniques.`;
781 }
782
783 return analysis;
784 }
785
786 /**
787 * Generate investigation recommendations
788 */
789 static generateInvestigationTips(analysis, dbResults) {
790 const tips = {
791 fingerprint: analysis.fingerprint,
792 threat_indicators: [],
793 investigation_steps: [],
794 context: {},
795 };
796
797 // Check for old TLS versions
798 if (
799 analysis.breakdown?.part_a?.tls_version?.security === "CRITICAL" ||
800 analysis.breakdown?.part_a?.tls_version?.security === "HIGH"
801 ) {
802 tips.threat_indicators.push({
803 severity: "HIGH",
804 finding: "Outdated TLS version detected",
805 detail: analysis.breakdown.part_a.tls_version.description,
806 recommendation:
807 "Legacy TLS versions (1.0, 1.1) are deprecated and vulnerable. Investigate if this is expected for legacy systems or potential malware.",
808 });
809 }
810
811 // Check for missing ALPN
812 if (analysis.breakdown?.part_a?.alpn?.code === "00") {
813 tips.threat_indicators.push({
814 severity: "MEDIUM",
815 finding: "No ALPN extension present",
816 detail: "Client did not specify Application-Layer Protocol Negotiation",
817 recommendation:
818 "Lack of ALPN may indicate non-browser client, automated tool, or malware. Cross-reference with expected traffic patterns.",
819 });
820 }
821
822 // Check for IP-based connection (no SNI)
823 if (analysis.breakdown?.part_a?.sni_presence?.code === "i") {
824 tips.threat_indicators.push({
825 severity: "MEDIUM",
826 finding: "Direct IP connection (no SNI)",
827 detail: "Client connecting directly to IP address without Server Name Indication",
828 recommendation:
829 "Direct IP connections can indicate C2 traffic, security tools, or applications bypassing DNS. Verify against known infrastructure.",
830 });
831 }
832
833 // Add database context
834 if (dbResults && dbResults.length > 0) {
835 tips.context.known_applications = dbResults.map((r) => ({
836 application: r.application || "Unknown",
837 os: r.os || "Unknown",
838 verified: r.verified || false,
839 observation_count: r.observation_count || 0,
840 }));
841
842 tips.investigation_steps.push({
843 step: 1,
844 action: "Verify application match",
845 detail: `Database shows ${dbResults.length} known application(s) with this fingerprint. Verify if observed traffic matches expected application behavior.`,
846 });
847 } else {
848 tips.threat_indicators.push({
849 severity: "LOW",
850 finding: "Unknown fingerprint",
851 detail: "This fingerprint is not in the JA4DB community database",
852 recommendation:
853 "Unknown fingerprints warrant further investigation. Could be custom application, emerging threat, or rare legitimate software.",
854 });
855
856 tips.investigation_steps.push({
857 step: 1,
858 action: "Baseline the fingerprint",
859 detail:
860 "Document source IPs, destinations, timing, and any associated payloads. Determine if this is a new legitimate application or potential threat.",
861 });
862 }
863
864 // General investigation steps
865 tips.investigation_steps.push(
866 {
867 step: tips.investigation_steps.length + 1,
868 action: "Analyze traffic patterns",
869 detail:
870 "Look for: frequency of connections, time of day patterns, associated domains/IPs, payload sizes, and session durations.",
871 },
872 {
873 step: tips.investigation_steps.length + 2,
874 action: "Correlate with other indicators",
875 detail:
876 "Combine JA4 with JA4S (server response), JA4H (HTTP patterns), and JA4X (certificate) for high-fidelity detection.",
877 },
878 {
879 step: tips.investigation_steps.length + 3,
880 action: "Check threat intelligence",
881 detail:
882 "Search for this fingerprint in threat intelligence feeds, malware sandboxes, and security vendor databases.",
883 },
884 );
885
886 return tips;
887 }
888}
889
890// Initialize database
891const db = new JA4Database();
892
893// Create MCP server
894const server = new Server(
895 {
896 name: "ja4-analysis-server",
897 version: "1.0.0",
898 },
899 {
900 capabilities: {
901 tools: {},
902 prompts: {},
903 resources: {
904 subscribe: false,
905 listChanged: false,
906 },
907 },
908 },
909);
910
911// List available tools
912server.setRequestHandler(ListToolsRequestSchema, async () => {
913 return {
914 tools: [
915 {
916 name: "analyze_ja4",
917 description:
918 "Analyze a JA4 (TLS Client) fingerprint and provide detailed breakdown with human-readable explanation. Input format: t13d1516h2_8daaf6152771_02713d6af862",
919 inputSchema: {
920 type: "object",
921 properties: {
922 fingerprint: {
923 type: "string",
924 description: "JA4 fingerprint in format: a_b_c (e.g., t13d1516h2_8daaf6152771_02713d6af862)",
925 },
926 include_database_lookup: {
927 type: "boolean",
928 description: "Look up fingerprint in JA4DB community database",
929 default: true,
930 },
931 },
932 required: ["fingerprint"],
933 },
934 },
935 {
936 name: "analyze_ja4s",
937 description: "Analyze a JA4S (TLS Server Response) fingerprint and explain server-side characteristics",
938 inputSchema: {
939 type: "object",
940 properties: {
941 fingerprint: {
942 type: "string",
943 description: "JA4S fingerprint in format: a_b_c",
944 },
945 include_database_lookup: {
946 type: "boolean",
947 description: "Look up fingerprint in JA4DB",
948 default: true,
949 },
950 },
951 required: ["fingerprint"],
952 },
953 },
954 {
955 name: "analyze_ja4h",
956 description: "Analyze a JA4H (HTTP Client) fingerprint and explain HTTP request characteristics",
957 inputSchema: {
958 type: "object",
959 properties: {
960 fingerprint: {
961 type: "string",
962 description: "JA4H fingerprint in format: a_b_c_d",
963 },
964 include_database_lookup: {
965 type: "boolean",
966 description: "Look up fingerprint in JA4DB",
967 default: true,
968 },
969 },
970 required: ["fingerprint"],
971 },
972 },
973 {
974 name: "analyze_ja4x",
975 description:
976 "Analyze a JA4X (X.509 Certificate) fingerprint and provide detailed breakdown. Input format: a_b_c",
977 inputSchema: {
978 type: "object",
979 properties: {
980 fingerprint: {
981 type: "string",
982 description: "JA4X fingerprint in format: a_b_c",
983 },
984 include_database_lookup: {
985 type: "boolean",
986 description: "Look up fingerprint in JA4DB",
987 default: true,
988 },
989 },
990 required: ["fingerprint"],
991 },
992 },
993 {
994 name: "analyze_ja4t",
995 description:
996 "Analyze a JA4T (TCP Client) fingerprint and provide detailed breakdown with OS and network analysis. Input format: window_size_tcp_options_mss_window_scale",
997 inputSchema: {
998 type: "object",
999 properties: {
1000 fingerprint: {
1001 type: "string",
1002 description:
1003 "JA4T fingerprint in format: window_size_tcp_options_mss_window_scale (e.g., 65535_2-4-8-1-3_1460_7)",
1004 },
1005 include_database_lookup: {
1006 type: "boolean",
1007 description: "Look up fingerprint in JA4DB",
1008 default: true,
1009 },
1010 },
1011 required: ["fingerprint"],
1012 },
1013 },
1014 {
1015 name: "compare_fingerprints",
1016 description:
1017 "Compare two JA4+ fingerprints and identify similarities and differences. Useful for tracking related clients or detecting variations.",
1018 inputSchema: {
1019 type: "object",
1020 properties: {
1021 fingerprint1: {
1022 type: "string",
1023 description: "First fingerprint",
1024 },
1025 fingerprint2: {
1026 type: "string",
1027 description: "Second fingerprint",
1028 },
1029 },
1030 required: ["fingerprint1", "fingerprint2"],
1031 },
1032 },
1033 {
1034 name: "detect_patterns",
1035 description:
1036 "Analyze multiple fingerprints to detect patterns, identify outliers, and group similar clients. Useful for threat hunting and identifying campaigns.",
1037 inputSchema: {
1038 type: "object",
1039 properties: {
1040 fingerprints: {
1041 type: "array",
1042 items: { type: "string" },
1043 description: "Array of JA4+ fingerprints to analyze (minimum 2)",
1044 minItems: 2,
1045 },
1046 },
1047 required: ["fingerprints"],
1048 },
1049 },
1050 {
1051 name: "search_database",
1052 description: "Search the JA4DB community database by application name, OS, or other criteria",
1053 inputSchema: {
1054 type: "object",
1055 properties: {
1056 query: {
1057 type: "string",
1058 description: "Search term (application name, OS, etc.)",
1059 },
1060 search_type: {
1061 type: "string",
1062 enum: ["application", "os", "all"],
1063 description: "Type of search to perform",
1064 default: "all",
1065 },
1066 limit: {
1067 type: "number",
1068 description: "Maximum number of results to return",
1069 default: 10,
1070 },
1071 },
1072 required: ["query"],
1073 },
1074 },
1075 {
1076 name: "get_investigation_tips",
1077 description:
1078 "Generate investigation recommendations based on fingerprint analysis. Provides threat indicators, investigation steps, and context.",
1079 inputSchema: {
1080 type: "object",
1081 properties: {
1082 fingerprint: {
1083 type: "string",
1084 description: "JA4+ fingerprint to investigate",
1085 },
1086 fingerprint_type: {
1087 type: "string",
1088 enum: ["ja4", "ja4s", "ja4h", "ja4x", "ja4t"],
1089 description: "Type of fingerprint",
1090 default: "ja4",
1091 },
1092 },
1093 required: ["fingerprint"],
1094 },
1095 },
1096 {
1097 name: "database_stats",
1098 description: "Get statistics about the JA4DB community database (record counts, last update, etc.)",
1099 inputSchema: {
1100 type: "object",
1101 properties: {},
1102 },
1103 },
1104 {
1105 name: "refresh_database",
1106 description: "Force refresh of the JA4DB community database from ja4db.com",
1107 inputSchema: {
1108 type: "object",
1109 properties: {},
1110 },
1111 },
1112 ],
1113 };
1114});
1115
1116// List available resources
1117server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
1118 const resources = [
1119 // Database resources
1120 {
1121 uri: "ja4-database://fingerprints",
1122 name: "JA4 Fingerprints Database",
1123 title: "Complete JA4 Fingerprints Database",
1124 description: "Complete database of known JA4 fingerprints with applications and OS information",
1125 mimeType: "application/json",
1126 annotations: {
1127 audience: ["user", "assistant"],
1128 priority: 0.9,
1129 },
1130 },
1131 {
1132 uri: "ja4-database://applications",
1133 name: "Applications Database",
1134 title: "Applications with JA4 Fingerprints",
1135 description: "List of applications and their associated JA4 fingerprints",
1136 mimeType: "application/json",
1137 annotations: {
1138 audience: ["user", "assistant"],
1139 priority: 0.8,
1140 },
1141 },
1142 {
1143 uri: "ja4-database://operating-systems",
1144 name: "Operating Systems Database",
1145 title: "OS Patterns in JA4 Database",
1146 description: "Operating system patterns found in JA4 fingerprints",
1147 mimeType: "application/json",
1148 annotations: {
1149 audience: ["user", "assistant"],
1150 priority: 0.8,
1151 },
1152 },
1153 {
1154 uri: "ja4-database://statistics",
1155 name: "Database Statistics",
1156 title: "JA4 Database Statistics",
1157 description: "Statistics about the JA4 fingerprints database",
1158 mimeType: "application/json",
1159 annotations: {
1160 audience: ["user", "assistant"],
1161 priority: 0.7,
1162 },
1163 },
1164 {
1165 uri: "ja4-database://verified",
1166 name: "Verified Fingerprints",
1167 title: "Verified JA4 Fingerprints",
1168 description: "Only verified JA4 fingerprints from the database",
1169 mimeType: "application/json",
1170 annotations: {
1171 audience: ["user", "assistant"],
1172 priority: 0.8,
1173 },
1174 },
1175 // Documentation resources
1176 {
1177 uri: "ja4-docs://protocols",
1178 name: "Protocol Reference",
1179 title: "JA4 Protocol Codes Reference",
1180 description: "Reference documentation for JA4 protocol codes (t, q, d)",
1181 mimeType: "application/json",
1182 annotations: {
1183 audience: ["user", "assistant"],
1184 priority: 0.9,
1185 },
1186 },
1187 {
1188 uri: "ja4-docs://tls-versions",
1189 name: "TLS Versions Reference",
1190 title: "TLS Version Mappings",
1191 description: "TLS version codes and their meanings in JA4 fingerprints",
1192 mimeType: "application/json",
1193 annotations: {
1194 audience: ["user", "assistant"],
1195 priority: 0.8,
1196 },
1197 },
1198 {
1199 uri: "ja4-docs://tcp-options",
1200 name: "TCP Options Reference",
1201 title: "TCP Options Documentation",
1202 description: "Complete reference for TCP options used in JA4T fingerprints",
1203 mimeType: "application/json",
1204 annotations: {
1205 audience: ["user", "assistant"],
1206 priority: 0.8,
1207 },
1208 },
1209 {
1210 uri: "ja4-docs://http-methods",
1211 name: "HTTP Methods Reference",
1212 title: "HTTP Method Codes",
1213 description: "HTTP method codes used in JA4H fingerprints",
1214 mimeType: "application/json",
1215 annotations: {
1216 audience: ["user", "assistant"],
1217 priority: 0.7,
1218 },
1219 },
1220 {
1221 uri: "ja4-docs://alpn-protocols",
1222 name: "ALPN Protocols Reference",
1223 title: "ALPN Protocol Mappings",
1224 description: "Application-Layer Protocol Negotiation codes and protocols",
1225 mimeType: "application/json",
1226 annotations: {
1227 audience: ["user", "assistant"],
1228 priority: 0.7,
1229 },
1230 },
1231 ];
1232
1233 return {
1234 resources: resources.slice(request.params?.cursor || 0),
1235 };
1236});
1237
1238// Read resource contents
1239server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1240 const uri = request.params.uri;
1241
1242 try {
1243 if (uri.startsWith("ja4-database://")) {
1244 await db.loadDatabase();
1245 const resourceType = uri.replace("ja4-database://", "");
1246
1247 switch (resourceType) {
1248 case "fingerprints":
1249 return {
1250 contents: [
1251 {
1252 uri,
1253 mimeType: "application/json",
1254 text: JSON.stringify(db.database, null, 2),
1255 },
1256 ],
1257 };
1258
1259 case "applications":
1260 const applications = {};
1261 if (db.database && db.database.length > 0) {
1262 db.database.forEach((record) => {
1263 if (record.Application && record.Application !== "") {
1264 if (!applications[record.Application]) {
1265 applications[record.Application] = [];
1266 }
1267 applications[record.Application].push({
1268 ja4: record.JA4,
1269 ja4s: record.JA4S,
1270 ja4h: record.JA4H,
1271 ja4x: record.JA4X,
1272 ja4t: record.JA4T,
1273 os: record["Operating System"],
1274 verified: record.Verified === "Yes",
1275 });
1276 }
1277 });
1278 }
1279 return {
1280 contents: [
1281 {
1282 uri,
1283 mimeType: "application/json",
1284 text: JSON.stringify(applications, null, 2),
1285 },
1286 ],
1287 };
1288
1289 case "operating-systems":
1290 const operatingSystems = {};
1291 if (db.database && db.database.length > 0) {
1292 db.database.forEach((record) => {
1293 if (record["Operating System"] && record["Operating System"] !== "") {
1294 const os = record["Operating System"];
1295 if (!operatingSystems[os]) {
1296 operatingSystems[os] = [];
1297 }
1298 operatingSystems[os].push({
1299 ja4: record.JA4,
1300 ja4s: record.JA4S,
1301 ja4h: record.JA4H,
1302 ja4x: record.JA4X,
1303 ja4t: record.JA4T,
1304 application: record.Application,
1305 verified: record.Verified === "Yes",
1306 });
1307 }
1308 });
1309 }
1310 return {
1311 contents: [
1312 {
1313 uri,
1314 mimeType: "application/json",
1315 text: JSON.stringify(operatingSystems, null, 2),
1316 },
1317 ],
1318 };
1319
1320 case "statistics":
1321 const stats = await db.getStatistics();
1322 return {
1323 contents: [
1324 {
1325 uri,
1326 mimeType: "application/json",
1327 text: JSON.stringify(stats, null, 2),
1328 },
1329 ],
1330 };
1331
1332 case "verified":
1333 const verified = db.database ? db.database.filter((record) => record.Verified === "Yes") : [];
1334 return {
1335 contents: [
1336 {
1337 uri,
1338 mimeType: "application/json",
1339 text: JSON.stringify(verified, null, 2),
1340 },
1341 ],
1342 };
1343
1344 default:
1345 throw new Error(`Unknown database resource: ${resourceType}`);
1346 }
1347 } else if (uri.startsWith("ja4-docs://")) {
1348 const docType = uri.replace("ja4-docs://", "");
1349
1350 switch (docType) {
1351 case "protocols":
1352 const protocols = {
1353 t: { code: "t", description: "TCP", full_name: "Transmission Control Protocol" },
1354 q: { code: "q", description: "QUIC", full_name: "Quick UDP Internet Connections" },
1355 d: { code: "d", description: "DTLS", full_name: "Datagram Transport Layer Security" },
1356 };
1357 return {
1358 contents: [
1359 {
1360 uri,
1361 mimeType: "application/json",
1362 text: JSON.stringify(protocols, null, 2),
1363 },
1364 ],
1365 };
1366
1367 case "tls-versions":
1368 const tlsVersions = {
1369 10: { version: "TLS 1.0", description: "Legacy TLS version", security: "Deprecated" },
1370 11: { version: "TLS 1.1", description: "Legacy TLS version", security: "Deprecated" },
1371 12: { version: "TLS 1.2", description: "Current standard TLS version", security: "Secure" },
1372 13: { version: "TLS 1.3", description: "Latest TLS version", security: "Most Secure" },
1373 };
1374 return {
1375 contents: [
1376 {
1377 uri,
1378 mimeType: "application/json",
1379 text: JSON.stringify(tlsVersions, null, 2),
1380 },
1381 ],
1382 };
1383
1384 case "tcp-options":
1385 const tcpOptions = {
1386 0: { kind: 0, name: "End of Option List", description: "End of TCP options list" },
1387 1: { kind: 1, name: "No Operation", description: "No operation (padding)" },
1388 2: { kind: 2, name: "Maximum Segment Size", description: "Maximum segment size" },
1389 3: { kind: 3, name: "Window Scale", description: "Window scale factor" },
1390 4: { kind: 4, name: "SACK Permitted", description: "Selective acknowledgment permitted" },
1391 5: { kind: 5, name: "SACK", description: "Selective acknowledgment" },
1392 6: { kind: 6, name: "Echo", description: "Echo (obsolete)" },
1393 7: { kind: 7, name: "Echo Reply", description: "Echo reply (obsolete)" },
1394 8: { kind: 8, name: "Timestamp", description: "TCP timestamp" },
1395 9: {
1396 kind: 9,
1397 name: "Partial Order Connection Permitted",
1398 description: "Partial order connection permitted",
1399 },
1400 10: { kind: 10, name: "Partial Order Service Profile", description: "Partial order service profile" },
1401 11: { kind: 11, name: "Connection Count", description: "Connection count" },
1402 12: { kind: 12, name: "Connection Count New", description: "Connection count (new)" },
1403 13: { kind: 13, name: "Connection Count Echo", description: "Connection count echo" },
1404 14: { kind: 14, name: "TCP Alternate Checksum Request", description: "TCP alternate checksum request" },
1405 15: { kind: 15, name: "TCP Alternate Checksum Data", description: "TCP alternate checksum data" },
1406 };
1407 return {
1408 contents: [
1409 {
1410 uri,
1411 mimeType: "application/json",
1412 text: JSON.stringify(tcpOptions, null, 2),
1413 },
1414 ],
1415 };
1416
1417 case "http-methods":
1418 const httpMethods = {
1419 ge: { code: "ge", method: "GET", description: "Retrieve data" },
1420 po: { code: "po", method: "POST", description: "Submit data" },
1421 he: { code: "he", method: "HEAD", description: "Retrieve headers only" },
1422 pu: { code: "pu", method: "PUT", description: "Update resource" },
1423 de: { code: "de", method: "DELETE", description: "Delete resource" },
1424 op: { code: "op", method: "OPTIONS", description: "Get allowed methods" },
1425 pa: { code: "pa", method: "PATCH", description: "Partial update" },
1426 };
1427 return {
1428 contents: [
1429 {
1430 uri,
1431 mimeType: "application/json",
1432 text: JSON.stringify(httpMethods, null, 2),
1433 },
1434 ],
1435 };
1436
1437 case "alpn-protocols":
1438 const alpnProtocols = {
1439 h1: { code: "h1", description: "HTTP/1.1", protocol: "HTTP/1.1" },
1440 h2: { code: "h2", description: "HTTP/2", protocol: "HTTP/2" },
1441 h3: { code: "h3", description: "HTTP/3", protocol: "HTTP/3" },
1442 dt: { code: "dt", description: "DoT", protocol: "DNS over TLS" },
1443 dq: { code: "dq", description: "DoQ", protocol: "DNS over QUIC" },
1444 };
1445 return {
1446 contents: [
1447 {
1448 uri,
1449 mimeType: "application/json",
1450 text: JSON.stringify(alpnProtocols, null, 2),
1451 },
1452 ],
1453 };
1454
1455 default:
1456 throw new Error(`Unknown documentation resource: ${docType}`);
1457 }
1458 } else {
1459 throw new Error(`Unknown resource scheme: ${uri}`);
1460 }
1461 } catch (error) {
1462 throw new Error(`Failed to read resource ${uri}: ${error.message}`);
1463 }
1464});
1465
1466// List available prompts
1467server.setRequestHandler(ListPromptsRequestSchema, async () => {
1468 return {
1469 prompts: [
1470 {
1471 name: "analyst-guidance",
1472 description: "JA4+ fingerprint analysis guidance for security analysts and threat hunters",
1473 arguments: [],
1474 },
1475 ],
1476 };
1477});
1478
1479// Handle prompt requests
1480server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1481 const { name } = request.params;
1482
1483 if (name === "analyst-guidance") {
1484 return {
1485 messages: [
1486 {
1487 role: "user",
1488 content: {
1489 type: "text",
1490 text: `# JA4+ Network Fingerprint Analysis Assistant
1491
1492You are a cybersecurity analyst assistant specializing in JA4+ network fingerprinting for threat detection, malware analysis, and network security monitoring.
1493
1494## Core Knowledge
1495
1496### JA4+ Fingerprint Types
1497- **JA4 (TLS Client)**: \`a_b_c\` format - identifies client applications and TLS configurations
1498- **JA4S (TLS Server)**: \`a_b_c\` format - identifies server responses and cipher selection
1499- **JA4H (HTTP Client)**: \`a_b_c_d\` format - identifies HTTP client behavior patterns
1500- **JA4X (X.509 Certificate)**: \`a_b_c\` format - identifies certificate patterns for infrastructure tracking
1501
1502### Critical Security Indicators
1503**High-Risk TLS Versions:**
1504- \`t10\` or \`t11\` = TLS 1.0/1.1 (DEPRECATED, high risk)
1505- \`t12\` = TLS 1.2 (acceptable but aging)
1506- \`t13\` = TLS 1.3 (preferred, most secure)
1507
1508**Malware/Bot Indicators:**
1509- Missing ALPN (\`00\`) in modern traffic = potential non-browser client
1510- Legacy TLS versions in new connections = possible malware
1511- No cookies (\`n\`) + no referer (\`n\`) in JA4H = likely automated/bot traffic
1512- Self-signed certificates (JA4X part_a ≈ part_b) = potential C2 infrastructure
1513
1514**Evasion Detection:**
1515- Same JA4 \`a_c\` parts with different \`b\` = cipher randomization/evasion
1516- Unusual cipher counts or extension patterns = custom implementations
1517
1518## Analysis Workflow
1519
1520### 1. Single Fingerprint Analysis
1521Always start with the appropriate analyze tool:
1522\`\`\`
1523analyze_ja4({ fingerprint: "...", include_database_lookup: true })
1524analyze_ja4s({ fingerprint: "...", include_database_lookup: true })
1525analyze_ja4h({ fingerprint: "...", include_database_lookup: true })
1526analyze_ja4x({ fingerprint: "...", include_database_lookup: true })
1527\`\`\`
1528
1529### 2. Generate Investigation Guidance
1530For suspicious fingerprints:
1531\`\`\`
1532get_investigation_tips({ fingerprint: "...", fingerprint_type: "ja4" })
1533\`\`\`
1534
1535### 3. Pattern Detection & Campaign Analysis
1536For multiple related fingerprints:
1537\`\`\`
1538detect_patterns({ fingerprints: ["fp1", "fp2", "fp3", ...] })
1539\`\`\`
1540
1541### 4. Comparative Analysis
1542To understand relationships:
1543\`\`\`
1544compare_fingerprints({ fingerprint1: "...", fingerprint2: "..." })
1545\`\`\`
1546
1547## Interpretation Guidelines
1548
1549### Database Match Assessment
1550- **verified: true** = High confidence, community validated
1551- **High observation_count** = Common/legitimate application
1552- **No matches** = Unknown client, investigate further
1553- **Multiple matches** = Fingerprint collision, context matters
1554
1555### Locality-Preserving Analysis
1556Use partial matching:
1557- **JA4_a matching**: Same protocol + version + counts = related configurations
1558- **JA4_b differences**: Different cipher selection = potential evasion
1559- **JA4_c matching**: Same extensions = same underlying client type
1560- **JA4_ac matching**: Ignore cipher changes, focus on client behavior
1561
1562### Threat Hunting Patterns
1563**Malware Detection:**
1564- Combine JA4 + JA4S for high-fidelity detection
1565- Look for known bad fingerprint combinations
1566- Check for unusual TLS configurations
1567
1568**C2 Infrastructure Tracking:**
1569- Use JA4X to track certificate reuse across IPs
1570- Identify self-signed certificate patterns
1571- Monitor certificate authority anomalies
1572
1573**Campaign Attribution:**
1574- Group fingerprints by common JA4_ac patterns
1575- Track cipher selection evolution over time
1576
1577## Response Framework
1578
1579For each analysis, provide:
15801. **Security Assessment** - Risk level and specific findings
15812. **Application Context** - Likely identification and legitimacy
15823. **Investigation Path** - Next steps and correlations
15834. **Threat Intelligence** - Known associations and attribution
1584
1585## Key Reminders
1586- Always use database lookups to distinguish known-good from unknown traffic
1587- Combine fingerprint types (JA4+JA4S, JA4+JA4H) for complete picture
1588- Focus on anomalies - unexpected configurations in expected environments
1589- Consider temporal patterns - new fingerprints, sudden changes
1590- Use partial matching to detect evasion and track evolving threats
1591
1592Your analysis should provide both immediate actionable intelligence and strategic context for ongoing security monitoring and threat hunting.`,
1593 },
1594 },
1595 ],
1596 };
1597 }
1598
1599 throw new Error(`Prompt not found: ${name}`);
1600});
1601
1602// Handle tool calls
1603server.setRequestHandler(CallToolRequestSchema, async (request) => {
1604 const { name, arguments: args } = request.params;
1605
1606 try {
1607 switch (name) {
1608 case "analyze_ja4": {
1609 const analysis = JA4Analyzer.parseJA4(args.fingerprint);
1610 let result = { analysis };
1611
1612 if (args.include_database_lookup !== false) {
1613 const dbResults = db.lookupJA4(args.fingerprint);
1614 result.database_matches = {
1615 count: dbResults.length,
1616 results: dbResults.slice(0, 5),
1617 };
1618 }
1619
1620 return {
1621 content: [
1622 {
1623 type: "text",
1624 text: JSON.stringify(result, null, 2),
1625 },
1626 ],
1627 };
1628 }
1629
1630 case "analyze_ja4s": {
1631 const analysis = JA4Analyzer.parseJA4S(args.fingerprint);
1632 let result = { analysis };
1633
1634 if (args.include_database_lookup !== false) {
1635 const dbResults = db.lookupJA4S(args.fingerprint);
1636 result.database_matches = {
1637 count: dbResults.length,
1638 results: dbResults.slice(0, 5),
1639 };
1640 }
1641
1642 return {
1643 content: [
1644 {
1645 type: "text",
1646 text: JSON.stringify(result, null, 2),
1647 },
1648 ],
1649 };
1650 }
1651
1652 case "analyze_ja4h": {
1653 const analysis = JA4Analyzer.parseJA4H(args.fingerprint);
1654 let result = { analysis };
1655
1656 if (args.include_database_lookup !== false) {
1657 const dbResults = db.lookupJA4H(args.fingerprint);
1658 result.database_matches = {
1659 count: dbResults.length,
1660 results: dbResults.slice(0, 5),
1661 };
1662 }
1663
1664 return {
1665 content: [
1666 {
1667 type: "text",
1668 text: JSON.stringify(result, null, 2),
1669 },
1670 ],
1671 };
1672 }
1673
1674 case "analyze_ja4x": {
1675 const analysis = JA4Analyzer.parseJA4X(args.fingerprint);
1676 let result = { analysis };
1677
1678 if (args.include_database_lookup !== false) {
1679 const dbResults = db.lookupJA4X(args.fingerprint);
1680 result.database_matches = {
1681 count: dbResults.length,
1682 results: dbResults.slice(0, 5),
1683 };
1684 }
1685
1686 return {
1687 content: [
1688 {
1689 type: "text",
1690 text: JSON.stringify(result, null, 2),
1691 },
1692 ],
1693 };
1694 }
1695
1696 case "analyze_ja4t": {
1697 const analysis = JA4Analyzer.parseJA4T(args.fingerprint);
1698 let result = { analysis };
1699
1700 if (args.include_database_lookup !== false) {
1701 const dbResults = db.lookupJA4T(args.fingerprint);
1702 result.database_matches = {
1703 count: dbResults.length,
1704 results: dbResults.slice(0, 5),
1705 };
1706 }
1707
1708 return {
1709 content: [
1710 {
1711 type: "text",
1712 text: JSON.stringify(result, null, 2),
1713 },
1714 ],
1715 };
1716 }
1717
1718 case "compare_fingerprints": {
1719 const comparison = JA4Analyzer.compareFingerprints(args.fingerprint1, args.fingerprint2);
1720
1721 return {
1722 content: [
1723 {
1724 type: "text",
1725 text: JSON.stringify(comparison, null, 2),
1726 },
1727 ],
1728 };
1729 }
1730
1731 case "detect_patterns": {
1732 const patterns = JA4Analyzer.detectPatterns(args.fingerprints);
1733
1734 return {
1735 content: [
1736 {
1737 type: "text",
1738 text: JSON.stringify(patterns, null, 2),
1739 },
1740 ],
1741 };
1742 }
1743
1744 case "search_database": {
1745 let results = [];
1746 const searchType = args.search_type || "all";
1747
1748 if (searchType === "application" || searchType === "all") {
1749 results = results.concat(db.searchByApplication(args.query));
1750 }
1751
1752 if (searchType === "os" || searchType === "all") {
1753 results = results.concat(db.searchByOS(args.query));
1754 }
1755
1756 // Remove duplicates
1757 results = Array.from(new Set(results.map((r) => JSON.stringify(r)))).map((r) => JSON.parse(r));
1758
1759 const limit = args.limit || 10;
1760
1761 return {
1762 content: [
1763 {
1764 type: "text",
1765 text: JSON.stringify(
1766 {
1767 query: args.query,
1768 search_type: searchType,
1769 total_results: results.length,
1770 results: results.slice(0, limit),
1771 },
1772 null,
1773 2,
1774 ),
1775 },
1776 ],
1777 };
1778 }
1779
1780 case "get_investigation_tips": {
1781 const type = args.fingerprint_type || "ja4";
1782 let analysis;
1783 let dbResults = [];
1784
1785 switch (type) {
1786 case "ja4":
1787 analysis = JA4Analyzer.parseJA4(args.fingerprint);
1788 dbResults = db.lookupJA4(args.fingerprint);
1789 break;
1790 case "ja4s":
1791 analysis = JA4Analyzer.parseJA4S(args.fingerprint);
1792 dbResults = db.lookupJA4S(args.fingerprint);
1793 break;
1794 case "ja4h":
1795 analysis = JA4Analyzer.parseJA4H(args.fingerprint);
1796 dbResults = db.lookupJA4H(args.fingerprint);
1797 break;
1798 case "ja4x":
1799 analysis = JA4Analyzer.parseJA4X(args.fingerprint);
1800 dbResults = db.lookupJA4X(args.fingerprint);
1801 break;
1802 case "ja4t":
1803 analysis = JA4Analyzer.parseJA4T(args.fingerprint);
1804 dbResults = db.lookupJA4T(args.fingerprint);
1805 break;
1806 }
1807
1808 const tips = JA4Analyzer.generateInvestigationTips(analysis, dbResults);
1809
1810 return {
1811 content: [
1812 {
1813 type: "text",
1814 text: JSON.stringify(tips, null, 2),
1815 },
1816 ],
1817 };
1818 }
1819
1820 case "database_stats": {
1821 const stats = db.getStatistics();
1822
1823 return {
1824 content: [
1825 {
1826 type: "text",
1827 text: JSON.stringify(stats, null, 2),
1828 },
1829 ],
1830 };
1831 }
1832
1833 case "refresh_database": {
1834 try {
1835 await db.downloadDatabase();
1836 await db.loadDatabase();
1837 const stats = db.getStatistics();
1838
1839 return {
1840 content: [
1841 {
1842 type: "text",
1843 text: JSON.stringify(
1844 {
1845 status: "Database refreshed successfully",
1846 stats,
1847 },
1848 null,
1849 2,
1850 ),
1851 },
1852 ],
1853 };
1854 } catch (error) {
1855 return {
1856 content: [
1857 {
1858 type: "text",
1859 text: JSON.stringify(
1860 {
1861 status: "Failed to refresh database",
1862 error: error.message,
1863 note: "Server will continue with cached data if available",
1864 },
1865 null,
1866 2,
1867 ),
1868 },
1869 ],
1870 };
1871 }
1872 }
1873
1874 default:
1875 throw new Error(`Unknown tool: ${name}`);
1876 }
1877 } catch (error) {
1878 return {
1879 content: [
1880 {
1881 type: "text",
1882 text: JSON.stringify(
1883 {
1884 error: error.message,
1885 stack: error.stack,
1886 },
1887 null,
1888 2,
1889 ),
1890 },
1891 ],
1892 isError: true,
1893 };
1894 }
1895});
1896
1897// Start server
1898async function main() {
1899 try {
1900 console.error("JA4 Analysis MCP Server starting...");
1901 console.error("Node version:", process.version);
1902 console.error("Platform:", process.platform);
1903
1904 // Start server first, then load database in background
1905 console.error("Creating StdioServerTransport...");
1906 const transport = new StdioServerTransport();
1907
1908 console.error("Attempting to connect server...");
1909 await server.connect(transport);
1910 console.error("JA4 Analysis MCP Server connected successfully");
1911
1912 // Add connection event handlers
1913 transport.onclose = () => {
1914 console.error("Transport closed");
1915 };
1916
1917 transport.onerror = (error) => {
1918 console.error("Transport error:", error);
1919 };
1920
1921 // Load database in background
1922 console.error("Loading JA4DB database in background...");
1923 db.loadDatabase()
1924 .then(() => {
1925 console.error(`JA4DB loaded: ${db.data.length} records`);
1926 })
1927 .catch((error) => {
1928 console.error("Warning: Failed to load JA4DB database:", error.message);
1929 console.error("Server will continue without database lookups");
1930 });
1931
1932 // Keep the process alive
1933 console.error("Server is ready and listening...");
1934
1935 // Show JA4T example
1936 console.error("\n=== JA4T TCP Fingerprinting Example ===");
1937 try {
1938 const exampleJA4T = "65535_2-4-8-1-3_1460_7";
1939 const analysis = JA4Analyzer.parseJA4T(exampleJA4T);
1940 console.error(`JA4T: ${exampleJA4T}`);
1941 console.error(
1942 `Window Size: ${analysis.breakdown.window_size.value} (${analysis.breakdown.window_size.description})`,
1943 );
1944 console.error(
1945 `TCP Options: ${analysis.breakdown.tcp_options.options.map((o) => `${o.kind}=${o.name}`).join(", ")}`,
1946 );
1947 console.error(`MSS: ${analysis.breakdown.mss.value} (${analysis.analysis.network_overhead.description})`);
1948 console.error(
1949 `Window Scale: ${analysis.breakdown.window_scale.value} (Actual window: ${analysis.analysis.actual_window_size})`,
1950 );
1951 console.error(`OS Indicators: ${analysis.analysis.os_indicators.join(", ")}`);
1952 console.error("======================================\n");
1953 } catch (error) {
1954 console.error("JA4T example error:", error.message);
1955 }
1956 } catch (error) {
1957 console.error("Fatal error during startup:", error);
1958 console.error("Stack trace:", error.stack);
1959 throw error;
1960 }
1961}
1962
1963main().catch((error) => {
1964 console.error("Fatal error in main:", error);
1965 console.error("Stack trace:", error.stack);
1966 process.exit(1);
1967});
1968
1969// Handle process events
1970process.on("SIGINT", () => {
1971 console.error("Received SIGINT, shutting down gracefully...");
1972 process.exit(0);
1973});
1974
1975process.on("SIGTERM", () => {
1976 console.error("Received SIGTERM, shutting down gracefully...");
1977 process.exit(0);
1978});
1979
1980process.on("uncaughtException", (error) => {
1981 console.error("Uncaught exception:", error);
1982 console.error("Stack trace:", error.stack);
1983 process.exit(1);
1984});
1985
1986process.on("unhandledRejection", (reason, promise) => {
1987 console.error("Unhandled rejection at:", promise, "reason:", reason);
1988 process.exit(1);
1989});