The docker build contexts and syslog_bridge.py lived at repo root, which meant setuptools (include = ["decnet*"]) never shipped them. Agents installed via `pip install $RELEASE_DIR` got site-packages/decnet/** but no templates/, so every deploy blew up in deployer._sync_logging_helper with FileNotFoundError on templates/syslog_bridge.py. Move templates/ -> decnet/templates/ and declare it as setuptools package-data. Path resolutions in services/*.py and engine/deployer.py drop one .parent since templates now lives beside the code. Test fixtures, bandit exclude path, and coverage omit glob updated to match.
145 lines
4.5 KiB
Python
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
|
|
syslog-relay 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 = "relay@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()
|