Replaces LICENSE (GPLv3 -> AGPLv3) and prepends `SPDX-License-Identifier: AGPL-3.0-or-later` to every source file across decnet/, decnet_web/, tests/, scripts/, and tools/. Rationale: closes the GPLv3 ASP loophole so any party operating a modified DECNET as a network service must offer their modified source. Personal copyright (Samuel Paschuan) + inbound=outbound contributions make a future unilateral relicense infeasible. - LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt) - COPYRIGHT: project copyright notice - tools/add_spdx_headers.py: idempotent header injector (shebang- and PEP 263-aware) Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh). No behavior change; comments only.
74 lines
2.8 KiB
Python
74 lines
2.8 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Image instrumenter — requires :mod:`PIL` (optional dependency).
|
|
|
|
For PNG/JPEG/GIF we append a tEXt/EXIF chunk carrying the slug so
|
|
``exiftool`` / ``identify -verbose`` surface the slug, then route the
|
|
detection via a sibling **plain-text companion file**. The image
|
|
itself can't really embed an HTTP fetcher — image decoders don't
|
|
run network requests on decode — so the realistic detection surface
|
|
is "attacker exfils the image, runs metadata tools on it, hits our
|
|
URL when curious about the embedded marker."
|
|
|
|
When Pillow isn't installed we reject and direct the operator to
|
|
``passthrough`` (which preserves the bytes; the slug then lives in
|
|
the filename only).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
|
|
from decnet.canary.base import (
|
|
CanaryArtifact,
|
|
CanaryContext,
|
|
CanaryInstrumenter,
|
|
InstrumenterRejectedError,
|
|
)
|
|
|
|
|
|
class ImageInstrumenter(CanaryInstrumenter):
|
|
name = "image"
|
|
mime_prefixes = ("image/png", "image/jpeg", "image/gif")
|
|
|
|
def instrument(
|
|
self, blob: bytes, ctx: CanaryContext, *, target_path: str,
|
|
) -> CanaryArtifact:
|
|
try:
|
|
from PIL import Image, PngImagePlugin
|
|
except ImportError as e:
|
|
raise InstrumenterRejectedError(
|
|
"image instrumenter requires Pillow; install it (`pip "
|
|
"install Pillow`) or re-upload the artifact with "
|
|
"kind=passthrough so it ships unmodified."
|
|
) from e
|
|
|
|
slug_url = f"{ctx.http_base.rstrip('/')}/c/{ctx.callback_token}"
|
|
try:
|
|
buf_in = io.BytesIO(blob)
|
|
img = Image.open(buf_in)
|
|
fmt = (img.format or "").upper()
|
|
buf_out = io.BytesIO()
|
|
if fmt == "PNG":
|
|
meta = PngImagePlugin.PngInfo()
|
|
meta.add_text("Comment", f"reference: {slug_url}")
|
|
meta.add_text("X-Canary", ctx.callback_token)
|
|
img.save(buf_out, format="PNG", pnginfo=meta)
|
|
elif fmt in ("JPEG", "JPG"):
|
|
# Pillow encodes JPEG comments via the ``comment`` kwarg.
|
|
img.save(buf_out, format="JPEG", comment=slug_url.encode())
|
|
else:
|
|
# GIF and friends — Pillow doesn't expose comment metadata
|
|
# uniformly. Re-encode as-is and skip the metadata embed.
|
|
img.save(buf_out, format=fmt or "PNG")
|
|
mutated = buf_out.getvalue()
|
|
except Exception as e:
|
|
raise InstrumenterRejectedError(f"failed to instrument image: {e!s}") from e
|
|
|
|
return CanaryArtifact(
|
|
path=target_path,
|
|
content=mutated,
|
|
mode=0o644,
|
|
mtime_offset=-86400 * 30,
|
|
instrumenter=self.name,
|
|
notes=[f"image metadata carries {slug_url} (slug={ctx.callback_token})"],
|
|
)
|