Add 20 honeypot services: email, DB, ICS, cloud, IoT, network protocols

Tier 1 (upstream images): telnet (cowrie), smtp (mailoney),
elasticsearch (elasticpot), conpot (Modbus/S7/SNMP ICS).

Tier 2 (custom asyncio honeypots): pop3, imap, mysql, mssql, redis,
mongodb, postgres, ldap, vnc, docker_api, k8s, sip, mqtt, llmnr, snmp,
tftp — each with Dockerfile, entrypoint, and protocol-accurate
handshake/credential capture.

Adds 256 pytest cases covering registration, compose fragments,
LOG_TARGET propagation, and Dockerfile presence for all 25 services.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 23:07:44 -03:00
parent 65e3ea6b08
commit e42fcab760
70 changed files with 3099 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY mysql_honeypot.py /opt/mysql_honeypot.py
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 3306
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,3 @@
#!/bin/bash
set -e
exec python3 /opt/mysql_honeypot.py

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
MySQL honeypot.
Sends a realistic MySQL 5.7 server handshake, reads the client login
packet, extracts username, then closes with Access Denied. Logs auth
attempts as JSON.
"""
import asyncio
import json
import os
import socket
import struct
from datetime import datetime, timezone
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "dbserver")
LOG_TARGET = os.environ.get("LOG_TARGET", "")
# Minimal MySQL 5.7 server greeting (protocol v10)
_GREETING = (
b"\x0a" # protocol version 10
b"5.7.38-honeypot\x00" # server version + NUL
b"\x01\x00\x00\x00" # connection id = 1
b"\x70\x76\x21\x6d\x61\x67\x69\x63" # auth-plugin-data part 1
b"\x00" # filler
b"\xff\xf7" # capability flags low
b"\x21" # charset utf8
b"\x02\x00" # status flags
b"\xff\x81" # capability flags high
b"\x15" # auth plugin data length
b"\x00" * 10 # reserved
b"\x21\x4f\x7d\x25\x3e\x55\x4d\x7c\x67\x75\x5e\x31\x00" # auth part 2
b"mysql_native_password\x00" # auth plugin name
)
def _make_packet(payload: bytes, seq: int = 0) -> bytes:
length = len(payload)
return struct.pack("<I", length)[:3] + bytes([seq]) + payload
def _forward(event: dict) -> None:
if not LOG_TARGET:
return
try:
host, port = LOG_TARGET.rsplit(":", 1)
with socket.create_connection((host, int(port)), timeout=3) as s:
s.sendall((json.dumps(event) + "\n").encode())
except Exception:
pass
def _log(event_type: str, **kwargs) -> None:
event = {
"ts": datetime.now(timezone.utc).isoformat(),
"service": "mysql",
"host": HONEYPOT_NAME,
"event": event_type,
**kwargs,
}
print(json.dumps(event), flush=True)
_forward(event)
class MySQLProtocol(asyncio.Protocol):
def __init__(self):
self._transport = None
self._peer = None
self._buf = b""
self._greeted = False
def connection_made(self, transport):
self._transport = transport
self._peer = transport.get_extra_info("peername", ("?", 0))
_log("connect", src=self._peer[0], src_port=self._peer[1])
transport.write(_make_packet(_GREETING, seq=0))
self._greeted = True
def data_received(self, data):
self._buf += data
# MySQL packets: 3-byte length + 1-byte seq + payload
while len(self._buf) >= 4:
length = struct.unpack("<I", self._buf[:3] + b"\x00")[0]
if len(self._buf) < 4 + length:
break
payload = self._buf[4:4 + length]
self._buf = self._buf[4 + length:]
self._handle_packet(payload)
def _handle_packet(self, payload: bytes):
if not payload:
return
# Login packet: capability flags (4), max_packet (4), charset (1), reserved (23), username (NUL-terminated)
if len(payload) > 32:
try:
# skip capability(4) + max_pkt(4) + charset(1) + reserved(23) = 32 bytes
username_start = 32
nul = payload.index(b"\x00", username_start)
username = payload[username_start:nul].decode(errors="replace")
except (ValueError, IndexError):
username = "<parse_error>"
_log("auth", src=self._peer[0], username=username)
# Send Access Denied error
err = b"\xff" + struct.pack("<H", 1045) + b"#28000Access denied for user\x00"
self._transport.write(_make_packet(err, seq=2))
self._transport.close()
def connection_lost(self, exc):
_log("disconnect", src=self._peer[0] if self._peer else "?")
async def main():
_log("startup", msg=f"MySQL honeypot starting as {HONEYPOT_NAME}")
loop = asyncio.get_running_loop()
server = await loop.create_server(MySQLProtocol, "0.0.0.0", 3306)
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())