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});