Two new synthesised-artifact generators that bake the obfuscated fingerprint payload into plausible-looking decoy files: * fingerprint_html — a mundane "Internal Asset Directory" page with a small table of fake hosts; the obfuscated payload is inlined at the bottom of <body>. Visible content (row pool slice, sync timestamp) also varies per mint via SHA-256-derived stable ints, so two extracted canaries don't diff to zero even on the rendered surface. * fingerprint_svg — standalone SVG with an embedded <script> CDATA block. SVG <script> only fires for top-level loads / <object> / <iframe>; <img>-referenced renders are safely inert. Both derive the mint UUID via uuid.uuid5 from the callback token, so re-mints are byte-identical (preserving the generator determinism contract) AND the same token produces the same mint UUID across HTML and SVG variants — the worker can correlate beacons across artifact shapes. Wired into the factory + KNOWN_GENERATORS, default placement paths under ~/Documents/asset_directory.html and ~/Documents/network_topology.svg for both linux and windows personas. Tests cover determinism, per-token divergence, structural validity (DOCTYPE/SVG headers), and that the beacon URL stays inside the obfuscated string array (not in plaintext). The two new entries skip in test_generators.py when Node toolchain is absent so bare CI checkouts still pass.
87 lines
3.9 KiB
Python
87 lines
3.9 KiB
Python
"""Persona-aware path resolution for canary artifacts.
|
|
|
|
Linux-persona deckies use POSIX-shaped paths under ``/home/<user>``.
|
|
"Windows" personas (still Linux containers under the hood — see
|
|
:mod:`decnet.archetypes`) use Windows-shaped paths under
|
|
``/home/<user>/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
|