Contributing¶
Contributions welcome — the extension seams (decoy types, threat feeds, alert notifiers) are deliberately small and uniform, so most features land as one focused class plus tests.
Running the project locally¶
The repo doubles as the dev/demo project: the installable package lives in honeydj/, and config/ holds a ready-made project harness (settings, URLs, ASGI, Celery app) that is not shipped to PyPI.
git clone https://github.com/bistasulove/honeydj.git
cd honeydj
cp .env.example .env
docker compose up
That brings up PostgreSQL 16, Redis 7, Daphne on port 8000, a Celery worker, and Celery beat, with migrations applied automatically.
Tests, lint, and types:
docker compose run --rm web pytest # target: 80%+ coverage
ruff check . && mypy . # config lives in pyproject.toml
To exercise the full pipeline end to end, run python manage.py simulate_scanner (see the Quickstart) and watch the worker logs: docker compose logs -f celery.
Adding a new decoy route type¶
A decoy type pairs a DecoyType choice with a fake response. The rendering and capture logic is shared by the middleware and the fallback views, so there is exactly one place to add each piece:
- Add the choice to
DecoyRoute.DecoyType(honeydj/honeypot/models.py) and toHoneyEvent.DecoyType(honeydj/events/models.py) — the event model records the type on every hit. Generate migrations for both. - Add the fake response in
render_decoy()(honeydj/honeypot/decoys.py). House rules: convincing content, never a 404, realisticServer/X-Powered-Byheaders, and make it a terminal page — no redirects that could self-loop when a broad regex route matches the redirect target. - Optionally seed a default route for it in the
seed_decoy_routesmanagement command, and add a fallback view/URL inhoneydj/honeypot/views.py+urls.pyif the path should be trapped even with no DecoyRoute row in the database. - Test both entry points: a middleware-matched request and (if you added one) the fallback view produce identical responses and equivalent events.
Adding a new threat feed adapter¶
Covered in detail with a full worked example in Threat feeds — writing a custom adapter. The short version:
- Subclass
ThreatFeedAdapterinhoneydj/feeds/adapters/yourfeed.py— implementlookup(ip)returning aThreatFeedResultorNone. Never raise: any network/parse failure is logged and swallowed. - Add a matching choice to
ThreatFeedEntry.Source(honeydj/feeds/models.py) + migration —result.sourceis stored verbatim there. - Append an instance to
FEED_REGISTRYinhoneydj/feeds/registry.py. - Expose the API key via
apply_honeydj_settings' defaults (honeydj/contrib/quickstart.py) so the adapter self-disables cleanly when unconfigured. - Tests: mock the HTTP layer (the suite uses
responses); cover the happy path, a 429, a network error, and the unconfigured case.
Adding a new alert notifier¶
- Write a class in
honeydj/alerts/notifiers.pysatisfying theNotifierprotocol:send(rule, attacker, event) -> bool. ReturnTrueonly on confirmed delivery; catch your own failures, log, and returnFalse— notifiers never raise (they run on the tail of enrichment). Build the message from the shared_summary_lines()/_build_payload()helpers so every channel reports the same facts. - Add the choice to
AlertRule.NotifierType(honeydj/alerts/models.py) - migration, and map it in
NOTIFIER_REGISTRY. - Document the expected
notifier_configkeys (follow the existing pattern: fail with a logged error andFalsewhen a required key is missing). - Verify the admin's Send test alert action works with your channel — that's the first thing an operator will try.
PR process and code style¶
- Branch from
master; open a PR againstmaster. CI (.github/workflows/ci.yml) runs ruff, mypy, and pytest with a coverage gate on every push and PR — all three must pass. - Conventional commits:
feat:,fix:,refactor:,test:,docs:. - Type hints on all functions — the codebase is fully typed and mypy-checked (django-stubs).
- Django ORM only — no raw SQL unless a migration truly requires it.
- Celery tasks must be idempotent — retries and duplicate deliveries happen; design for them (see
enrich_eventfor the pattern). - No
print()— use Django logging (logger = logging.getLogger(__name__)). - Tests target 80%+ coverage; new features ship with tests.
- Docs live in
docs/site/(this site). If your change alters behaviour described here, update the page in the same PR.