File size: 2,301 Bytes
5d97275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
"""Adapters: other inputs seen as the streams the engine understands.

JSON-RPC (chain nodes, and any JSON-RPC API): one call -> one canonical request text for the HTTP tiers
('post /rpc | m:<method> | p:<params...>') plus the flow-like event the entity stream takes (client -> node, 'port' =
method hash, 'no reply' = error response or expensive method), so RPC floods, method enumeration and spam show up
as behaviour per client without any chain-specific model.
"""
from __future__ import annotations

import json
import zlib

from dibaguard.http import canon
from dibaguard.shield.playbook import EXPENSIVE_RPC, match_rpc


def rpc_text(body: str | bytes, path: str = "/rpc") -> tuple[str, int, list[str]]:
    """-> (canonical text, deepest decoding layer, playbook ids)"""
    try:
        obj = json.loads(body)
    except (ValueError, TypeError):
        text, deep = canon.canon(body if isinstance(body, str) else body.decode("utf-8", "replace"))
        return f"post {path} | b:{text[:4096]}", deep, []
    calls = obj if isinstance(obj, list) else [obj]
    parts, ids, deepest = [], [], 0
    for c in calls[:50]:
        if not isinstance(c, dict):
            continue
        m = str(c.get("method", ""))
        ids += match_rpc(m)
        p = c.get("params", [])
        ptxt, deep = canon.canon(json.dumps(p, ensure_ascii=False)[:2048])
        deepest = max(deepest, deep)
        parts.append(f"m:{m.lower()} | p:{ptxt}")
    text = f"post {path} | " + " | ".join(parts) + (f" | batch:{len(calls)}" if len(calls) > 1 else "")
    return text[:8192], deepest, sorted(set(ids))


def rpc_event(method: str, client: str, node: str, t: float, request_bytes: int, error: bool) -> dict:
    """one flow-like event for Guard.flows(): the method is the 'service', an error or an expensive method is 'no reply'"""
    m = method.lower()
    expensive = any(m.startswith(e) for e in EXPENSIVE_RPC)
    port = 1024 + (zlib.crc32(m.encode()) % 60000)
    return {"t": t, "src": client, "dst": node, "dport": port, "proto": 6, "bytes_in": float(request_bytes), "pkts_in": 1.0,
            "bytes_out": 0.0 if (error or expensive) else 200.0, "pkts_out": 0.0 if (error or expensive) else 1.0,
            "client_flags": 27, "server_flags": 0 if error else 27, "flags": 27, "duration_ms": 5.0}