From 5ef48d60becd4cd8bbd5cffe8546ce0e79e63d0d Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 11 Apr 2026 03:44:41 -0400 Subject: [PATCH] fix(conpot): add syslog bridge entrypoint for logging pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conpot is a third-party app with its own Python logger — it never calls decnet_logging. Added entrypoint.py as a subprocess wrapper that: - Launches conpot and captures its stdout/stderr - Classifies each line (startup/request/warning/error/log) - Extracts source IPs via regex - Emits RFC 5424 syslog lines to stdout for Docker/collector pickup Entrypoint is self-contained (no import of shared decnet_logging.py) because the conpot base image runs Python 3.6, which cannot parse the dict[str, Any] / str | None type syntax used in the canonical file. --- templates/conpot/Dockerfile | 18 ++++- templates/conpot/entrypoint.py | 144 +++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 templates/conpot/entrypoint.py diff --git a/templates/conpot/Dockerfile b/templates/conpot/Dockerfile index 3cca519..6bfad6d 100644 --- a/templates/conpot/Dockerfile +++ b/templates/conpot/Dockerfile @@ -11,8 +11,18 @@ RUN find /opt /usr /etc /home -name "*.xml" -exec sed -i 's/port="5020"/port="50 RUN (apt-get update && apt-get install -y --no-install-recommends libcap2-bin 2>/dev/null) || (apk add --no-cache libcap 2>/dev/null) || true RUN find /home/conpot/.local/bin /usr /opt -type f -name 'python*' -exec setcap 'cap_net_bind_service+eip' {} \; 2>/dev/null || true -# The upstream image already runs as the non-root 'conpot' user. -# We do NOT switch to a 'decnet' user here — doing so breaks pkg_resources -# because conpot's eggs live under /home/conpot/.local and are only on the -# Python path when the interpreter runs as 'conpot'. +# Bridge conpot's own logger into DECNET's RFC 5424 syslog pipeline. +# entrypoint.py is self-contained (inlines the formatter) because the +# conpot base image runs Python 3.6, which cannot import the shared +# decnet_logging.py (that file uses 3.9+ / 3.10+ type syntax). +COPY entrypoint.py /home/conpot/entrypoint.py +RUN chown conpot:conpot /home/conpot/entrypoint.py \ + && chmod +x /home/conpot/entrypoint.py + +# The upstream image already runs as non-root 'conpot'. +# We do NOT switch to a 'decnet' user — doing so breaks pkg_resources +# because conpot's eggs live under /home/conpot/.local and are only on +# the Python path for that user. USER conpot + +ENTRYPOINT ["/usr/bin/python3", "/home/conpot/entrypoint.py"] diff --git a/templates/conpot/entrypoint.py b/templates/conpot/entrypoint.py new file mode 100644 index 0000000..534eeb0 --- /dev/null +++ b/templates/conpot/entrypoint.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +Entrypoint wrapper for the Conpot ICS/SCADA honeypot. + +Launches conpot as a child process and bridges its log output into the +DECNET structured syslog pipeline. Each line from conpot stdout/stderr +is classified and emitted as an RFC 5424 syslog line so the host-side +collector can ingest it alongside every other service. + +Written to be compatible with Python 3.6 (the conpot base image version). +""" +from __future__ import print_function + +import os +import re +import signal +import subprocess +import sys +from datetime import datetime, timezone + +# ── RFC 5424 inline formatter (Python 3.6-compatible) ───────────────────────── + +_FACILITY_LOCAL0 = 16 +_SD_ID = "decnet@55555" +_NILVALUE = "-" + +SEVERITY_INFO = 6 +SEVERITY_WARNING = 4 +SEVERITY_ERROR = 3 + + +def _sd_escape(value): + return value.replace("\\", "\\\\").replace('"', '\\"').replace("]", "\\]") + + +def _syslog_line(event_type, severity=SEVERITY_INFO, **fields): + pri = "<{}>".format(_FACILITY_LOCAL0 * 8 + severity) + ts = datetime.now(timezone.utc).isoformat() + host = NODE_NAME[:255] + appname = "conpot" + msgid = event_type[:32] + + if fields: + params = " ".join('{}="{}"'.format(k, _sd_escape(str(v))) for k, v in fields.items()) + sd = "[{} {}]".format(_SD_ID, params) + else: + sd = _NILVALUE + + return "{pri}1 {ts} {host} {appname} {nil} {msgid} {sd}".format( + pri=pri, ts=ts, host=host, appname=appname, + nil=_NILVALUE, msgid=msgid, sd=sd, + ) + + +def _log(event_type, severity=SEVERITY_INFO, **fields): + print(_syslog_line(event_type, severity, **fields), flush=True) + + +# ── Config ──────────────────────────────────────────────────────────────────── + +NODE_NAME = os.environ.get("NODE_NAME", "conpot-node") +TEMPLATE = os.environ.get("CONPOT_TEMPLATE", "default") + +_CONPOT_CMD = [ + "/home/conpot/.local/bin/conpot", + "--template", TEMPLATE, + "--logfile", "/var/log/conpot/conpot.log", + "-f", + "--temp_dir", "/tmp", +] + +# Grab the first routable IPv4 address from a log line +_IP_RE = re.compile(r"\b((?!127\.)(?!0\.)(?!255\.)\d{1,3}(?:\.\d{1,3}){3})\b") + +_REQUEST_RE = re.compile( + r"request|recv|received|connect|session|query|command|" + r"modbus|snmp|http|s7comm|bacnet|enip", + re.IGNORECASE, +) +_ERROR_RE = re.compile(r"error|exception|traceback|critical|fail", re.IGNORECASE) +_WARN_RE = re.compile(r"warning|warn", re.IGNORECASE) +_STARTUP_RE = re.compile( + r"starting|started|listening|server|initializ|template|conpot", + re.IGNORECASE, +) + + +# ── Classifier ──────────────────────────────────────────────────────────────── + +def _classify(raw): + """Return (event_type, severity, fields) for one conpot log line.""" + fields = {} + + m = _IP_RE.search(raw) + if m: + fields["src"] = m.group(1) + + fields["msg"] = raw[:300] + + if _ERROR_RE.search(raw): + return "error", SEVERITY_ERROR, fields + if _WARN_RE.search(raw): + return "warning", SEVERITY_WARNING, fields + if _REQUEST_RE.search(raw): + return "request", SEVERITY_INFO, fields + if _STARTUP_RE.search(raw): + return "startup", SEVERITY_INFO, fields + return "log", SEVERITY_INFO, fields + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + _log("startup", msg="Conpot ICS honeypot starting (template={})".format(TEMPLATE)) + + proc = subprocess.Popen( + _CONPOT_CMD, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + ) + + def _forward(sig, _frame): + proc.send_signal(sig) + + signal.signal(signal.SIGTERM, _forward) + signal.signal(signal.SIGINT, _forward) + + try: + for raw_line in proc.stdout: + line = raw_line.rstrip() + if not line: + continue + event_type, severity, fields = _classify(line) + _log(event_type, severity, **fields) + finally: + proc.wait() + _log("shutdown", msg="Conpot ICS honeypot stopped") + sys.exit(proc.returncode) + + +if __name__ == "__main__": + main()