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
- Mint — a token is
sha256(f"{package}:{version}:{salt}")[:24]. With no explicit salt, a randomsecrets.token_hex(8)salt makes each token unique; pass a salt to derive a token reproducibly. - 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. - Beacon — when the planted artifact runs/installs outside your control, it phones home carrying the token (plus an environment fingerprint and version).
- Detect —
registry.detect(token, ...)looks the token up in the store. A hit means the canary fired: the record is markedfired, counters and source IPs/environments accumulate, and you get aBeaconResult(matched=True). An unknown/forged token returnsmatched=Falseand 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
saltmakes 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_watermarkembeds 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
InMemoryStoreand aRedisStoreadapter, but you can provide any store implementing theCanaryStoreprotocol. 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ae2eb0f17149529989c04ac846a3e6b824f04fb2a2ca43ddd3cb2300724a2a4
|
|
| MD5 |
53dbbc963f7a90656fdfcb81a48a0a9b
|
|
| BLAKE2b-256 |
80a77b82b11dea640aeecfd0623cd9938be21cb75539aa020501d10e1c100d8c
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3cb0b91080319978dec6cbc6f74417eb32504164b86aefc25f94fcec4f8cfc0
|
|
| MD5 |
e403be48fe7be296ec611ae2611b9e8d
|
|
| BLAKE2b-256 |
cd398cba92cf8b2a6a6dc8a2a826198117aabec87cd711275f430f5c54c92a44
|