diff --git a/README.md b/README.md index b3c92cc..47100e5 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,633 @@ # DECNET -A honeypot/deception network framework. Deploys fake machines (**deckies**) with realistic services (SSH, SMB, RDP, FTP, HTTP) that appear as real LAN hosts — complete with their own MACs and IPs — to lure, detect, and profile attackers. All interactions are forwarded to an isolated logging pipeline (ELK / SIEM). +A honeypot deception network framework. Spin up a fleet of fake machines — called **deckies** — that appear as real, heterogeneous LAN hosts to anyone scanning the network. Each decky gets its own MAC address, IP, hostname, services, OS fingerprint, and log pipeline. + +Attackers probe the network, DECNET traps every interaction, and you watch from a safe, isolated logging stack. + +--- + +## Table of Contents + +- [How It Works](#how-it-works) +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [CLI Reference](#cli-reference) +- [Archetypes](#archetypes) +- [Services](#services) +- [OS Fingerprint Spoofing](#os-fingerprint-spoofing) +- [Distro Profiles](#distro-profiles) +- [Config File](#config-file) +- [Logging](#logging) +- [Network Drivers](#network-drivers) +- [Architecture](#architecture) +- [Writing a Custom Service Plugin](#writing-a-custom-service-plugin) +- [Development & Testing](#development--testing) + +--- + +## How It Works ``` -attacker ──► decoy network (deckies) - │ - └──► log forwarder ──► isolated SIEM (ELK) +Attacker scans 192.168.1.110–119 + │ + ▼ + ┌──────────────────────────────────────────────┐ + │ DECNET LAN (MACVLAN) │ + │ │ + │ decky-01 192.168.1.110 ssh + http │ + │ decky-02 192.168.1.111 rdp + smb + mssql │ + │ decky-03 192.168.1.112 mqtt + snmp │ + │ ... │ + └──────────────────────────────────────────────┘ + │ + ▼ all interactions forwarded via RFC 5424 syslog + ┌──────────────────────┐ + │ ELK / SIEM stack │ (isolated network — not reachable from decoys) + └──────────────────────┘ ``` +Each decky is a small cluster of Docker containers sharing one network namespace: + +- **Base container** — holds the MACVLAN IP, sets TCP/IP stack sysctls for OS fingerprint spoofing, runs `sleep infinity`. +- **Service containers** — one per honeypot service, all sharing the base's network so they appear to come from the same IP. + +From the outside a decky looks identical to a real machine: it has its own MAC address (assigned by MACVLAN), its own IP, its own hostname, and its TCP/IP stack behaves like the OS it is pretending to be. + --- ## Requirements -- Python ≥ 3.11 -- Docker + Docker Compose -- Root / `sudo` for MACVLAN networking (bare metal or VM recommended; WSL has known limitations) +- Linux host (bare metal or VM — WSL has MACVLAN limitations) +- Docker Engine 24+ +- Python 3.11+ +- Root / `sudo` for network setup (MACVLAN creation, host interface config) +- NIC in promiscuous mode for MACVLAN (or use `--ipvlan` on WiFi) --- -## Install +## Installation ```bash +git clone DECNET +cd DECNET pip install -e . ``` ---- - -## Usage +Verify: ```bash -# List available honeypot service plugins -decnet services - -# Dry-run — generate compose file, no containers started -decnet deploy --mode unihost --deckies 3 --randomize-services --dry-run - -# Deploy 5 deckies with random services -sudo decnet deploy --mode unihost --deckies 5 --interface eth0 --randomize-services - -# Deploy with specific services and log forwarding -sudo decnet deploy --mode unihost --deckies 3 --services ssh,smb --log-target 192.168.1.5:5140 - -# Deploy from an INI config file -sudo decnet deploy --config decnet.ini - -# Status -decnet status - -# Teardown -sudo decnet teardown --all -sudo decnet teardown --id decky-01 +decnet --help +decnet services # list all 25 registered honeypot services +decnet archetypes # list machine archetype profiles +decnet distros # list available OS distro profiles ``` -### Key flags +--- + +## Quick Start + +### Dry run — generate compose, no containers + +```bash +decnet deploy --mode unihost --deckies 5 --randomize-services --dry-run +``` + +### Deploy with random services + +```bash +sudo decnet deploy --mode unihost --deckies 5 --interface eth0 --randomize-services +``` + +### Deploy a specific role + +```bash +sudo decnet deploy --mode unihost --deckies 3 --archetype windows-workstation +``` + +### Deploy from a config file + +```bash +sudo decnet deploy --config test-full.ini +``` + +### Check status + +```bash +decnet status +``` + +### Tear everything down + +```bash +sudo decnet teardown --all +sudo decnet teardown --id decky-02 # single decky +``` + +--- + +## CLI Reference + +### `decnet deploy` + +| Flag | Default | Description | +|---|---|---| +| `--mode` | `unihost` | Deployment mode: `unihost` or `swarm` | +| `--deckies` / `-n` | — | Number of deckies to deploy (required without `--config`) | +| `--interface` / `-i` | auto-detected | Host NIC to attach MACVLAN to | +| `--subnet` | auto-detected | LAN subnet CIDR, e.g. `192.168.1.0/24` | +| `--ip-start` | auto | First IP to assign to deckies | +| `--services` | — | Comma-separated service slugs, e.g. `ssh,smb,rdp` | +| `--randomize-services` | false | Assign random services to each decky | +| `--distro` | auto-cycled | Comma-separated distro slugs, e.g. `debian,ubuntu22` | +| `--randomize-distros` | false | Assign a random distro to each decky | +| `--archetype` / `-a` | — | Machine archetype slug (sets services + OS family automatically) | +| `--log-target` | — | Forward logs to `ip:port` (RFC 5424 syslog) | +| `--log-file` | — | Write logs to this path inside containers | +| `--ipvlan` | false | Use IPvlan L2 instead of MACVLAN (required on WiFi) | +| `--dry-run` | false | Generate compose file without starting containers | +| `--no-cache` | false | Force rebuild all images | +| `--config` / `-c` | — | Path to INI config file | + +### `decnet status` + +Print a table of all deployed deckies, their IPs, services, hostnames, and container states. + +### `decnet teardown` | Flag | Description | |---|---| -| `--mode` | `unihost` (single host) or `swarm` (multi-host) | -| `--deckies N` | Number of fake machines to spin up | -| `--interface` | Host NIC (auto-detected if omitted) | -| `--subnet` | LAN subnet CIDR (auto-detected if omitted) | -| `--ip-start` | First decky IP (auto if omitted) | -| `--services` | Comma-separated list: `ssh,smb,rdp,ftp,http` | -| `--randomize-services` | Assign random service mix to each decky | -| `--log-target` | Forward logs to `ip:port` (e.g. Logstash) | -| `--dry-run` | Generate compose file without starting containers | -| `--no-cache` | Force rebuild all images | -| `--config` | Path to INI config file | +| `--all` | Tear down all deckies and remove the MACVLAN network | +| `--id ` | Stop and remove a single decky by name | + +### `decnet services` + +List all registered honeypot service plugins with their ports and Docker images. + +### `decnet distros` + +List all available OS distro profiles. + +### `decnet archetypes` + +List all machine archetype profiles with their default services and descriptions. --- -## Deployment Modes +## Archetypes -**UNIHOST** — one real host spins up _n_ deckies via Docker Compose. Simplest setup, single machine. +Archetypes are pre-packaged machine identities. One slug sets services, preferred distros, and OS fingerprint all at once — no need to think about individual components. -**SWARM (MULTIHOST)** — _n_ real hosts each running deckies. Orchestrated via Ansible or similar tooling. +| Slug | Services | OS Fingerprint | Description | +|---|---|---|---| +| `windows-workstation` | smb, rdp | windows | Corporate Windows desktop | +| `windows-server` | smb, rdp, ldap | windows | Windows domain member | +| `domain-controller` | ldap, smb, rdp, llmnr | windows | Active Directory DC | +| `linux-server` | ssh, http | linux | General-purpose Linux host | +| `web-server` | http, ftp | linux | Public-facing web host | +| `database-server` | mysql, postgres, redis | linux | Data tier host | +| `mail-server` | smtp, pop3, imap | linux | SMTP/IMAP/POP3 relay | +| `file-server` | smb, ftp, ssh | linux | SMB/FTP/SFTP storage node | +| `printer` | snmp, ftp | embedded | Network-attached printer | +| `iot-device` | mqtt, snmp, telnet | embedded | Embedded/IoT device | +| `industrial-control` | conpot, snmp | embedded | ICS/SCADA node | +| `voip-server` | sip | linux | SIP PBX / VoIP gateway | +| `monitoring-node` | snmp, ssh | linux | Infrastructure monitoring host | +| `devops-host` | docker_api, ssh, k8s | linux | CI/CD / container host | + +#### CLI + +```bash +sudo decnet deploy --deckies 4 --archetype windows-workstation +``` + +#### INI + +```ini +[corp-workstations] +archetype = windows-workstation +amount = 4 +``` + +--- + +## Services + +25 honeypot services are registered out of the box. Use their slug in `--services` or `services=` in a config file. + +| Slug | Ports | Protocol / Role | +|---|---|---| +| `ssh` | 22 | SSH (Cowrie honeypot) | +| `http` | 80, 443 | HTTP/HTTPS web server | +| `ftp` | 21 | FTP file transfer | +| `tftp` | 69 | TFTP (trivial file transfer) | +| `smb` | 445, 139 | SMB/CIFS file shares | +| `rdp` | 3389 | Remote Desktop Protocol | +| `telnet` | 23 | Telnet remote access | +| `vnc` | 5900 | VNC remote desktop | +| `smtp` | 25, 587 | SMTP mail relay | +| `imap` | 143, 993 | IMAP mail access | +| `pop3` | 110, 995 | POP3 mail access | +| `ldap` | 389, 636 | LDAP / Active Directory | +| `llmnr` | 5355, 5353 | LLMNR / mDNS (Windows name resolution) | +| `mysql` | 3306 | MySQL database | +| `postgres` | 5432 | PostgreSQL database | +| `mssql` | 1433 | Microsoft SQL Server | +| `mongodb` | 27017 | MongoDB document store | +| `redis` | 6379 | Redis key-value store | +| `elasticsearch` | 9200 | Elasticsearch REST API | +| `mqtt` | 1883 | MQTT IoT broker | +| `snmp` | 161 | SNMP network management | +| `sip` | 5060 | SIP VoIP protocol | +| `k8s` | 6443, 8080 | Kubernetes API server | +| `docker_api` | 2375, 2376 | Docker Remote API | +| `conpot` | 502, 161, 80 | ICS/SCADA (Modbus, S7, DNP3) | + +List live at any time with `decnet services`. + +### Per-service persona config + +Most services accept persona configuration to make honeypot responses more convincing. Config is passed via INI subsections (`[decky-name.service]`) or the `service_config` field in code. + +```ini +[decky-webmail.http] +server_header = Apache/2.4.54 (Debian) +fake_app = wordpress + +[decky-winbox.smb] +workgroup = CORP +server_name = WINSRV-DC01 +os_version = Windows Server 2016 + +[decky-legacy.ssh] +ssh_version = OpenSSH_7.4p1 Debian-10+deb9u7 +kernel_version = 4.9.0-19-amd64 +users = root:root,admin:password +``` + +### Bring-your-own service (BYOS) + +Drop in a custom service definition using the `custom-` prefix in an INI config: + +```ini +[custom-myapp] +binary = my-docker-image:latest +exec = /usr/bin/myapp -p 9999 +ports = 9999 +``` + +The service is registered at runtime and can be referenced as `myapp` in any decky's `services=` list. + +--- + +## OS Fingerprint Spoofing + +DECNET injects Linux kernel TCP/IP stack parameters (`sysctls`) into each decky's base container so that active OS detection (e.g. `nmap -O`) returns the expected OS rather than "Linux". + +The most important probe nmap uses is the IP TTL. Secondary tuning covers TCP SYN retry behaviour and initial receive window size. + +### OS families + +| Family | TTL | `tcp_syn_retries` | Notes | +|---|---|---|---| +| `linux` | 64 | 6 | Default | +| `windows` | 128 | 2 | + 8 MB recv buffer | +| `bsd` | 64 | 6 | FreeBSD / macOS-style | +| `embedded` | 255 | 3 | Printers, IoT, PLCs | +| `cisco` | 255 | 2 | Network devices | + +Because service containers share the base container's network namespace (`network_mode: service:`), the spoofed stack applies to **all** traffic from the decky — no per-service config needed. + +### Automatic via archetype + +Archetypes set `nmap_os` automatically. A `windows-workstation` decky comes with TTL 128 out of the box. + +### Explicit in INI + +```ini +[decky-winbox] +services = rdp, smb, mssql +nmap_os = windows # also accepts nmap-os= + +[decky-iot] +services = mqtt, snmp +nmap_os = embedded + +[decky-legacy] +services = telnet, vnc, ssh +nmap_os = bsd +``` + +Priority: **explicit `nmap_os=`** > archetype default > `linux`. + +### Verify with nmap + +```bash +sudo nmap -O 192.168.1.114 # should report Windows +sudo nmap -O 192.168.1.117 # should report embedded / network device +``` + +> **Note:** Linux kernel containers cannot perfectly replicate every nmap OS probe (sequence generation, ECN flags, etc.). TTL and TCP window tuning cover the most reliable detection vectors. Full impersonation would require a userspace TCP stack. + +--- + +## Distro Profiles + +The distro controls which Docker base image is used for the IP-holding base container, giving each decky a different OS identity at the image layer and varying the hostname style. + +| Slug | Docker Image | Display Name | +|---|---|---| +| `debian` | `debian:bookworm-slim` | Debian 12 (Bookworm) | +| `ubuntu22` | `ubuntu:22.04` | Ubuntu 22.04 LTS (Jammy) | +| `ubuntu20` | `ubuntu:20.04` | Ubuntu 20.04 LTS (Focal) | +| `rocky9` | `rockylinux:9-minimal` | Rocky Linux 9 | +| `centos7` | `centos:7` | CentOS 7 | +| `alpine` | `alpine:3.19` | Alpine Linux 3.19 | +| `fedora` | `fedora:39` | Fedora 39 | +| `kali` | `kalilinux/kali-rolling` | Kali Linux (Rolling) | +| `arch` | `archlinux:latest` | Arch Linux | + +When no distro is specified, DECNET cycles through all profiles in round-robin to maximise heterogeneity automatically. + +```bash +# Explicit single distro +sudo decnet deploy --deckies 3 --services ssh --distro rocky9 + +# Mix of distros (cycled) +sudo decnet deploy --deckies 6 --services ssh --distro debian,ubuntu22,rocky9 + +# Fully random +sudo decnet deploy --deckies 5 --randomize-services --randomize-distros +``` + +--- + +## Config File + +For anything beyond a handful of deckies, use an INI config file. It gives you per-decky IPs, per-service personas, archetype pools, and custom service definitions all in one place. + +```bash +decnet deploy --config mynet.ini --dry-run +sudo decnet deploy --config mynet.ini --log-target 192.168.1.200:5140 +``` + +### Structure + +```ini +# ── Global settings ─────────────────────────────────────────────────────────── + +[general] +net = 192.168.1.0/24 # subnet CIDR +gw = 192.168.1.1 # gateway IP +interface = eth0 # host NIC (optional, auto-detected if omitted) +log_target = 192.168.1.200:5140 # syslog forwarding target (optional) + +# ── Decky sections ──────────────────────────────────────────────────────────── + +[decky-01] +ip = 192.168.1.110 # optional; auto-allocated if omitted +services = ssh, http # comma-separated service slugs +nmap_os = linux # OS fingerprint family (optional, default: linux) + +# ── Per-service persona ─────────────────────────────────────────────────────── + +[decky-01.ssh] +ssh_version = OpenSSH_8.9p1 Ubuntu-3ubuntu0.6 +kernel_version = 5.15.0-91-generic +users = root:toor,admin:admin123 + +[decky-01.http] +server_header = nginx/1.18.0 +fake_app = wordpress + +# ── Archetype shorthand ─────────────────────────────────────────────────────── + +[corp-workstations] +archetype = windows-workstation # sets services, distros, and nmap_os automatically +amount = 10 # spawn 10 deckies from this definition + +# ── Bring-your-own service ──────────────────────────────────────────────────── + +[custom-myapp] +binary = my-image:latest +exec = /usr/bin/myapp -p 9999 +ports = 9999 +``` + +### Field reference + +#### `[general]` + +| Key | Required | Description | +|---|---|---| +| `net` | Yes | Subnet CIDR for the decoy LAN | +| `gw` | Yes | Gateway IP | +| `interface` | No | Host NIC; auto-detected if absent | +| `log_target` | No | `ip:port` for RFC 5424 syslog forwarding | + +#### Decky sections + +| Key | Required | Description | +|---|---|---| +| `ip` | No | Static IP; auto-allocated from subnet if absent | +| `services` | See note | Comma-separated service slugs | +| `archetype` | See note | Archetype slug; sets services + nmap_os unless overridden | +| `nmap_os` | No | OS fingerprint family: `linux` / `windows` / `bsd` / `embedded` / `cisco` | +| `amount` | No | Spawn N deckies from this block (default: 1); cannot combine with `ip=` | + +> One of `services=`, `archetype=`, or `--randomize-services` is required per decky. + +#### Per-service subsections `[decky-name.service]` + +Key/value pairs are passed directly to the service plugin as persona config. Common keys: + +| Service | Accepted keys | +|---|---| +| `ssh` | `ssh_version`, `kernel_version`, `users` | +| `http` | `server_header`, `response_code`, `fake_app` | +| `smtp` | `smtp_banner`, `smtp_mta` | +| `smb` | `workgroup`, `server_name`, `os_version` | +| `rdp` | `os_version`, `build` | +| `mysql` | `mysql_version`, `mysql_banner` | +| `redis` | `redis_version` | +| `postgres` | `pg_version` | +| `mongodb` | `mongo_version` | +| `elasticsearch` | `es_version`, `cluster_name` | +| `ldap` | `base_dn`, `domain` | +| `snmp` | `snmp_community`, `sys_descr` | +| `mqtt` | `mqtt_version` | +| `sip` | `sip_server`, `sip_domain` | +| `k8s` | `k8s_version` | +| `docker_api` | `docker_version` | +| `vnc` | `vnc_version` | +| `mssql` | `mssql_version` | + +When using `amount=`, a subsection like `[group-name.ssh]` automatically propagates to all expanded deckies (`group-name-01`, `group-name-02`, …). + +### Full example + +See [`test-full.ini`](test-full.ini) — covers all 25 services across 10 role-themed deckies with per-service personas, archetype pools, OS fingerprint assignments, and inline comments explaining each choice. + +--- + +## Logging + +All attacker interactions are forwarded off the decoy network to an isolated logging sink. The log pipeline lives on a separate internal Docker bridge (`decnet_logs`) that is not reachable from the fake LAN. + +### Syslog forwarding (RFC 5424) + +```bash +sudo decnet deploy --config mynet.ini --log-target 192.168.1.200:5140 +``` + +Or in `[general]`: + +```ini +log_target = 192.168.1.200:5140 +``` + +### File logging + +```bash +sudo decnet deploy --config mynet.ini --log-file /var/log/decnet/decnet.log +``` + +The log directory is bind-mounted into every service container. Log entries follow RFC 5424 syslog format. + +### Log target health check + +Before deployment, DECNET probes the log target and warns if it is unreachable: + +``` +Warning: log target 192.168.1.200:5140 is unreachable. Logs will be lost if it stays down. +``` + +Deployment continues regardless — the log target can come up later. + +--- + +## Network Drivers + +### MACVLAN (default) + +Each decky gets a unique MAC address assigned by the kernel, making it appear as a distinct physical machine on the LAN. Requires the host NIC to support promiscuous mode. + +```bash +sudo decnet deploy --interface eth0 --deckies 5 --randomize-services +``` + +**Known limitation:** The host cannot communicate directly with its own MACVLAN children by default. DECNET automatically creates a `decnet_macvlan0` host-side interface as a hairpin workaround so that `decnet status` and log collection continue to work from the host. + +### IPvlan L2 (`--ipvlan`) + +Use IPvlan L2 when MACVLAN is not available — typically on WiFi interfaces where the access point filters non-registered MACs. IPvlan shares the host MAC and gives each decky a unique IP only. + +```bash +sudo decnet deploy --interface wlp6s0 --ipvlan --deckies 3 --randomize-services +``` --- ## Architecture -- **Containers**: Docker Compose with `debian:bookworm-slim` as the default base image. Mixing Ubuntu, CentOS, and other distros is encouraged to make the decoy network look heterogeneous. -- **Networking**: MACVLAN/IPVLAN — each decky gets its own MAC and IP, appearing as a distinct real machine on the LAN. -- **Log pipeline**: Logstash → ELK stack → SIEM on an isolated network unreachable from the decoy network. -- **Services**: Plugin-based registry (`decnet/services/`). Each plugin declares its ports, default image, and container config. - ``` decnet/ -├── cli.py # Typer CLI — deploy, status, teardown, services -├── config.py # Pydantic models (DecnetConfig, DeckyConfig) -├── composer.py # Docker Compose YAML generator -├── deployer.py # Container lifecycle management -├── network.py # IP allocation, interface/subnet detection -├── ini_loader.py # INI config file support +├── cli.py # Typer CLI entry point; builds DecnetConfig from flags/INI +├── config.py # Pydantic models: DeckyConfig, DecnetConfig; state persistence +├── composer.py # Generates docker-compose.yml from DecnetConfig +├── deployer.py # Docker SDK: bring-up, teardown, status +├── network.py # MACVLAN/IPvlan creation, IP allocation, hairpin interface +├── archetypes.py # Machine archetype profiles (14 built-in) +├── distros.py # OS distro profiles (9 built-in), hostname generation +├── os_fingerprint.py # TCP/IP sysctl profiles per OS family for nmap spoofing +├── ini_loader.py # INI config file parser +├── custom_service.py # Bring-your-own service runtime registration +├── services/ +│ ├── base.py # BaseService ABC — contract every plugin must implement +│ ├── registry.py # Auto-discovers and registers all BaseService subclasses +│ └── *.py # 25 individual honeypot service plugins ├── logging/ -│ └── forwarder.py # Log target probe + forwarding -└── services/ - ├── registry.py # Plugin registry - ├── ssh.py - ├── smb.py - ├── rdp.py - ├── ftp.py - └── http.py +│ ├── forwarder.py # RFC 5424 syslog UDP forwarder +│ ├── file_handler.py +│ └── syslog_formatter.py +└── templates/ # Dockerfiles and service entrypoint scripts ``` +### Container model + +``` +decky-01 (base) ← MACVLAN IP owner; sleep infinity; sysctls applied here + ├─ decky-01-ssh ← network_mode: service:decky-01 (shares IP + MAC) + ├─ decky-01-http ← network_mode: service:decky-01 + └─ decky-01-smb ← network_mode: service:decky-01 +``` + +Service containers carry no network config of their own. From the outside, every port on a decky appears to belong to a single machine. + --- -## INI Config +## Writing a Custom Service Plugin -You can describe a fully custom decoy fleet in an INI file instead of CLI flags: +1. Create `decnet/services/myservice.py`: -```ini -[global] -interface = eth0 -log_target = 192.168.1.5:5140 +```python +from decnet.services.base import BaseService -[decky-01] -services = ssh,smb -base_image = debian:bookworm-slim -hostname = DESKTOP-A1B2C3 +class MyService(BaseService): + name = "myservice" + ports = [1234] + default_image = "my-docker-image:latest" -[decky-02] -services = rdp,http -base_image = ubuntu:22.04 -hostname = WIN-SERVER-02 + def compose_fragment(self, decky_name, log_target=None, service_cfg=None): + cfg = service_cfg or {} + return { + "image": self.default_image, + "container_name": f"{decky_name}-myservice", + "restart": "unless-stopped", + "environment": { + "MY_BANNER": cfg.get("banner", "default banner"), + }, + } ``` +2. The registry auto-discovers all `BaseService` subclasses at import time — no registration step needed. + +3. Use it immediately: + ```bash -sudo decnet deploy --config decnet.ini +decnet services # myservice appears in the list +sudo decnet deploy --deckies 2 --services myservice +``` + +For services that require a custom Dockerfile, set `default_image = "build"` and override `dockerfile_context()` to return the path to your build context directory. The composer injects `BASE_IMAGE` as a build arg so your Dockerfile picks up the correct distro image automatically: + +```dockerfile +ARG BASE_IMAGE=debian:bookworm-slim +FROM ${BASE_IMAGE} +... ``` --- -## Adding a Service Plugin +## Development & Testing -1. Create `decnet/services/yourservice.py` implementing the `BaseService` interface. -2. Register it in `decnet/services/registry.py`. -3. Verify with `decnet services`. +```bash +pip install -e . +python -m pytest # 478 tests, < 1 second +``` + +The test suite covers: + +| File | What it tests | +|---|---| +| `test_composer.py` | Compose generation, BASE_IMAGE injection, distro heterogeneity | +| `test_os_fingerprint.py` | OS sysctl profiles, compose injection, archetype coverage, CLI propagation | +| `test_ini_loader.py` | INI parsing, subsection propagation, custom services, `nmap_os` | +| `test_services.py` | Per-service persona config, compose fragments | +| `test_network.py` | IP allocation, range calculation | +| `test_log_file_mount.py` | Log directory bind-mount injection | +| `test_syslog_formatter.py` | RFC 5424 syslog formatting | +| `test_archetypes.py` | Archetype validation and field correctness | +| `test_cli_service_pool.py` | CLI service resolution | + +Every new feature requires passing tests before merging.