Skip to main content

Supply-chain canary token minting and beacon/trigger detection.

Project description

Canary Kit

Canary Kit detects software supply-chain compromise: it plants uniquely-derived canary tokens in a package and raises a high-signal alert the moment one "fires" — i.e. when the planted code beacons home from an environment you don't control (a leaked build, a typosquat install, an attacker unpacking your release).

A canary token is worthless to the attacker but invaluable to you: legitimate internal use is silent, so any beacon you receive is, by construction, suspicious.

This is the standalone, MIT-licensed extraction of the supply-chain canary core from the WraithWall security platform. It has no Flask, web framework, or network dependency in its core — just the minting, registration, and trigger-detection logic.

Install

pip install .
# optional extras:
pip install ".[redis]"   # inject your own redis client
pip install ".[test]"    # pytest

Requires Python >= 3.10. The core has zero third-party dependencies.

Quickstart

from canary_kit import CanaryRegistry, InMemoryStore

registry = CanaryRegistry(store=InMemoryStore())

# Mint + register a canary for a package you're about to ship.
record = registry.register("acme-internal-utils", "1.4.2", owner="sec-team")
print(record.token)            # e.g. 9f1c... (24 hex chars)

# Later: a beacon arrives carrying a token. Did it match one we issued?
hit = registry.detect(record.token, ip_address="198.51.100.23", env_hash="a1b2")
print(hit.matched)             # True  -> the canary fired
print(hit.record.fire_count)   # 1

# A forged / unknown token never matches.
miss = registry.detect("deadbeefdeadbeefdeadbeef")
print(miss.matched, miss.reason)   # False unknown token

Run the full demo:

python examples/quickstart.py

Public API

# Minting & derivations (stdlib only)
mint_token(package_name: str, version: str, *, salt: str | None = None) -> str
derive_token(package_name: str, version: str, salt: str) -> str  # deterministic
encode_watermark(token: str) -> str          # zero-width watermark of token[:8]
decode_watermark(text: str) -> str | None    # recover token[:8] from text

# Metadata model
class CanaryToken:                            # dataclass
    token, package_name, version, token_type, created_at,
    fired, fire_count, last_fired, fire_ips, fire_environments, extra
    .to_dict() -> dict
    CanaryToken.from_dict(dict) -> CanaryToken

# Token types
TOKEN_TYPE_RUNTIME, TOKEN_TYPE_DNS, TOKEN_TYPE_WATERMARK, TOKEN_TYPES

# Registration + detection
class CanaryRegistry(store: CanaryStore | None = None)
    .register(package_name, version, *, token_type=..., salt=None,
              token=None, **extra) -> CanaryToken
    .get(token) -> CanaryToken | None
    .list_tokens() -> list[str]
    .all_records() -> list[CanaryToken]
    .detect(token, *, ip_address="", env_hash="unknown",
            version="unknown") -> BeaconResult   # alias: report_beacon

class BeaconResult:                           # dataclass
    matched: bool; token: str; record: CanaryToken | None; reason: str

# Pluggable storage (Redis is optional and never auto-connects)
class CanaryStore(Protocol):   put/get/all/tokens
class InMemoryStore(...)        # default, thread-safe
class RedisStore(client, *, prefix="canary_kit:", ttl_seconds=...)  # inject client

CLI usage

# Mint + register a token (persisted to a JSON registry file)
$ python -m canary_kit mint acme-internal-utils 1.4.2 --type runtime
{
  "token": "9f1c2a...",
  "package_name": "acme-internal-utils",
  "version": "1.4.2",
  "token_type": "runtime",
  "fired": false,
  ...
}

# List issued tokens
$ python -m canary_kit list
9f1c2a...  acme-internal-utils@1.4.2  [runtime]  armed x0

# Simulate / record a beacon hit
$ python -m canary_kit beacon 9f1c2a... --ip 198.51.100.23 --env a1b2
MATCH: canary fired for token 9f1c2a...

# Unknown token -> exit code 1
$ python -m canary_kit beacon deadbeef...
NO MATCH: unknown token (token='deadbeef...')

Use --registry PATH to choose the JSON registry file (default .canary_kit_registry.json in the working directory). The installed console script canary-kit is equivalent to python -m canary_kit.

How detection works

  1. Mint — a token is sha256(f"{package}:{version}:{salt}")[:24]. With no explicit salt, a random secrets.token_hex(8) salt makes each token unique; pass a salt to derive a token reproducibly.
  2. Plant & register — you embed that token in your package (as runtime beacon code, a DNS hostname in metadata, or a zero-width docstring watermark) and call registry.register(...) to record its metadata in the store.
  3. Beacon — when the planted artifact runs/installs outside your control, it phones home carrying the token (plus an environment fingerprint and version).
  4. Detectregistry.detect(token, ...) looks the token up in the store. A hit means the canary fired: the record is marked fired, counters and source IPs/environments accumulate, and you get a BeaconResult(matched=True). An unknown/forged token returns matched=False and changes nothing.

Because issued tokens are random and never used in legitimate traffic, a match is an inherently high-signal indicator of a leaked or tampered package.

Limitations

  • Token entropy. The minted token is 24 hex characters (96 bits) from SHA-256. This is sufficient for anyone who cannot brute-force the token space, but a token leaked in source control or a CI log is game-over — rotate the salt and re-mint.
  • No network dependency. Canary Kit does not listen, poll, or ship data anywhere. Beacon detection is a registry lookup you call from your own telemetry pipeline — you must build the ingestion path.
  • Deterministic derivation. Passing an explicit salt makes the token reproducible, which is useful for testing but means anyone who knows the inputs can precompute the token. Use the random-salt default for production.
  • Zero-width watermarking. encode_watermark / decode_watermark embeds the first 8 hex characters of a token as zero-width Unicode characters (U+200B, U+200C, U+200D, U+FEFF). This survives copy-paste but not re-encoding (e.g., minification, OCR, or charset conversion that strips zero-width codepoints).
  • Storage choice is yours. The library ships with an InMemoryStore and a RedisStore adapter, but you can provide any store implementing the CanaryStore protocol. There is no built-in persistence to disk or cloud.

Development

git clone https://github.com/niffyhunt/wraithwall-toolkit
cd wraithwall-toolkit/packages/canary-kit
pip install -e ".[test]"
pytest

Contributions are welcome. The core is deliberately dependency-free; any proposed addition that introduces a runtime dependency should justify it.

License

MIT — see LICENSE. Copyright (c) 2026 niffy_hunt.


Part of the WraithWall project — https://wraithwall.online · by niffy_hunt

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

canary_kit-0.1.0.tar.gz (15.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

canary_kit-0.1.0-py3-none-any.whl (14.7 kB view details)

Uploaded Python 3

File details

Details for the file canary_kit-0.1.0.tar.gz.

File metadata

  • Download URL: canary_kit-0.1.0.tar.gz
  • Upload date:
  • Size: 15.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for canary_kit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4ae2eb0f17149529989c04ac846a3e6b824f04fb2a2ca43ddd3cb2300724a2a4
MD5 53dbbc963f7a90656fdfcb81a48a0a9b
BLAKE2b-256 80a77b82b11dea640aeecfd0623cd9938be21cb75539aa020501d10e1c100d8c

See more details on using hashes here.

File details

Details for the file canary_kit-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: canary_kit-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 14.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for canary_kit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c3cb0b91080319978dec6cbc6f74417eb32504164b86aefc25f94fcec4f8cfc0
MD5 e403be48fe7be296ec611ae2611b9e8d
BLAKE2b-256 cd398cba92cf8b2a6a6dc8a2a826198117aabec87cd711275f430f5c54c92a44

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page