# SPDX-License-Identifier: AGPL-3.0-or-later """Realism contract adapter for canary generators. Stage 7 of the realism migration. The orchestrator's planner picks a ``canary_*`` :class:`~decnet.realism.taxonomy.ContentClass` 1–3% of the time on file ticks; this module turns that pick into a :class:`~decnet.canary.base.CanaryArtifact` (bytes the SSH driver plants) plus a persisted :class:`~decnet.web.db.models.CanaryToken` row so the canary worker recognises the slug when an attacker trips it. What this is NOT: it doesn't pick *when* canaries fire — that's the realism planner's job. It doesn't decide *where* on the filesystem the canary lands beyond what realism naming + persona conventions already produce. It's a thin bytes-and-row factory bolted onto the realism contract. Stealth (per ``feedback_stealth.md``): we never leak the ``DECNET`` literal into anything that survives to the planted file. The underlying generators are already stealth-clean; this wrapper must not undo that. """ from __future__ import annotations import os import secrets as _secrets from datetime import datetime, timezone from typing import Any, Optional from decnet.canary.base import CanaryArtifact, CanaryContext from decnet.canary.factory import get_generator from decnet.logging import get_logger from decnet.realism.personas import login_for from decnet.realism.taxonomy import ContentClass, Plan log = get_logger("canary.cultivator") # realism content_class → canary generator name. Mirrors # :data:`decnet.canary.factory.KNOWN_GENERATORS`. _CLASS_TO_GENERATOR: dict[ContentClass, str] = { ContentClass.CANARY_AWS_CREDS: "aws_creds", ContentClass.CANARY_ENV_FILE: "env_file", ContentClass.CANARY_GIT_CONFIG: "git_config", ContentClass.CANARY_SSH_KEY: "ssh_key", ContentClass.CANARY_HONEYDOC: "honeydoc", ContentClass.CANARY_HONEYDOC_DOCX: "honeydoc_docx", ContentClass.CANARY_HONEYDOC_PDF: "honeydoc_pdf", ContentClass.CANARY_MYSQL_DUMP: "mysql_dump", ContentClass.CANARY_FINGERPRINT_HTML: "fingerprint_html", ContentClass.CANARY_FINGERPRINT_SVG: "fingerprint_svg", } # Generator → CanaryKind. The trip surface (HTTP slug callback / DNS # resolution / passive bait) determines how the canary worker matches # an attacker callback to this token. Aligned with # :data:`decnet.web.db.models.canary.CanaryKind`. _GENERATOR_TO_KIND: dict[str, str] = { "aws_creds": "aws_passive", # no embedded callback; passive bait "env_file": "http", "git_config": "http", "honeydoc": "http", "honeydoc_docx": "http", "honeydoc_pdf": "http", "ssh_key": "dns", # trip is DNS resolution of host comment "mysql_dump": "dns", # trip is DNS resolution of subdomain "fingerprint_html": "http", # obfuscated JS beacons GET /c/ "fingerprint_svg": "http", # same, embedded inside SVG