From df3f04c10ecd0de5fe9687762f67ea935d19210a Mon Sep 17 00:00:00 2001 From: anti Date: Tue, 14 Apr 2026 02:14:46 -0400 Subject: [PATCH] revert: undo service badge filter, parser normalization, and SSH relay Reverts commits 8c249f6, a6c7cfd, 7ff5703. The SSH log relay approach requires container redeployment and doesn't retroactively fix existing attacker profiles. Rolling back to reassess the approach. --- decnet/collector/worker.py | 4 +- decnet/correlation/parser.py | 4 +- templates/ssh/Dockerfile | 2 - templates/ssh/entrypoint.sh | 4 +- templates/ssh/log_relay.py | 106 ------------------------------- tests/test_collector.py | 8 --- tests/test_correlation.py | 13 ---- tests/test_ssh_log_relay.py | 117 ----------------------------------- 8 files changed, 4 insertions(+), 254 deletions(-) delete mode 100644 templates/ssh/log_relay.py delete mode 100644 tests/test_ssh_log_relay.py diff --git a/decnet/collector/worker.py b/decnet/collector/worker.py index 4dc7b8a..d96ed4f 100644 --- a/decnet/collector/worker.py +++ b/decnet/collector/worker.py @@ -24,7 +24,7 @@ _RFC5424_RE = re.compile( r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 3: APP-NAME (service) - r"\S+ " # PROCID (NILVALUE or PID) + r"- " # PROCID always NILVALUE r"(\S+) " # 4: MSGID (event_type) r"(.+)$", # 5: SD element + optional MSG ) @@ -33,8 +33,6 @@ _PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") - - def parse_rfc5424(line: str) -> Optional[dict[str, Any]]: """ Parse an RFC 5424 DECNET log line into a structured dict. diff --git a/decnet/correlation/parser.py b/decnet/correlation/parser.py index 9fa7420..e457254 100644 --- a/decnet/correlation/parser.py +++ b/decnet/correlation/parser.py @@ -26,7 +26,7 @@ _RFC5424_RE = re.compile( r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 3: APP-NAME (service) - r"\S+ " # PROCID (NILVALUE or PID) + r"- " # PROCID always NILVALUE r"(\S+) " # 4: MSGID (event_type) r"(.+)$", # 5: SD element + optional MSG ) @@ -41,7 +41,6 @@ _PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip") - @dataclass class LogEvent: """A single parsed event from a DECNET syslog line.""" @@ -100,7 +99,6 @@ def parse_line(line: str) -> LogEvent | None: return None fields = _parse_sd_params(sd_rest) - attacker_ip = _extract_attacker_ip(fields) return LogEvent( diff --git a/templates/ssh/Dockerfile b/templates/ssh/Dockerfile index 5eeacc4..230d429 100644 --- a/templates/ssh/Dockerfile +++ b/templates/ssh/Dockerfile @@ -65,8 +65,6 @@ RUN mkdir -p /root/projects /root/backups /var/www/html && \ printf 'DB_HOST=10.0.0.5\nDB_USER=admin\nDB_PASS=changeme123\nDB_NAME=prod_db\n' > /root/projects/.env && \ printf '[Unit]\nDescription=App Server\n[Service]\nExecStart=/usr/bin/python3 /opt/app/server.py\n' > /root/projects/app.service -COPY decnet_logging.py /opt/decnet_logging.py -COPY log_relay.py /opt/log_relay.py COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/templates/ssh/entrypoint.sh b/templates/ssh/entrypoint.sh index b9090fe..c5c8291 100644 --- a/templates/ssh/entrypoint.sh +++ b/templates/ssh/entrypoint.sh @@ -34,8 +34,8 @@ fi # Logging pipeline: named pipe → rsyslogd (RFC 5424) → stdout → Docker log capture mkfifo /var/run/decnet-logs -# Relay pipe through Python log_relay — normalizes sshd/bash events to DECNET format -python3 /opt/log_relay.py & +# Relay pipe to stdout so Docker captures all syslog events +cat /var/run/decnet-logs & # Start rsyslog (reads /etc/rsyslog.d/99-decnet.conf, writes to the pipe above) rsyslogd diff --git a/templates/ssh/log_relay.py b/templates/ssh/log_relay.py deleted file mode 100644 index 5fefb00..0000000 --- a/templates/ssh/log_relay.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -""" -SSH log relay — reads rsyslog output from the named pipe and re-emits -matched sshd/bash events as proper DECNET RFC 5424 syslog lines to stdout. - -Matched events: - - Accepted password (login_success) - - Connection closed (connection_closed) - - Disconnected from user (disconnect) - - Session opened (session_opened) - - bash CMD (command) -""" - -import os -import re -import sys - -from decnet_logging import syslog_line, write_syslog_file, SEVERITY_INFO, SEVERITY_WARNING - -NODE_NAME = os.environ.get("NODE_NAME", "ssh-decky") -SERVICE = "ssh" - -# sshd patterns -_ACCEPTED_RE = re.compile( - r"Accepted (\S+) for (\S+) from (\S+) port (\d+)" -) -_SESSION_RE = re.compile( - r"session opened for user (\S+?)(?:\(uid=\d+\))? by" -) -_DISCONNECTED_RE = re.compile( - r"Disconnected from user (\S+) (\S+) port (\d+)" -) -_CONN_CLOSED_RE = re.compile( - r"Connection closed by (\S+) port (\d+)" -) - -# bash PROMPT_COMMAND pattern -_BASH_CMD_RE = re.compile( - r"CMD\s+uid=(\S+)\s+pwd=(\S+)\s+cmd=(.*)" -) - - -def _handle_line(line: str) -> None: - """Parse a raw rsyslog line and emit a DECNET syslog line if it matches.""" - - # --- Accepted password --- - m = _ACCEPTED_RE.search(line) - if m: - method, user, src_ip, port = m.groups() - write_syslog_file(syslog_line( - SERVICE, NODE_NAME, "login_success", SEVERITY_WARNING, - src_ip=src_ip, username=user, auth_method=method, src_port=port, - )) - return - - # --- Session opened --- - m = _SESSION_RE.search(line) - if m: - user = m.group(1) - write_syslog_file(syslog_line( - SERVICE, NODE_NAME, "session_opened", SEVERITY_INFO, - username=user, - )) - return - - # --- Disconnected from user --- - m = _DISCONNECTED_RE.search(line) - if m: - user, src_ip, port = m.groups() - write_syslog_file(syslog_line( - SERVICE, NODE_NAME, "disconnect", SEVERITY_INFO, - src_ip=src_ip, username=user, src_port=port, - )) - return - - # --- Connection closed --- - m = _CONN_CLOSED_RE.search(line) - if m: - src_ip, port = m.groups() - write_syslog_file(syslog_line( - SERVICE, NODE_NAME, "connection_closed", SEVERITY_INFO, - src_ip=src_ip, src_port=port, - )) - return - - # --- bash CMD --- - m = _BASH_CMD_RE.search(line) - if m: - uid, pwd, cmd = m.groups() - write_syslog_file(syslog_line( - SERVICE, NODE_NAME, "command", SEVERITY_INFO, - uid=uid, pwd=pwd, command=cmd, - )) - return - - -def main() -> None: - pipe_path = "/var/run/decnet-logs" - while True: - with open(pipe_path, "r") as pipe: - for line in pipe: - _handle_line(line.rstrip("\n")) - - -if __name__ == "__main__": - main() diff --git a/tests/test_collector.py b/tests/test_collector.py index ca99e4c..d43f2e3 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -131,14 +131,6 @@ class TestParseRfc5424: assert result["msg"] == "login attempt" - def test_non_nil_procid_accepted(self): - line = '<38>1 2026-04-14T05:48:12.611006+00:00 SRV-BRAVO-13 sshd 282 - - Accepted password for root from 192.168.1.5 port 50854 ssh2' - result = parse_rfc5424(line) - assert result is not None - assert result["service"] == "sshd" - assert result["decky"] == "SRV-BRAVO-13" - - class TestIsServiceContainer: def test_known_container_returns_true(self): with patch("decnet.collector.worker._load_service_container_names", return_value=_KNOWN_NAMES): diff --git a/tests/test_correlation.py b/tests/test_correlation.py index cb186e1..7764ec8 100644 --- a/tests/test_correlation.py +++ b/tests/test_correlation.py @@ -155,19 +155,6 @@ class TestParserAttackerIP: assert parse_line(line) is None -class TestParserProcidFlexibility: - def test_non_nil_procid_accepted(self): - line = '<38>1 2026-04-14T05:48:12.611006+00:00 SRV-BRAVO-13 sshd 282 - - Accepted password for root' - event = parse_line(line) - assert event is not None - assert event.service == "sshd" - assert event.decky == "SRV-BRAVO-13" - - def test_nil_procid_still_works(self): - event = parse_line(_make_line()) - assert event is not None - - # --------------------------------------------------------------------------- # graph.py — AttackerTraversal # --------------------------------------------------------------------------- diff --git a/tests/test_ssh_log_relay.py b/tests/test_ssh_log_relay.py deleted file mode 100644 index 7745ab1..0000000 --- a/tests/test_ssh_log_relay.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Tests for the SSH log relay that normalizes sshd/bash events.""" - -import os -import sys -import types -from pathlib import Path - -import pytest - -_SSH_TPL = str(Path(__file__).resolve().parent.parent / "templates" / "ssh") - - -def _load_relay(): - """Import log_relay with a real decnet_logging from the SSH template dir.""" - # Clear any stale stubs - for mod_name in ("decnet_logging", "log_relay"): - sys.modules.pop(mod_name, None) - - if _SSH_TPL not in sys.path: - sys.path.insert(0, _SSH_TPL) - - import log_relay - return log_relay - - -_relay = _load_relay() - - -def _capture(line: str) -> str | None: - """Run _handle_line, collect output via monkey-patched write_syslog_file.""" - collected: list[str] = [] - original = _relay.write_syslog_file - _relay.write_syslog_file = lambda s: collected.append(s) - try: - _relay._handle_line(line) - finally: - _relay.write_syslog_file = original - return collected[0] if collected else None - - -class TestSshdAcceptedPassword: - def test_accepted_password_emits_login_success(self): - emitted = _capture( - '<38>1 2026-04-14T05:48:12.611006+00:00 SRV-BRAVO-13 sshd 282 - - Accepted password for root from 192.168.1.5 port 50854 ssh2' - ) - assert emitted is not None - assert "login_success" in emitted - assert 'src_ip="192.168.1.5"' in emitted - assert 'username="root"' in emitted - assert 'auth_method="password"' in emitted - - def test_accepted_publickey(self): - emitted = _capture( - '<38>1 2026-04-14T05:48:12.611006+00:00 SRV-BRAVO-13 sshd 282 - - Accepted publickey for admin from 10.0.0.1 port 12345 ssh2' - ) - assert emitted is not None - assert 'auth_method="publickey"' in emitted - assert 'username="admin"' in emitted - - -class TestSshdSessionOpened: - def test_session_opened(self): - emitted = _capture( - '<86>1 2026-04-14T05:48:12.611880+00:00 SRV-BRAVO-13 sshd 282 - - pam_unix(sshd:session): session opened for user root(uid=0) by (uid=0)' - ) - assert emitted is not None - assert "session_opened" in emitted - assert 'username="root"' in emitted - - -class TestSshdDisconnected: - def test_disconnected(self): - emitted = _capture( - '<38>1 2026-04-14T05:54:50.710536+00:00 SRV-BRAVO-13 sshd 282 - - Disconnected from user root 192.168.1.5 port 50854' - ) - assert emitted is not None - assert "disconnect" in emitted - assert 'src_ip="192.168.1.5"' in emitted - assert 'username="root"' in emitted - - -class TestSshdConnectionClosed: - def test_connection_closed(self): - emitted = _capture( - '<38>1 2026-04-14T05:47:55.621236+00:00 SRV-BRAVO-13 sshd 280 - - Connection closed by 192.168.1.5 port 52900 [preauth]' - ) - assert emitted is not None - assert "connection_closed" in emitted - assert 'src_ip="192.168.1.5"' in emitted - - -class TestBashCommand: - def test_bash_cmd(self): - emitted = _capture( - '<14>1 2026-04-14T05:48:12.628417+00:00 SRV-BRAVO-13 bash - - - CMD uid=0 pwd=/root cmd=ls /var/www/html' - ) - assert emitted is not None - assert "command" in emitted - assert 'command="ls /var/www/html"' in emitted - - def test_bash_cmd_with_pipes(self): - emitted = _capture( - '<14>1 2026-04-14T05:48:32.006502+00:00 SRV-BRAVO-13 bash - - - CMD uid=0 pwd=/root cmd=cat /etc/passwd | grep root' - ) - assert emitted is not None - assert "cat /etc/passwd | grep root" in emitted - - -class TestUnmatchedLines: - def test_pam_env_ignored(self): - assert _capture('<83>1 2026-04-14T05:48:12.615198+00:00 SRV-BRAVO-13 sshd 282 - - pam_env(sshd:session): Unable to open env file') is None - - def test_session_closed_ignored(self): - assert _capture('<86>1 2026-04-14T05:54:50.710577+00:00 SRV-BRAVO-13 sshd 282 - - pam_unix(sshd:session): session closed for user root') is None - - def test_syslogin_ignored(self): - assert _capture('<38>1 2026-04-14T05:54:50.710307+00:00 SRV-BRAVO-13 sshd 282 - - syslogin_perform_logout: logout() returned an error') is None