"""Persona-aware path resolution for canary artifacts. Linux-persona deckies use POSIX-shaped paths under ``/home/``. "Windows" personas (still Linux containers under the hood — see :mod:`decnet.archetypes`) use Windows-shaped paths under ``/home//AppData/...`` so an attacker browsing the filesystem through a planted RDP/SMB session sees the right shape. The persona lookup is best-effort: callers pass the :attr:`decnet.archetypes.Archetype.nmap_os` value (``"linux"`` or ``"windows"``); unknown personas fall through to ``"linux"``. Operators can always override by passing an explicit ``placement_path`` when creating a token. """ from __future__ import annotations DEFAULT_LINUX_USER = "admin" DEFAULT_WINDOWS_USER = "Administrator" # Canonical placements for the synthesizer-driven baseline tokens. # Operators can override per-token via the API, but these are the # defaults the deploy-time seed uses. _LINUX_DEFAULTS: dict[str, str] = { "git_config": "/home/{user}/.git/config", "env_file": "/home/{user}/.env", "ssh_key": "/home/{user}/.ssh/id_rsa", "aws_creds": "/home/{user}/.aws/credentials", "honeydoc": "/home/{user}/Documents/quarterly_report.html", "honeydoc_docx": "/home/{user}/Documents/quarterly_report.docx", "honeydoc_pdf": "/home/{user}/Documents/quarterly_report.pdf", "fingerprint_html": "/home/{user}/Documents/asset_directory.html", "fingerprint_svg": "/home/{user}/Documents/network_topology.svg", } _WINDOWS_DEFAULTS: dict[str, str] = { "git_config": "/home/{user}/AppData/Local/Programs/Git/etc/gitconfig", "env_file": "/home/{user}/Desktop/prod.env", "ssh_key": "/home/{user}/.ssh/id_rsa", # OpenSSH on Windows uses the same path "aws_creds": "/home/{user}/.aws/credentials", "honeydoc": "/home/{user}/Documents/quarterly_report.html", "honeydoc_docx": "/home/{user}/Documents/quarterly_report.docx", "honeydoc_pdf": "/home/{user}/Documents/quarterly_report.pdf", "fingerprint_html": "/home/{user}/Documents/asset_directory.html", "fingerprint_svg": "/home/{user}/Documents/network_topology.svg", } def default_user(persona: str) -> str: """Return the conventional unprivileged username for a persona.""" return DEFAULT_WINDOWS_USER if persona == "windows" else DEFAULT_LINUX_USER def default_path_for(generator: str, persona: str = "linux") -> str: """Resolve the default placement path for a synthesized token. Returns an absolute container path with ``{user}`` already expanded. Falls back to a sane Linux default for unknown personas — better to plant *something* than fail the deploy hook. """ table = _WINDOWS_DEFAULTS if persona == "windows" else _LINUX_DEFAULTS template = table.get(generator) if not template: # Unknown generator — fall back to a generic /tmp drop so the # planter still has somewhere to write. The API rejects # unknown generators upstream, so this branch is defensive. return f"/tmp/{generator}.canary" # nosec B108 — placement inside attacker-facing decoy container, not host /tmp return template.format(user=default_user(persona)) def normalize_placement(path: str) -> str: """Validate and normalize an operator-supplied placement path. Forbids relative paths, NUL bytes, and shell metacharacters that ``docker exec sh -c`` can't safely round-trip. Returns the sanitised path unchanged when valid; raises :class:`ValueError` otherwise so the API can return a 400 with a clear message. """ if not path or not path.startswith("/"): raise ValueError("placement_path must be absolute (start with '/')") if "\x00" in path: raise ValueError("placement_path may not contain NUL") if "\n" in path or "\r" in path: raise ValueError("placement_path may not contain newlines") if "../" in path or path.endswith("/.."): raise ValueError("placement_path may not contain '..' segments") return path