Skip to content

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:

  1. Add the choice to DecoyRoute.DecoyType (honeydj/honeypot/models.py) and to HoneyEvent.DecoyType (honeydj/events/models.py) — the event model records the type on every hit. Generate migrations for both.
  2. Add the fake response in render_decoy() (honeydj/honeypot/decoys.py). House rules: convincing content, never a 404, realistic Server/X-Powered-By headers, and make it a terminal page — no redirects that could self-loop when a broad regex route matches the redirect target.
  3. Optionally seed a default route for it in the seed_decoy_routes management command, and add a fallback view/URL in honeydj/honeypot/views.py + urls.py if the path should be trapped even with no DecoyRoute row in the database.
  4. 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:

  1. Subclass ThreatFeedAdapter in honeydj/feeds/adapters/yourfeed.py — implement lookup(ip) returning a ThreatFeedResult or None. Never raise: any network/parse failure is logged and swallowed.
  2. Add a matching choice to ThreatFeedEntry.Source (honeydj/feeds/models.py) + migration — result.source is stored verbatim there.
  3. Append an instance to FEED_REGISTRY in honeydj/feeds/registry.py.
  4. Expose the API key via apply_honeydj_settings' defaults (honeydj/contrib/quickstart.py) so the adapter self-disables cleanly when unconfigured.
  5. 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

  1. Write a class in honeydj/alerts/notifiers.py satisfying the Notifier protocol: send(rule, attacker, event) -> bool. Return True only on confirmed delivery; catch your own failures, log, and return False — 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.
  2. Add the choice to AlertRule.NotifierType (honeydj/alerts/models.py)
  3. migration, and map it in NOTIFIER_REGISTRY.
  4. Document the expected notifier_config keys (follow the existing pattern: fail with a logged error and False when a required key is missing).
  5. 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 against master. 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_event for 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.