fix(collector,correlation): extract attacker IP from sshd/pam free-form prose
Native sshd and pam_unix lines route through rsyslog without the relay@55555 SD wrapper and without key=value pairs, so attacker_ip fell through to "Unknown". Add a prose-IP fallback to both parsers: anchored patterns (from/rhost/client/src) win first so we never pick the local listener in "Connection from X port Y on Z port 22", with a bare-IPv4 scan as the last resort.
This commit is contained in:
@@ -93,7 +93,40 @@ class TestParseRfc5424:
|
||||
assert result["decky"] == "omega-decky"
|
||||
assert result["service"] == "sshd"
|
||||
assert "Accepted password" in result["msg"]
|
||||
assert result["attacker_ip"] == "Unknown" # no key=value in this msg
|
||||
# Native sshd lines have no key=value; the prose fallback pulls
|
||||
# the IP out of "from <ip>".
|
||||
assert result["attacker_ip"] == "192.168.1.5"
|
||||
|
||||
def test_extracts_attacker_ip_from_sshd_prose(self):
|
||||
"""sshd routed via rsyslog emits free prose with no SD block and no
|
||||
key=value pairs. The parser must still find the remote IP."""
|
||||
cases = [
|
||||
(
|
||||
"<38>1 2026-04-27T03:08:48+00:00 dmz-gateway sshd 940 - - "
|
||||
"Failed password for root from 157.66.144.16 port 42772 ssh2",
|
||||
"157.66.144.16",
|
||||
),
|
||||
(
|
||||
"<38>1 2026-04-27T03:08:45+00:00 dmz-gateway sshd 940 - - "
|
||||
"Connection from 157.66.144.16 port 42772 on 10.0.0.2 port 22 rdomain \"\"",
|
||||
"157.66.144.16", # must beat the local listener 10.0.0.2
|
||||
),
|
||||
(
|
||||
"<38>1 2026-04-27T03:08:49+00:00 dmz-gateway sshd 940 - - "
|
||||
"Connection closed by authenticating user root 157.66.144.16 port 42772 [preauth]",
|
||||
"157.66.144.16",
|
||||
),
|
||||
(
|
||||
"<38>1 2026-04-27T03:08:46+00:00 dmz-gateway sshd 940 - - "
|
||||
"pam_unix(sshd:auth): authentication failure; "
|
||||
"logname= uid=0 euid=0 tty=ssh ruser= rhost=157.66.144.16 user=root",
|
||||
"157.66.144.16",
|
||||
),
|
||||
]
|
||||
for line, expected in cases:
|
||||
result = parse_rfc5424(line)
|
||||
assert result is not None, line
|
||||
assert result["attacker_ip"] == expected, (line, result["attacker_ip"])
|
||||
|
||||
def test_extracts_attacker_ip_from_msg_body_kv(self):
|
||||
"""SSH container's bash PROMPT_COMMAND uses `logger -t bash "CMD ... src=IP ..."`
|
||||
|
||||
@@ -154,6 +154,32 @@ class TestParserAttackerIP:
|
||||
line = format_rfc5424("http", "-", "evt", SEVERITY_INFO)
|
||||
assert parse_line(line) is None
|
||||
|
||||
def test_attacker_ip_from_sshd_prose(self):
|
||||
"""sshd routed via rsyslog has no SD block — IP lives in free prose.
|
||||
Anchored "from <ip>" must beat the local listener in
|
||||
"Connection from X port Y on Z port 22"."""
|
||||
cases = [
|
||||
(
|
||||
"<38>1 2026-04-27T03:08:48+00:00 dmz-gateway sshd - - - "
|
||||
"Failed password for root from 157.66.144.16 port 42772 ssh2",
|
||||
"157.66.144.16",
|
||||
),
|
||||
(
|
||||
"<38>1 2026-04-27T03:08:45+00:00 dmz-gateway sshd - - - "
|
||||
"Connection from 157.66.144.16 port 42772 on 10.0.0.2 port 22",
|
||||
"157.66.144.16",
|
||||
),
|
||||
(
|
||||
"<38>1 2026-04-27T03:08:46+00:00 dmz-gateway sshd - - - "
|
||||
"pam_unix(sshd:auth): authentication failure; rhost=157.66.144.16 user=root",
|
||||
"157.66.144.16",
|
||||
),
|
||||
]
|
||||
for line, expected in cases:
|
||||
event = parse_line(line)
|
||||
assert event is not None, line
|
||||
assert event.attacker_ip == expected, (line, event.attacker_ip)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# graph.py — AttackerTraversal
|
||||
|
||||
Reference in New Issue
Block a user