feat(pr2): HTTP/2+HTTP/3 fingerprint extractors — JA4H, H2 SETTINGS, JA4-QUIC

This commit is contained in:
2026-05-10 00:47:19 -04:00
parent 0653e500b5
commit 92632d7afd
25 changed files with 1885 additions and 48 deletions

View File

@@ -180,6 +180,15 @@ class AttackerIdentity(SQLModel, table=True):
hassh_hashes: Optional[str] = Field(
default=None, sa_column=Column("hassh_hashes", Text, nullable=True)
)
ja4h_hashes: Optional[str] = Field(
default=None, sa_column=Column("ja4h_hashes", Text, nullable=True)
)
ja4_quic_hashes: Optional[str] = Field(
default=None, sa_column=Column("ja4_quic_hashes", Text, nullable=True)
)
http_versions_seen: Optional[str] = Field(
default=None, sa_column=Column("http_versions_seen", Text, nullable=True)
)
# JSON list[str] — SHA-256 fingerprints of leaf certs presented by
# attacker-run TLS servers, captured by the active prober alongside
# JARM. Same federation-gossip rationale as ja3_hashes/hassh_hashes:

View File

@@ -89,6 +89,15 @@ class CanaryFingerprintEvidence(TypedDict):
matched_signature: str # signature ID, not raw fingerprint blob
class HttpFingerprintEvidence(TypedDict):
kind: str # "ja4h" | "h2_settings" | "h3_settings" | "ja4_quic"
hash: str # fingerprint hash string (or empty for settings events)
protocol: str # "h1" | "h2" | "h2c" | "h3"
client_ip: str
seen_at: str # ISO8601 UTC
raw: Optional[dict] # raw settings dict for h2_settings / h3_settings
# ── Tables ──────────────────────────────────────────────────────────

View File

@@ -626,6 +626,56 @@ async def _extract_bounty(
if log_data.get("service") == "smtp_relay":
await _publish_probe_pending(log_data, _fields)
# 13. JA4H HTTP-layer fingerprint (from http/https templates via fp socket)
_ja4h = _fields.get("ja4h")
if _ja4h and log_data.get("event_type") == "http_request_fingerprint":
await repo.add_bounty({
"decky": log_data.get("decky"),
"service": log_data.get("service"),
"attacker_ip": log_data.get("attacker_ip"),
"bounty_type": "fingerprint",
"payload": {
"fingerprint_type": "ja4h",
"ja4h": _ja4h,
"protocol": _fields.get("protocol", "h1"),
"method": _fields.get("method"),
"path": _fields.get("path"),
},
})
# 14. H2/H3 SETTINGS frame fingerprint (from Caddy fp module)
_evt_type = log_data.get("event_type", "")
if _evt_type in ("http2_settings", "http3_settings"):
await repo.add_bounty({
"decky": log_data.get("decky"),
"service": log_data.get("service"),
"attacker_ip": log_data.get("attacker_ip"),
"bounty_type": "fingerprint",
"payload": {
"fingerprint_type": _evt_type,
"settings": _fields.get("settings"),
"frame_order": _fields.get("frame_order"),
"protocol": "h2" if _evt_type == "http2_settings" else "h3",
},
})
# 15. JA4-QUIC fingerprint from fleet-wide sniffer (UDP/443)
_ja4q = _fields.get("ja4_quic")
if _ja4q and log_data.get("event_type") == "quic_client_hello":
await repo.add_bounty({
"decky": log_data.get("decky"),
"service": log_data.get("service", "sniffer"),
"attacker_ip": log_data.get("attacker_ip"),
"bounty_type": "fingerprint",
"payload": {
"fingerprint_type": "ja4_quic",
"ja4_quic": _ja4q,
"sni": _fields.get("sni") or None,
"alpn": _fields.get("alpn") or None,
"raw_ciphers": _fields.get("raw_ciphers"),
},
})
_RCPT_SPLIT_RE = re.compile(r"[,\s]+")
_ADDR_AT_RE = re.compile(r"@([A-Za-z0-9.\-]+)")