Wire all 25 services into --randomize-services and add heterogeneous OS fingerprints
- Replace hardcoded ALL_SERVICE_NAMES=[5 services] in cli.py with
_all_service_names() pulling dynamically from the plugin registry;
randomize-services now draws from all 25 registered honeypots
- Add build_base field to DistroProfile: apt-compatible image for service
Dockerfiles (ubuntu22/ubuntu20/kali get their own; others fall back to
debian:bookworm-slim since Dockerfiles use apt-get)
- Add build_base to DeckyConfig; propagate from distro in _build_deckies
and _build_deckies_from_ini
- Inject BASE_IMAGE build arg in composer.py for every build-based service
so each decky's containers reflect its assigned distro
- Update all 21 service Dockerfiles: FROM debian:bookworm-slim →
ARG BASE_IMAGE=debian:bookworm-slim / FROM ${BASE_IMAGE}
- Add tests/test_cli_service_pool.py and tests/test_composer.py (306 total)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
81
tests/test_cli_service_pool.py
Normal file
81
tests/test_cli_service_pool.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Tests for the CLI service pool — verifies that --randomize-services draws
|
||||
from all registered services, not just the original hardcoded 5.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from decnet.cli import _all_service_names, _build_deckies
|
||||
from decnet.services.registry import all_services
|
||||
|
||||
|
||||
ORIGINAL_5 = {"ssh", "smb", "rdp", "http", "ftp"}
|
||||
|
||||
|
||||
def test_all_service_names_covers_full_registry():
|
||||
"""_all_service_names() must return every service in the registry."""
|
||||
pool = set(_all_service_names())
|
||||
registry = set(all_services().keys())
|
||||
assert pool == registry
|
||||
|
||||
|
||||
def test_all_service_names_is_sorted():
|
||||
names = _all_service_names()
|
||||
assert names == sorted(names)
|
||||
|
||||
|
||||
def test_all_service_names_includes_at_least_25():
|
||||
assert len(_all_service_names()) >= 25
|
||||
|
||||
|
||||
def test_all_service_names_includes_all_original_5():
|
||||
pool = set(_all_service_names())
|
||||
assert ORIGINAL_5.issubset(pool)
|
||||
|
||||
|
||||
def test_randomize_services_pool_exceeds_original_5():
|
||||
"""
|
||||
After enough random draws, at least one service outside the original 5 must appear.
|
||||
With 25 services and picking 1-3 at a time, 200 draws makes this ~100% certain.
|
||||
"""
|
||||
all_drawn: set[str] = set()
|
||||
for _ in range(200):
|
||||
deckies = _build_deckies(
|
||||
n=1,
|
||||
ips=["10.0.0.10"],
|
||||
services_explicit=None,
|
||||
randomize_services=True,
|
||||
)
|
||||
all_drawn.update(deckies[0].services)
|
||||
|
||||
beyond_original = all_drawn - ORIGINAL_5
|
||||
assert beyond_original, (
|
||||
f"After 200 draws only saw the original 5 services. "
|
||||
f"All drawn: {sorted(all_drawn)}"
|
||||
)
|
||||
|
||||
|
||||
def test_build_deckies_randomize_services_valid():
|
||||
"""All randomly chosen services must exist in the registry."""
|
||||
registry = set(all_services().keys())
|
||||
for _ in range(50):
|
||||
deckies = _build_deckies(
|
||||
n=3,
|
||||
ips=["10.0.0.10", "10.0.0.11", "10.0.0.12"],
|
||||
services_explicit=None,
|
||||
randomize_services=True,
|
||||
)
|
||||
for decky in deckies:
|
||||
unknown = set(decky.services) - registry
|
||||
assert not unknown, f"Decky {decky.name} got unknown services: {unknown}"
|
||||
|
||||
|
||||
def test_build_deckies_explicit_services_unchanged():
|
||||
"""Explicit service list must pass through untouched."""
|
||||
deckies = _build_deckies(
|
||||
n=2,
|
||||
ips=["10.0.0.10", "10.0.0.11"],
|
||||
services_explicit=["ssh", "ftp"],
|
||||
randomize_services=False,
|
||||
)
|
||||
for decky in deckies:
|
||||
assert decky.services == ["ssh", "ftp"]
|
||||
163
tests/test_composer.py
Normal file
163
tests/test_composer.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Tests for the composer — verifies BASE_IMAGE injection and distro heterogeneity.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from decnet.config import DeckyConfig, DecnetConfig
|
||||
from decnet.composer import generate_compose
|
||||
from decnet.distros import all_distros, DISTROS
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
APT_COMPATIBLE = {
|
||||
"debian:bookworm-slim",
|
||||
"ubuntu:22.04",
|
||||
"ubuntu:20.04",
|
||||
"kalilinux/kali-rolling",
|
||||
}
|
||||
|
||||
BUILD_SERVICES = [
|
||||
"http", "rdp", "smb", "ftp", "pop3", "imap",
|
||||
"mysql", "mssql", "redis", "mongodb", "postgres",
|
||||
"ldap", "vnc", "docker_api", "k8s", "sip",
|
||||
"mqtt", "llmnr", "snmp", "tftp",
|
||||
]
|
||||
|
||||
UPSTREAM_SERVICES = ["ssh", "telnet", "smtp", "elasticsearch", "conpot"]
|
||||
|
||||
|
||||
def _make_config(services, distro="debian", base_image=None, build_base=None):
|
||||
profile = DISTROS[distro]
|
||||
decky = DeckyConfig(
|
||||
name="decky-01",
|
||||
ip="10.0.0.10",
|
||||
services=services,
|
||||
distro=distro,
|
||||
base_image=base_image or profile.image,
|
||||
build_base=build_base or profile.build_base,
|
||||
hostname="test-host",
|
||||
)
|
||||
return DecnetConfig(
|
||||
mode="unihost",
|
||||
interface="eth0",
|
||||
subnet="10.0.0.0/24",
|
||||
gateway="10.0.0.1",
|
||||
deckies=[decky],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BASE_IMAGE injection — build services
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("svc", BUILD_SERVICES)
|
||||
def test_build_service_gets_base_image_arg(svc):
|
||||
"""Every build service must have BASE_IMAGE injected in compose args."""
|
||||
config = _make_config([svc], distro="debian")
|
||||
compose = generate_compose(config)
|
||||
key = f"decky-01-{svc}"
|
||||
fragment = compose["services"][key]
|
||||
assert "build" in fragment, f"{svc}: missing 'build' key"
|
||||
assert "args" in fragment["build"], f"{svc}: build section missing 'args'"
|
||||
assert "BASE_IMAGE" in fragment["build"]["args"], f"{svc}: BASE_IMAGE not in args"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("distro,expected_build_base", [
|
||||
("debian", "debian:bookworm-slim"),
|
||||
("ubuntu22", "ubuntu:22.04"),
|
||||
("ubuntu20", "ubuntu:20.04"),
|
||||
("kali", "kalilinux/kali-rolling"),
|
||||
("rocky9", "debian:bookworm-slim"),
|
||||
("alpine", "debian:bookworm-slim"),
|
||||
])
|
||||
def test_build_service_base_image_matches_distro(distro, expected_build_base):
|
||||
"""BASE_IMAGE arg must match the distro's build_base."""
|
||||
config = _make_config(["http"], distro=distro)
|
||||
compose = generate_compose(config)
|
||||
fragment = compose["services"]["decky-01-http"]
|
||||
assert fragment["build"]["args"]["BASE_IMAGE"] == expected_build_base
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BASE_IMAGE NOT injected for upstream-image services
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("svc", UPSTREAM_SERVICES)
|
||||
def test_upstream_service_has_no_build_section(svc):
|
||||
"""Upstream-image services must not receive a build section or BASE_IMAGE."""
|
||||
config = _make_config([svc])
|
||||
compose = generate_compose(config)
|
||||
fragment = compose["services"][f"decky-01-{svc}"]
|
||||
assert "build" not in fragment
|
||||
assert "image" in fragment
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base container uses distro image, not build_base
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("distro", list(DISTROS.keys()))
|
||||
def test_base_container_uses_full_distro_image(distro):
|
||||
"""The IP-holder base container must use distro.image, not build_base."""
|
||||
config = _make_config(["ssh"], distro=distro)
|
||||
compose = generate_compose(config)
|
||||
base = compose["services"]["decky-01"]
|
||||
expected = DISTROS[distro].image
|
||||
assert base["image"] == expected, (
|
||||
f"distro={distro}: base container image '{base['image']}' != '{expected}'"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Distro profile — build_base is always apt-compatible
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_all_distros_have_build_base():
|
||||
for slug, profile in all_distros().items():
|
||||
assert profile.build_base, f"Distro '{slug}' has empty build_base"
|
||||
|
||||
|
||||
def test_all_distro_build_bases_are_apt_compatible():
|
||||
for slug, profile in all_distros().items():
|
||||
assert profile.build_base in APT_COMPATIBLE, (
|
||||
f"Distro '{slug}' build_base '{profile.build_base}' is not apt-compatible. "
|
||||
f"Allowed: {APT_COMPATIBLE}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heterogeneity — multiple deckies with different distros get different images
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_multiple_deckies_different_build_bases():
|
||||
"""A multi-decky deployment with ubuntu22 and debian must differ in BASE_IMAGE."""
|
||||
deckies = [
|
||||
DeckyConfig(
|
||||
name="decky-01", ip="10.0.0.10",
|
||||
services=["http"], distro="debian",
|
||||
base_image="debian:bookworm-slim", build_base="debian:bookworm-slim",
|
||||
hostname="host-01",
|
||||
),
|
||||
DeckyConfig(
|
||||
name="decky-02", ip="10.0.0.11",
|
||||
services=["http"], distro="ubuntu22",
|
||||
base_image="ubuntu:22.04", build_base="ubuntu:22.04",
|
||||
hostname="host-02",
|
||||
),
|
||||
]
|
||||
config = DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||
deckies=deckies,
|
||||
)
|
||||
compose = generate_compose(config)
|
||||
|
||||
base_img_01 = compose["services"]["decky-01-http"]["build"]["args"]["BASE_IMAGE"]
|
||||
base_img_02 = compose["services"]["decky-02-http"]["build"]["args"]["BASE_IMAGE"]
|
||||
|
||||
assert base_img_01 == "debian:bookworm-slim"
|
||||
assert base_img_02 == "ubuntu:22.04"
|
||||
assert base_img_01 != base_img_02
|
||||
Reference in New Issue
Block a user