feat(canary): operator-upload instrumenters + tests
Seven instrumenters that mutate operator-supplied artifacts to
embed the callback URL:
- passthrough — bytes unchanged; only DNS-callback tokens trip
detection, with the slug embedded in the placement path
- plain — substitutes {{CANARY_URL}}/{{CANARY_HOST}} placeholders;
falls back to appending a comment line whose prefix adapts to the
apparent file syntax (#, //, ;)
- html — injects a 1x1 tracking pixel before </body>, appends
if the close tag is missing
- docx — direct zipfile manipulation (no python-docx dep):
inserts an external-image Relationship into word/_rels/document.xml.rels
and a matching <w:drawing> element before </w:body>
- xlsx — sibling of docx; injects an external-image relationship
into xl/_rels/workbook.xml.rels (orphan rels are still fetched on
open by most viewers)
- pdf — uses pikepdf to install /OpenAction /URI on the catalog;
rejects with a clear message when pikepdf isn't installed
- image — uses Pillow to embed slug + URL in PNG tEXt / JPEG
comment; rejects with a clear message when Pillow isn't installed
DOCX and XLSX share the rId allocator + relationship injector via
the docx module; both work on stdlib zipfile only.
Tests synthesise minimal real DOCX/XLSX fixtures inline, round-trip
each instrumenter, and assert the callback URL ends up in the
mutated bytes while the file still parses.
This commit is contained in:
37
decnet/canary/instrumenters/passthrough.py
Normal file
37
decnet/canary/instrumenters/passthrough.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Passthrough instrumenter — bytes go to disk unchanged.
|
||||
|
||||
Used as the dispatch fallback for content types we can't safely
|
||||
mutate (random binary blobs, container images, archives we don't
|
||||
recognise). In passthrough mode the only callback surface is the
|
||||
:attr:`CanaryToken.placement_path` itself: the operator must use a
|
||||
DNS-callback token whose slug appears in the filename, so a
|
||||
listing/access at the OS level resolves the slug as part of the
|
||||
path (e.g. ``/etc/<slug>.canary.example.test/secrets.bin``) when
|
||||
the attacker greps for hostnames in their loot.
|
||||
|
||||
The instrumenter does not enforce that — the API does, when it sees
|
||||
``instrumenter=passthrough`` with ``kind=http`` it returns 400.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from decnet.canary.base import CanaryArtifact, CanaryContext, CanaryInstrumenter
|
||||
|
||||
|
||||
class PassthroughInstrumenter(CanaryInstrumenter):
|
||||
name = "passthrough"
|
||||
mime_prefixes = () # dispatched by fallback in pick_instrumenter_for_mime
|
||||
|
||||
def instrument(
|
||||
self, blob: bytes, ctx: CanaryContext, *, target_path: str,
|
||||
) -> CanaryArtifact:
|
||||
return CanaryArtifact(
|
||||
path=target_path,
|
||||
content=blob,
|
||||
mode=0o644,
|
||||
mtime_offset=-86400 * 7,
|
||||
instrumenter=self.name,
|
||||
notes=[
|
||||
"passthrough: bytes unchanged — only DNS-callback tokens "
|
||||
"trip detection (slug must live in the placement path)",
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user