feat(canary): fingerprint_html + fingerprint_svg generators

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.
This commit is contained in:
2026-04-29 16:22:18 -04:00
parent 12cd7ad9cb
commit f64e78f78c
6 changed files with 363 additions and 0 deletions

View File

@@ -9,12 +9,31 @@ the artifact" property.
from __future__ import annotations
import re
import shutil
from pathlib import Path
import pytest
from decnet.canary import CanaryContext, get_generator
from decnet.canary.factory import KNOWN_GENERATORS
# fingerprint_* generators shell out to javascript-obfuscator via Node.
# Skip those parametrized cases when the toolchain isn't installed so a
# bare CI checkout doesn't fail before `npm install` runs.
_NEEDS_NODE = {"fingerprint_html", "fingerprint_svg"}
def _node_toolchain_ready() -> bool:
if shutil.which("node") is None:
return False
canary_dir = Path(__file__).resolve().parents[2] / "decnet" / "canary"
return (canary_dir / "node_modules" / "javascript-obfuscator").is_dir()
def _maybe_skip(name: str) -> None:
if name in _NEEDS_NODE and not _node_toolchain_ready():
pytest.skip(f"{name} requires node + javascript-obfuscator")
def _ctx(**kw) -> CanaryContext:
defaults = dict(
@@ -29,6 +48,7 @@ def _ctx(**kw) -> CanaryContext:
@pytest.mark.parametrize("name", KNOWN_GENERATORS)
def test_generator_is_deterministic(name: str) -> None:
_maybe_skip(name)
g = get_generator(name)
a = g.generate(_ctx())
b = g.generate(_ctx())
@@ -184,5 +204,7 @@ def test_artifacts_carry_notes() -> None:
# check what we did before the file lands. Empty notes would mean
# the operator is staring at opaque bytes.
for name in KNOWN_GENERATORS:
if name in _NEEDS_NODE and not _node_toolchain_ready():
continue
art = get_generator(name).generate(_ctx())
assert art.notes, f"{name} produced no notes"