Expose nmap_os in INI loader and update test-full.ini

- ini_loader.py: DeckySpec gains nmap_os field; load_ini parses nmap_os=
  (also accepts nmap-os= hyphen alias) and propagates it to amount-expanded deckies
- cli.py: _build_deckies_from_ini resolves nmap_os with priority:
  explicit INI key > archetype default > "linux"
- test-full.ini: every decky now carries nmap_os=; [windows-workstation]
  gains archetype= so its OS family is set correctly; decky-winbox/fileserv/
  ldapdc → windows, decky-iot → embedded, decky-legacy → bsd, rest → linux
- tests/test_ini_loader.py: 7 new tests covering nmap_os parsing, defaults,
  hyphen alias, and amount= expansion propagation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 13:23:45 -03:00
parent 6610856749
commit 086643ef5a
4 changed files with 258 additions and 2 deletions

View File

@@ -179,6 +179,8 @@ def _build_deckies_from_ini(
)
raise typer.Exit(1)
# nmap_os priority: explicit INI key > archetype default > "linux"
resolved_nmap_os = spec.nmap_os or (arch.nmap_os if arch else "linux")
deckies.append(DeckyConfig(
name=spec.name,
ip=ip,
@@ -189,7 +191,7 @@ def _build_deckies_from_ini(
hostname=hostname,
archetype=arch.slug if arch else None,
service_config=spec.service_config,
nmap_os=arch.nmap_os if arch else "linux",
nmap_os=resolved_nmap_os,
))
return deckies

View File

@@ -53,6 +53,7 @@ class DeckySpec:
services: list[str] | None = None
archetype: str | None = None
service_config: dict[str, dict] = field(default_factory=dict)
nmap_os: str | None = None # explicit OS family override (linux/windows/bsd/embedded/cisco)
@dataclass
@@ -113,6 +114,7 @@ def load_ini(path: str | Path) -> IniConfig:
svc_raw = s.get("services")
services = [sv.strip() for sv in svc_raw.split(",")] if svc_raw else None
archetype = s.get("archetype")
nmap_os = s.get("nmap_os") or s.get("nmap-os") or None
amount_raw = s.get("amount", "1")
try:
amount = int(amount_raw)
@@ -123,7 +125,7 @@ def load_ini(path: str | Path) -> IniConfig:
if amount == 1:
cfg.deckies.append(DeckySpec(
name=section, ip=ip, services=services, archetype=archetype,
name=section, ip=ip, services=services, archetype=archetype, nmap_os=nmap_os,
))
else:
# Expand into N deckies; explicit ip is ignored (can't share one IP)
@@ -138,6 +140,7 @@ def load_ini(path: str | Path) -> IniConfig:
ip=None,
services=services,
archetype=archetype,
nmap_os=nmap_os,
))
# Second pass: collect per-service subsections [decky-name.service]

192
test-full.ini Normal file
View File

@@ -0,0 +1,192 @@
# DECNET Full Test Config
# Covers all 25 registered services across 10 role-themed deckies + archetype pool.
# Distros are auto-cycled for heterogeneity (9 profiles, round-robin).
#
# nmap_os controls the TCP/IP stack sysctls injected into each decky's base
# container so nmap OS detection returns the expected OS family:
# linux → TTL 64, syn_retries 6
# windows → TTL 128, syn_retries 2, large recv buffer
# embedded → TTL 255, syn_retries 3
# bsd → TTL 64, syn_retries 6
# cisco → TTL 255, syn_retries 2
#
# Usage:
# decnet deploy --config test-full.ini --dry-run
# sudo decnet deploy --config test-full.ini --log-target 192.168.1.200:5140 \
# --log-file /var/log/decnet/decnet.log
[general]
net = 192.168.1.0/24
gw = 192.168.1.1
interface = wlp6s0
#log_target = 192.168.1.200:5140
# ── Archetype pool: 10 Windows workstations ───────────────────────────────────
# archetype=windows-workstation already sets nmap_os=windows automatically.
[windows-workstation]
archetype = windows-workstation
amount = 10
# ── Web / Mail stack ──────────────────────────────────────────────────────────
# Looks like an internet-facing Linux mail + web host
[decky-webmail]
ip = 192.168.1.110
services = http, smtp, imap, pop3
nmap_os = linux
[decky-webmail.http]
server_header = Apache/2.4.54 (Debian)
response_code = 200
fake_app = wordpress
[decky-webmail.smtp]
smtp_banner = 220 mail.corp.local ESMTP Postfix (Debian/GNU)
smtp_mta = mail.corp.local
# ── File / Transfer services ──────────────────────────────────────────────────
# Presents as a Windows/Samba file server — TTL 128 seals the illusion.
[decky-fileserv]
ip = 192.168.1.111
services = smb, ftp, tftp
nmap_os = windows
[decky-fileserv.smb]
workgroup = CORP
server_name = FILESERV01
os_version = Windows Server 2019
# ── LAMP-style database host ──────────────────────────────────────────────────
[decky-dbsrv01]
ip = 192.168.1.112
services = mysql, redis
nmap_os = linux
[decky-dbsrv01.mysql]
mysql_version = 5.7.38-log
mysql_banner = MySQL Community Server
[decky-dbsrv01.redis]
redis_version = 6.2.7
# ── Modern stack databases ────────────────────────────────────────────────────
[decky-dbsrv02]
ip = 192.168.1.113
services = postgres, mongodb, elasticsearch
nmap_os = linux
[decky-dbsrv02.postgres]
pg_version = 14.5
[decky-dbsrv02.mongodb]
mongo_version = 5.0.9
[decky-dbsrv02.elasticsearch]
es_version = 8.4.3
cluster_name = prod-search
# ── Windows workstation / server ──────────────────────────────────────────────
# RDP + SMB + MSSQL — nmap_os=windows gives TTL 128 to complete the fingerprint.
[decky-winbox]
ip = 192.168.1.114
services = rdp, smb, mssql
nmap_os = windows
[decky-winbox.rdp]
os_version = Windows Server 2016
build = 14393
[decky-winbox.smb]
workgroup = CORP
server_name = WINSRV-DC01
os_version = Windows Server 2016
[decky-winbox.mssql]
mssql_version = Microsoft SQL Server 2019
# ── DevOps / Container infra ──────────────────────────────────────────────────
[decky-devops]
ip = 192.168.1.115
services = k8s, docker_api
nmap_os = linux
[decky-devops.k8s]
k8s_version = v1.26.3
[decky-devops.docker_api]
docker_version = 24.0.2
# ── Directory / Auth services ─────────────────────────────────────────────────
# Active Directory DC persona — Windows TCP stack matches the LDAP/SMB services.
[decky-ldapdc]
ip = 192.168.1.116
services = ldap, ssh
nmap_os = windows
[decky-ldapdc.ldap]
base_dn = dc=corp,dc=local
domain = corp.local
[decky-ldapdc.ssh]
ssh_version = OpenSSH_8.9p1 Ubuntu-3ubuntu0.6
kernel_version = 5.15.0-91-generic
users = root:toor,admin:admin123,svc_backup:backup2024
# ── IoT / Industrial / Network management ─────────────────────────────────────
# TTL 255 is the embedded/network-device giveaway nmap looks for.
[decky-iot]
ip = 192.168.1.117
services = mqtt, snmp, conpot
nmap_os = embedded
[decky-iot.mqtt]
mqtt_version = Mosquitto 2.0.15
[decky-iot.snmp]
snmp_community = public
sys_descr = Linux router 5.4.0 #1 SMP x86_64
# ── VoIP / Local network services ────────────────────────────────────────────
[decky-voip]
ip = 192.168.1.118
services = sip, llmnr
nmap_os = linux
[decky-voip.sip]
sip_server = Asterisk PBX 18.12.0
sip_domain = pbx.corp.local
# ── Legacy admin / remote access ─────────────────────────────────────────────
# Old-school unpatched box — BSD stack for variety.
[decky-legacy]
ip = 192.168.1.119
services = telnet, vnc, ssh
nmap_os = bsd
[decky-legacy.ssh]
ssh_version = OpenSSH_7.4p1 Debian-10+deb9u7
kernel_version = 4.9.0-19-amd64
users = root:root,admin:password,pi:raspberry
[decky-legacy.vnc]
vnc_version = RealVNC 6.7.2

View File

@@ -156,3 +156,62 @@ def test_no_custom_services_gives_empty_list(tmp_path):
""")
cfg = load_ini(ini_file)
assert cfg.custom_services == []
# ---------------------------------------------------------------------------
# nmap_os parsing
# ---------------------------------------------------------------------------
def test_nmap_os_parsed_from_ini(tmp_path):
ini_file = _write_ini(tmp_path, """
[decky-win]
ip = 192.168.1.101
services = rdp, smb
nmap_os = windows
""")
cfg = load_ini(ini_file)
assert cfg.deckies[0].nmap_os == "windows"
def test_nmap_os_defaults_to_none_when_absent(tmp_path):
ini_file = _write_ini(tmp_path, """
[decky-01]
services = ssh
""")
cfg = load_ini(ini_file)
assert cfg.deckies[0].nmap_os is None
@pytest.mark.parametrize("os_family", ["linux", "windows", "bsd", "embedded", "cisco"])
def test_nmap_os_all_families_accepted(tmp_path, os_family):
ini_file = _write_ini(tmp_path, f"""
[decky-01]
services = ssh
nmap_os = {os_family}
""")
cfg = load_ini(ini_file)
assert cfg.deckies[0].nmap_os == os_family
def test_nmap_os_propagates_to_amount_expanded_deckies(tmp_path):
ini_file = _write_ini(tmp_path, """
[corp-printers]
services = snmp
nmap_os = embedded
amount = 3
""")
cfg = load_ini(ini_file)
assert len(cfg.deckies) == 3
for d in cfg.deckies:
assert d.nmap_os == "embedded"
def test_nmap_os_hyphen_alias_accepted(tmp_path):
"""nmap-os= (hyphen) should work as an alias for nmap_os=."""
ini_file = _write_ini(tmp_path, """
[decky-01]
services = ssh
nmap-os = bsd
""")
cfg = load_ini(ini_file)
assert cfg.deckies[0].nmap_os == "bsd"