feat(canary): kind reflects trip surface per generator
decnet/canary/cultivator wrote kind="http" for every cultivated token, even DNS-trip ones (ssh_key, mysql_dump) and passive bait (aws_creds). The canary worker uses kind to route attacker callbacks to the right token; a misaligned kind means a real DNS resolution of ssh_key or mysql_dump never attributes to the planted slug. Add _GENERATOR_TO_KIND aligned with CanaryKind in models/canary.py and look it up at create_canary_token time.
This commit is contained in:
@@ -49,6 +49,22 @@ _CLASS_TO_GENERATOR: dict[ContentClass, str] = {
|
||||
}
|
||||
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
|
||||
# Path conventions per generator. The realism planner doesn't know
|
||||
# about decoy-realistic credential locations (``~/.aws/credentials``,
|
||||
# ``~/.git/config``); we map them per-class here so the planted
|
||||
@@ -139,7 +155,7 @@ async def cultivate(
|
||||
# itself (improbable but possible — DOCX viewers can preview
|
||||
# autoplay-style).
|
||||
await repo.create_canary_token({
|
||||
"kind": "http", # MVP: all realism-cultivated tokens use HTTP
|
||||
"kind": _GENERATOR_TO_KIND.get(gen_name, "http"),
|
||||
"decky_name": plan.decky_name,
|
||||
"instrumenter": None,
|
||||
"generator": gen_name,
|
||||
|
||||
@@ -112,3 +112,34 @@ async def test_cultivate_artifact_does_not_leak_decnet_string(repo, monkeypatch)
|
||||
f"{cls.value!r} body leaked 'decnet': "
|
||||
f"{body[:120]!r}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cultivate_records_kind_per_generator(repo, monkeypatch):
|
||||
"""The token row's ``kind`` reflects the trip surface of the
|
||||
underlying generator: HTTP slug callback, DNS resolution, or
|
||||
passive bait. The canary worker uses ``kind`` to route incoming
|
||||
callbacks; a wrong kind means the trip won't attribute correctly."""
|
||||
monkeypatch.setenv("DECNET_CANARY_HTTP_BASE", "https://canary.example.test")
|
||||
monkeypatch.setenv("DECNET_CANARY_DNS_ZONE", "canary.example.test")
|
||||
cases = [
|
||||
(ContentClass.CANARY_AWS_CREDS, "aws_passive"),
|
||||
(ContentClass.CANARY_ENV_FILE, "http"),
|
||||
(ContentClass.CANARY_GIT_CONFIG, "http"),
|
||||
(ContentClass.CANARY_HONEYDOC, "http"),
|
||||
(ContentClass.CANARY_HONEYDOC_DOCX, "http"),
|
||||
(ContentClass.CANARY_HONEYDOC_PDF, "http"),
|
||||
(ContentClass.CANARY_SSH_KEY, "dns"),
|
||||
(ContentClass.CANARY_MYSQL_DUMP, "dns"),
|
||||
]
|
||||
for cls, expected_kind in cases:
|
||||
await cultivate(_plan(cls, persona=f"p-{cls.value}"), repo)
|
||||
rows = await repo.list_canary_tokens(decky_name="alpha")
|
||||
by_gen = {r["generator"]: r["kind"] for r in rows}
|
||||
for cls, expected_kind in cases:
|
||||
from decnet.canary.cultivator import _CLASS_TO_GENERATOR
|
||||
gen = _CLASS_TO_GENERATOR[cls]
|
||||
assert by_gen[gen] == expected_kind, (
|
||||
f"{cls.value!r} → generator {gen!r} got kind={by_gen[gen]!r}, "
|
||||
f"want {expected_kind!r}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user