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.
This commit is contained in:
2026-04-14 02:14:46 -04:00
parent 7ff5703250
commit df3f04c10e
8 changed files with 4 additions and 254 deletions

View File

@@ -24,7 +24,7 @@ _RFC5424_RE = re.compile(
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
r"(\S+) " # 3: APP-NAME (service) r"(\S+) " # 3: APP-NAME (service)
r"\S+ " # PROCID (NILVALUE or PID) r"- " # PROCID always NILVALUE
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG 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") _IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip")
def parse_rfc5424(line: str) -> Optional[dict[str, Any]]: def parse_rfc5424(line: str) -> Optional[dict[str, Any]]:
""" """
Parse an RFC 5424 DECNET log line into a structured dict. Parse an RFC 5424 DECNET log line into a structured dict.

View File

@@ -26,7 +26,7 @@ _RFC5424_RE = re.compile(
r"(\S+) " # 1: TIMESTAMP r"(\S+) " # 1: TIMESTAMP
r"(\S+) " # 2: HOSTNAME (decky name) r"(\S+) " # 2: HOSTNAME (decky name)
r"(\S+) " # 3: APP-NAME (service) r"(\S+) " # 3: APP-NAME (service)
r"\S+ " # PROCID (NILVALUE or PID) r"- " # PROCID always NILVALUE
r"(\S+) " # 4: MSGID (event_type) r"(\S+) " # 4: MSGID (event_type)
r"(.+)$", # 5: SD element + optional MSG 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") _IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip")
@dataclass @dataclass
class LogEvent: class LogEvent:
"""A single parsed event from a DECNET syslog line.""" """A single parsed event from a DECNET syslog line."""
@@ -100,7 +99,6 @@ def parse_line(line: str) -> LogEvent | None:
return None return None
fields = _parse_sd_params(sd_rest) fields = _parse_sd_params(sd_rest)
attacker_ip = _extract_attacker_ip(fields) attacker_ip = _extract_attacker_ip(fields)
return LogEvent( return LogEvent(

View File

@@ -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 '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 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -34,8 +34,8 @@ fi
# Logging pipeline: named pipe → rsyslogd (RFC 5424) → stdout → Docker log capture # Logging pipeline: named pipe → rsyslogd (RFC 5424) → stdout → Docker log capture
mkfifo /var/run/decnet-logs mkfifo /var/run/decnet-logs
# Relay pipe through Python log_relay — normalizes sshd/bash events to DECNET format # Relay pipe to stdout so Docker captures all syslog events
python3 /opt/log_relay.py & cat /var/run/decnet-logs &
# Start rsyslog (reads /etc/rsyslog.d/99-decnet.conf, writes to the pipe above) # Start rsyslog (reads /etc/rsyslog.d/99-decnet.conf, writes to the pipe above)
rsyslogd rsyslogd

View File

@@ -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()

View File

@@ -131,14 +131,6 @@ class TestParseRfc5424:
assert result["msg"] == "login attempt" 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: class TestIsServiceContainer:
def test_known_container_returns_true(self): def test_known_container_returns_true(self):
with patch("decnet.collector.worker._load_service_container_names", return_value=_KNOWN_NAMES): with patch("decnet.collector.worker._load_service_container_names", return_value=_KNOWN_NAMES):

View File

@@ -155,19 +155,6 @@ class TestParserAttackerIP:
assert parse_line(line) is None 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 # graph.py — AttackerTraversal
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -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