Files
DECNET/templates/conpot/entrypoint.py
anti 5ef48d60be fix(conpot): add syslog bridge entrypoint for logging pipeline
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.
2026-04-11 03:44:41 -04:00

145 lines
4.5 KiB
Python

#!/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()