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:
26
decnet/services/conpot.py
Normal file
26
decnet/services/conpot.py
Normal 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
|
||||
24
decnet/services/docker_api.py
Normal file
24
decnet/services/docker_api.py
Normal 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
|
||||
23
decnet/services/elasticsearch.py
Normal file
23
decnet/services/elasticsearch.py
Normal 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
24
decnet/services/imap.py
Normal 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
24
decnet/services/k8s.py
Normal 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
25
decnet/services/ldap.py
Normal 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
31
decnet/services/llmnr.py
Normal 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
|
||||
24
decnet/services/mongodb.py
Normal file
24
decnet/services/mongodb.py
Normal 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
24
decnet/services/mqtt.py
Normal 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
24
decnet/services/mssql.py
Normal 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
24
decnet/services/mysql.py
Normal 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
24
decnet/services/pop3.py
Normal 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
|
||||
24
decnet/services/postgres.py
Normal file
24
decnet/services/postgres.py
Normal 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
24
decnet/services/redis.py
Normal 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
24
decnet/services/sip.py
Normal 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
25
decnet/services/smtp.py
Normal 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
24
decnet/services/snmp.py
Normal 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
31
decnet/services/telnet.py
Normal 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
24
decnet/services/tftp.py
Normal 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
24
decnet/services/vnc.py
Normal 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
|
||||
Reference in New Issue
Block a user