Initial commit: DECNET honeypot/deception network framework
Core CLI, service plugins (SSH/SMB/FTP/HTTP/RDP), Docker Compose orchestration, MACVLAN networking, and Logstash log forwarding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
82
decnet/config.py
Normal file
82
decnet/config.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Pydantic models for DECNET configuration and runtime state.
|
||||
State is persisted to decnet-state.json in the working directory.
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
STATE_FILE = Path("decnet-state.json")
|
||||
|
||||
BASE_IMAGES = [
|
||||
"debian:bookworm-slim",
|
||||
"ubuntu:22.04",
|
||||
]
|
||||
|
||||
DECKY_NAME_WORDS = [
|
||||
"alpha", "bravo", "charlie", "delta", "echo",
|
||||
"foxtrot", "golf", "hotel", "india", "juliet",
|
||||
"kilo", "lima", "mike", "nova", "oscar",
|
||||
]
|
||||
|
||||
|
||||
def random_hostname() -> str:
|
||||
return f"SRV-{random.choice(DECKY_NAME_WORDS).upper()}-{random.randint(10, 99)}"
|
||||
|
||||
|
||||
class DeckyConfig(BaseModel):
|
||||
name: str
|
||||
ip: str
|
||||
services: list[str]
|
||||
base_image: str
|
||||
hostname: str
|
||||
|
||||
@field_validator("services")
|
||||
@classmethod
|
||||
def services_not_empty(cls, v: list[str]) -> list[str]:
|
||||
if not v:
|
||||
raise ValueError("A decky must have at least one service.")
|
||||
return v
|
||||
|
||||
|
||||
class DecnetConfig(BaseModel):
|
||||
mode: Literal["unihost", "swarm"]
|
||||
interface: str
|
||||
subnet: str
|
||||
gateway: str
|
||||
deckies: list[DeckyConfig]
|
||||
log_target: str | None = None # "ip:port" or None
|
||||
|
||||
@field_validator("log_target")
|
||||
@classmethod
|
||||
def validate_log_target(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
parts = v.rsplit(":", 1)
|
||||
if len(parts) != 2 or not parts[1].isdigit():
|
||||
raise ValueError("log_target must be in ip:port format, e.g. 192.168.1.5:5140")
|
||||
return v
|
||||
|
||||
|
||||
def save_state(config: DecnetConfig, compose_path: Path) -> None:
|
||||
payload = {
|
||||
"config": config.model_dump(),
|
||||
"compose_path": str(compose_path),
|
||||
}
|
||||
STATE_FILE.write_text(json.dumps(payload, indent=2))
|
||||
|
||||
|
||||
def load_state() -> tuple[DecnetConfig, Path] | None:
|
||||
if not STATE_FILE.exists():
|
||||
return None
|
||||
data = json.loads(STATE_FILE.read_text())
|
||||
return DecnetConfig(**data["config"]), Path(data["compose_path"])
|
||||
|
||||
|
||||
def clear_state() -> None:
|
||||
if STATE_FILE.exists():
|
||||
STATE_FILE.unlink()
|
||||
Reference in New Issue
Block a user