feat(realism): wire fingerprint_html/svg through taxonomy + UI

The two new fingerprint canary generators existed at the API level
since f64e78f but weren't visible to the realism engine or the
operator-facing dashboard. Threads them through every place that
enumerates canary content classes.

Backend:
* realism/taxonomy.py - two new ContentClass members
  (CANARY_FINGERPRINT_HTML, CANARY_FINGERPRINT_SVG); enum is
  wire-visible (synthetic_files.content_class column + bus discrim)
  so we add at the bottom, never reorder.
* canary/cultivator.py - class-to-generator dispatch, kind mapping
  (both http), and default placement paths
  (~/Documents/asset_directory.html and network_topology.svg).
* realism/naming.py + bodies.py - _name_canary / _body_canary entries.
* realism/planner.py - added to _DEFAULT_CANARY_CLASS_WEIGHTS and
  the _CANARY_CLASSES classification set.

Frontend:
* decnet_web/src/realism/labels.ts - display labels.
* decnet_web/src/components/RealismConfig/RealismConfig.tsx - default
  canary weight rows so operators see them in the realism config UI.
* decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx - added
  to the CONTENT_CLASSES allow-list so filter dropdowns show them.

Also: re-applied the nosec B404/B603 markers on canary/obfuscator.py;
the first commit's pre-commit autoformatter stripped them.

Tests: extended tests/realism/test_taxonomy.py's stability assertion
to include the two new values. Full canary + realism suites pass
(362 / 2 skipped).
This commit is contained in:
2026-04-29 16:44:03 -04:00
parent de6d5cd1a8
commit 907ade9142
10 changed files with 23 additions and 2 deletions

View File

@@ -46,6 +46,8 @@ _CLASS_TO_GENERATOR: dict[ContentClass, str] = {
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",
}
@@ -62,6 +64,8 @@ _GENERATOR_TO_KIND: dict[str, str] = {
"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/<slug>
"fingerprint_svg": "http", # same, embedded inside SVG <script>
}
@@ -78,6 +82,8 @@ _DEFAULT_PATH: dict[ContentClass, str] = {
ContentClass.CANARY_HONEYDOC_DOCX: "/home/{persona}/Documents/Q3-Operations-Review.docx",
ContentClass.CANARY_HONEYDOC_PDF: "/home/{persona}/Documents/Q3-Operations-Review.pdf",
ContentClass.CANARY_MYSQL_DUMP: "/var/backups/db_backup.sql",
ContentClass.CANARY_FINGERPRINT_HTML: "/home/{persona}/Documents/asset_directory.html",
ContentClass.CANARY_FINGERPRINT_SVG: "/home/{persona}/Documents/network_topology.svg",
}

View File

@@ -24,7 +24,7 @@ from __future__ import annotations
import hashlib
import json
import os
import subprocess
import subprocess # nosec B404 — Node helper exec is the whole point
from pathlib import Path
from typing import Any
@@ -103,7 +103,7 @@ def obfuscate(code: str, *, callback_token: str) -> str:
options = _config_from_seed(seed)
payload = json.dumps({"code": code, "options": options})
try:
proc = subprocess.run(
proc = subprocess.run( # nosec B603 — argv-form, no shell, fixed helper path; payload is JSON on stdin, not in argv
[_NODE_BIN, str(_HELPER)],
input=payload, capture_output=True, text=True,
timeout=_TIMEOUT_S, check=False,

View File

@@ -213,6 +213,8 @@ _BODIES: dict[ContentClass, Callable[[str, secrets.SystemRandom], str]] = {
ContentClass.CANARY_HONEYDOC_DOCX: _body_canary,
ContentClass.CANARY_HONEYDOC_PDF: _body_canary,
ContentClass.CANARY_MYSQL_DUMP: _body_canary,
ContentClass.CANARY_FINGERPRINT_HTML: _body_canary,
ContentClass.CANARY_FINGERPRINT_SVG: _body_canary,
}

View File

@@ -159,6 +159,8 @@ _NAMERS: dict[ContentClass, Callable[[str, secrets.SystemRandom], str]] = {
ContentClass.CANARY_HONEYDOC_DOCX: _name_canary,
ContentClass.CANARY_HONEYDOC_PDF: _name_canary,
ContentClass.CANARY_MYSQL_DUMP: _name_canary,
ContentClass.CANARY_FINGERPRINT_HTML: _name_canary,
ContentClass.CANARY_FINGERPRINT_SVG: _name_canary,
}

View File

@@ -62,6 +62,8 @@ _DEFAULT_CANARY_CLASS_WEIGHTS: tuple[tuple[ContentClass, int], ...] = (
(ContentClass.CANARY_HONEYDOC_DOCX, 1),
(ContentClass.CANARY_HONEYDOC_PDF, 1),
(ContentClass.CANARY_MYSQL_DUMP, 1),
(ContentClass.CANARY_FINGERPRINT_HTML, 1),
(ContentClass.CANARY_FINGERPRINT_SVG, 1),
)
_DEFAULT_CANARY_PROBABILITY = 0.03
@@ -134,6 +136,7 @@ _CANARY_CLASSES: set[ContentClass] = {
ContentClass.CANARY_GIT_CONFIG, ContentClass.CANARY_SSH_KEY,
ContentClass.CANARY_HONEYDOC, ContentClass.CANARY_HONEYDOC_DOCX,
ContentClass.CANARY_HONEYDOC_PDF, ContentClass.CANARY_MYSQL_DUMP,
ContentClass.CANARY_FINGERPRINT_HTML, ContentClass.CANARY_FINGERPRINT_SVG,
}

View File

@@ -62,6 +62,8 @@ class ContentClass(StrEnum):
CANARY_HONEYDOC_DOCX = "canary_honeydoc_docx"
CANARY_HONEYDOC_PDF = "canary_honeydoc_pdf"
CANARY_MYSQL_DUMP = "canary_mysql_dump"
CANARY_FINGERPRINT_HTML = "canary_fingerprint_html"
CANARY_FINGERPRINT_SVG = "canary_fingerprint_svg"
def is_canary(self) -> bool:
return self.value.startswith("canary_")

View File

@@ -45,6 +45,8 @@ const DEFAULTS: ConfigPayload = {
{ content_class: 'canary_honeydoc_docx', weight: 1 },
{ content_class: 'canary_honeydoc_pdf', weight: 1 },
{ content_class: 'canary_mysql_dump', weight: 1 },
{ content_class: 'canary_fingerprint_html', weight: 1 },
{ content_class: 'canary_fingerprint_svg', weight: 1 },
],
canary_probability: 0.03,
};

View File

@@ -52,6 +52,7 @@ const CONTENT_CLASSES = [
'canary_aws_creds', 'canary_env_file', 'canary_git_config',
'canary_ssh_key', 'canary_honeydoc', 'canary_honeydoc_docx',
'canary_honeydoc_pdf', 'canary_mysql_dump',
'canary_fingerprint_html', 'canary_fingerprint_svg',
] as const;
// ─── Helpers ─────────────────────────────────────────────────────────────────

View File

@@ -26,6 +26,8 @@ const LABELS: Record<string, string> = {
canary_honeydoc_docx: 'Canary · DOCX Honeydoc',
canary_honeydoc_pdf: 'Canary · PDF Honeydoc',
canary_mysql_dump: 'Canary · MySQL Dump',
canary_fingerprint_html: 'Canary · Fingerprint HTML',
canary_fingerprint_svg: 'Canary · Fingerprint SVG',
};
export function contentClassLabel(value: string): string {

View File

@@ -26,6 +26,7 @@ def test_content_class_values_are_stable() -> None:
"canary_aws_creds", "canary_env_file", "canary_git_config",
"canary_ssh_key", "canary_honeydoc", "canary_honeydoc_docx",
"canary_honeydoc_pdf", "canary_mysql_dump",
"canary_fingerprint_html", "canary_fingerprint_svg",
}