feat(sniffer): capture SSH client banner from TCP stream
Parse RFC 4253 §4.2 identification strings from the first attacker→decky data segment on TCP/22; emit ssh_client_banner syslog events and bus fan-out. Profiler's sniffer_rollup dedupes observed banners into a new AttackerBehavior.ssh_client_banners JSON column. Closes gap #3 from SIGNAL_CAPTURE_AUDIT.md.
This commit is contained in:
@@ -182,6 +182,13 @@ class AttackerBehavior(SQLModel, table=True):
|
||||
default=None,
|
||||
sa_column=Column("kex_order_raw", Text, nullable=True),
|
||||
) # JSON list[str] — kex_algorithms comma-separated strings
|
||||
# Sniffer-observed SSH client identification strings (RFC 4253 §4.2),
|
||||
# deduped in observation order. Captures the attacker's SSH client
|
||||
# software (e.g. "SSH-2.0-OpenSSH_9.2p1", "SSH-2.0-libssh2_1.10.0").
|
||||
ssh_client_banners: Optional[str] = Field(
|
||||
default=None,
|
||||
sa_column=Column("ssh_client_banners", Text, nullable=True),
|
||||
) # JSON list[str]
|
||||
retransmit_count: int = Field(default=0)
|
||||
# Behavioral (derived by the profiler from log-event timing)
|
||||
behavior_class: Optional[str] = None # beaconing | interactive | scanning | brute_force | slow_scan | mixed | unknown
|
||||
|
||||
@@ -683,6 +683,16 @@ class SQLModelRepository(BaseRepository):
|
||||
d["kex_order_raw"] = []
|
||||
elif raw_kex is None:
|
||||
d["kex_order_raw"] = []
|
||||
# Same list-or-None pattern for ssh_client_banners.
|
||||
raw_banners = d.get("ssh_client_banners")
|
||||
if isinstance(raw_banners, str):
|
||||
try:
|
||||
parsed_banners = json.loads(raw_banners)
|
||||
d["ssh_client_banners"] = parsed_banners if isinstance(parsed_banners, list) else [parsed_banners]
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
d["ssh_client_banners"] = []
|
||||
elif raw_banners is None:
|
||||
d["ssh_client_banners"] = []
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user