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
Metadata
Release files for canary-kit 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| canary_kit-0.1.0.tar.gz | 15.7 kB | Without provenance | Details for canary_kit-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| canary_kit-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for canary_kit-0.1.0-py3-none-any.whl |
Total release size: 30.4 kB
Release files / canary_kit-0.1.0.tar.gz
| Download URL | canary_kit-0.1.0.tar.gz |
|---|---|
| Size | 15.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4ae2eb0f17149529989c04ac846a3e6b824f04fb2a2ca43ddd3cb2300724a2a4
|
|
BLAKE2b-256 checksum How to use checksums |
80a77b82b11dea640aeecfd0623cd9938be21cb75539aa020501d10e1c100d8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / canary_kit-0.1.0-py3-none-any.whl
| Download URL | canary_kit-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c3cb0b91080319978dec6cbc6f74417eb32504164b86aefc25f94fcec4f8cfc0
|
|
BLAKE2b-256 checksum How to use checksums |
cd398cba92cf8b2a6a6dc8a2a826198117aabec87cd711275f430f5c54c92a44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Security for canary-kit 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.