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

26
decnet/services/conpot.py Normal file
View File

@@ -0,0 +1,26 @@
from decnet.services.base import BaseService
class ConpotService(BaseService):
"""ICS/SCADA honeypot covering Modbus (502), SNMP (161 UDP), and HTTP (80).
Uses the official honeynet/conpot image which ships a default ICS profile
that emulates a Siemens S7-200 PLC.
"""
name = "conpot"
ports = [502, 161, 80]
default_image = "honeynet/conpot"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
return {
"image": "honeynet/conpot",
"container_name": f"{decky_name}-conpot",
"restart": "unless-stopped",
"environment": {
"CONPOT_TEMPLATE": "default",
},
}
def dockerfile_context(self):
return None

View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "docker_api"
class DockerAPIService(BaseService):
name = "docker_api"
ports = [2375, 2376]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-docker-api",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

View File

@@ -0,0 +1,23 @@
from decnet.services.base import BaseService
class ElasticsearchService(BaseService):
name = "elasticsearch"
ports = [9200]
default_image = "dtagdevsec/elasticpot"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
env: dict = {
"ELASTICPOT_HOSTNAME": decky_name,
}
if log_target:
env["ELASTICPOT_LOG_TARGET"] = log_target
return {
"image": "dtagdevsec/elasticpot",
"container_name": f"{decky_name}-elasticsearch",
"restart": "unless-stopped",
"environment": env,
}
def dockerfile_context(self):
return None

24
decnet/services/imap.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "imap"
class IMAPService(BaseService):
name = "imap"
ports = [143, 993]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-imap",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/k8s.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "k8s"
class KubernetesAPIService(BaseService):
name = "k8s"
ports = [6443, 8080]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-k8s",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

25
decnet/services/ldap.py Normal file
View File

@@ -0,0 +1,25 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "ldap"
class LDAPService(BaseService):
name = "ldap"
ports = [389, 636]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-ldap",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

31
decnet/services/llmnr.py Normal file
View File

@@ -0,0 +1,31 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "llmnr"
class LLMNRService(BaseService):
"""LLMNR/mDNS/NBNS poisoning detector.
Listens on UDP 5355 (LLMNR) and UDP 5353 (mDNS) and logs any
name-resolution queries it receives — a strong indicator of an attacker
running Responder or similar tools on the LAN.
"""
name = "llmnr"
ports = [5355, 5353]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-llmnr",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "mongodb"
class MongoDBService(BaseService):
name = "mongodb"
ports = [27017]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mongodb",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/mqtt.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "mqtt"
class MQTTService(BaseService):
name = "mqtt"
ports = [1883]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mqtt",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/mssql.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "mssql"
class MSSQLService(BaseService):
name = "mssql"
ports = [1433]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mssql",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/mysql.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "mysql"
class MySQLService(BaseService):
name = "mysql"
ports = [3306]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mysql",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/pop3.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "pop3"
class POP3Service(BaseService):
name = "pop3"
ports = [110, 995]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-pop3",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "postgres"
class PostgresService(BaseService):
name = "postgres"
ports = [5432]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-postgres",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/redis.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "redis"
class RedisService(BaseService):
name = "redis"
ports = [6379]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-redis",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/sip.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "sip"
class SIPService(BaseService):
name = "sip"
ports = [5060]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-sip",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

25
decnet/services/smtp.py Normal file
View File

@@ -0,0 +1,25 @@
from decnet.services.base import BaseService
class SMTPService(BaseService):
name = "smtp"
ports = [25, 587]
default_image = "dtagdevsec/mailoney"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
env: dict = {
"MAILONEY_HOSTNAME": decky_name,
"MAILONEY_PORTS": "25,587",
}
if log_target:
env["MAILONEY_LOG_TARGET"] = log_target
return {
"image": "dtagdevsec/mailoney",
"container_name": f"{decky_name}-smtp",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": env,
}
def dockerfile_context(self):
return None

24
decnet/services/snmp.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "snmp"
class SNMPService(BaseService):
name = "snmp"
ports = [161]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-snmp",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

31
decnet/services/telnet.py Normal file
View File

@@ -0,0 +1,31 @@
from decnet.services.base import BaseService
class TelnetService(BaseService):
name = "telnet"
ports = [23]
default_image = "cowrie/cowrie"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
env: dict = {
"COWRIE_HONEYPOT_HOSTNAME": decky_name,
"COWRIE_TELNET_ENABLED": "true",
"COWRIE_TELNET_LISTEN_ENDPOINTS": "tcp:23:interface=0.0.0.0",
# Disable SSH so this container is telnet-only
"COWRIE_SSH_ENABLED": "false",
}
if log_target:
host, port = log_target.rsplit(":", 1)
env["COWRIE_OUTPUT_TCP_ENABLED"] = "true"
env["COWRIE_OUTPUT_TCP_HOST"] = host
env["COWRIE_OUTPUT_TCP_PORT"] = port
return {
"image": "cowrie/cowrie",
"container_name": f"{decky_name}-telnet",
"restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"],
"environment": env,
}
def dockerfile_context(self):
return None

24
decnet/services/tftp.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "tftp"
class TFTPService(BaseService):
name = "tftp"
ports = [69]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-tftp",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR

24
decnet/services/vnc.py Normal file
View File

@@ -0,0 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "vnc"
class VNCService(BaseService):
name = "vnc"
ports = [5900]
default_image = "build"
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-vnc",
"restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name},
}
if log_target:
fragment["environment"]["LOG_TARGET"] = log_target
return fragment
def dockerfile_context(self) -> Path | None:
return TEMPLATES_DIR