Repository navigation
Development
Ruff, mypy, pytest, one alembic head — the gate that keeps main green, and CI runs the exact same one.
uv sync # installs into .venv, including dev dependenciesNo bare-metal run path (see Installation) — but tests run
against in-memory SQLite, not real Postgres, so uv sync alone is enough
for lint/type-check/tests.
uv run pytest # full suite
uv run pytest tests/test_x.py -k y # one test
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy app alembic tests # type check (strict for app/, alembic/)CI runs this same gate on every push/PR
(.github/workflows/ci.yml) — ruff, mypy, the full test suite, and the
single-alembic-head check, plus a separate pip-audit pass over exactly
what uv.lock would install. Dependabot
watches for newer fixed versions of Python, Docker, and GitHub Actions
dependencies on top of that. Nothing here needs a real Postgres/Redis —
see tests/conftest.py.
CodeQL (codeql.yml) scans Python, the app's JavaScript and the
workflows; .github/codeql/codeql-config.yml leaves out vendored bundles,
generated migrations and the Jinja templates (misread as JS), plus
py/unused-import, which ruff already enforces. Models never import each
other at the top of a module: type-checking-only imports sit in an
if TYPE_CHECKING: block at the bottom, and an enum a model needs at
class-definition time lives in its own model-free module
(app/db/models/access_level.py, notification_channel.py), so CodeQL's
"Module-level cyclic import" stays clean.
fuzz/targets.py holds one target per parser of input the app doesn't
control — honeypot command output (app.ssh.*), the OpenCanary event log
(canary_log, canary_event), opencanary.conf, pasted SSH keys, the
redirect guard, the event-list time filter and tags. Each checks a
contract: parsers never raise, an OpenCanary line always becomes an event
the database accepts (bounded strings, a real port or none), guards
reject only with their documented error, and what the redirect guard lets
through is a same-site path.
fuzz.yml
runs every target with Atheris
(coverage-guided, Linux only) for a minute on each PR and 20 minutes
weekly; tests/test_fuzz_targets.py runs the same targets over a seed
corpus in the normal suite on every OS. Locally on Linux:
uv sync --group fuzz, then
uv run python -m fuzz.run <target> -max_total_time=60. A new parser of
untrusted input gets a target (and a matrix entry — the test checks the two
match); an input a crash was fixed for goes into the test's _SEEDS.
e2e/ opens the app in Chromium (Playwright) and fails on anything the
browser console calls an error — a CSP violation, an uncaught script
error, a static file that does not load — which the Python suite cannot
see. It also tries a few interactions that need the page's scripts (the
/ search shortcut, a chart tooltip, drag-to-zoom).
- Runs in CI as the
browserjob; it is not part of a plainpytest. - Locally:
uv sync --group e2e,uv run playwright install chromium,uv run pytest e2e. -
e2e/serve.pyis the app on in-memory SQLite with a small fixed data set (no Postgres/Redis/Celery; tasks a page waits for return a canned result).python -m e2e.servealso works on its own for looking at a change by hand; it prints aSESSION_TOKENfor thesessioncookie. - A new page: add its path to
PAGESine2e/test_pages.py.
The two apps are meant to look and work the same. The files listed in
shared-ui.json (chart code, script and template, the range picker, the
acknowledgement panel, tab navigation, search route and API, the
time-window helper, a few small scripts) are word-for-word identical to
debcontrol's, each with a SHA-256.
-
tests/test_shared_ui.pyfails when a listed file changes here only; the weekly Shared files workflow compares the two repositories. - Change a shared file in debcontrol first, then there:
python scripts/sync_shared_ui.py --updateandpython scripts/sync_shared_ui.py --to <this checkout>; open a PR in both repositories. - Nothing app-specific goes into a shared file. Translation keys under
monitoring.*,ack.*andsearch.*exist in both apps for that.
/honeypots is split by area, like debcontrol's /machines:
honeypots.py only includes honeypots_list, honeypots_detail,
honeypots_updates, honeypots_monitoring, honeypots_activity,
honeypots_logs and honeypots_config (each with its own router from
honeypots_common.honeypots_router()), in an order that puts fixed paths
(/new, /bulk/...) before /{id}/....
uv run alembic revision --autogenerate -m "Add honeypot.notes"Always read the generated file — autogenerate misses things (a renamed column looks like a drop + an add; a server-side default on an existing table needs a data migration if the column isn't nullable). Then:
uv run alembic upgrade head
uv run alembic heads # must show exactly one head before committingEvery new/changed model must also be imported in
app/db/models/__init__.py and
alembic/env.py — a model not imported there is
invisible to --autogenerate.
- Depend on
app.auth.dependencies.get_current_user(read) orrequire_write(mutating). - Resolve the
company_idin question and callapp.auth.scope.ensure_company_access(user, company_id, write=...)— or, for a list view, filter byapp.auth.scope.visible_company_ids(user)(a set of ids;Nonefor a superadmin means "no filter"). - If it's a mutation, wrap it with CSRF (
Depends(verify_csrf)on the route, or the router-leveldependencies=[...]if every route on it mutates) and callapp.audit.log_event(...)after the commit. - Add the equivalent i18n keys to every
app/i18n/locales/*.jsonfile if the page has user-facing strings going throught().
Wrap it in {{ t(request, "area.key") }} in the template, then add
"area.key" to app/i18n/locales/en.json and cs.json (a missing key
in one locale falls back to English, but a key added to only one file is
an oversight, not a feature — see app/i18n/__init__.py's docstring).
A string with a count takes count= —
t(request, "dashboard.companies_count", count=n) — and may have
key.one and (in Czech) key.few variants next to the plain key, which
is the form for 0 and for five or more: "1 firma", "3 firmy", "5 firem".
Never write "honeypot(s)" or "událost(í)".
Czech texts use formal address (vykání), Czech names for every page
and tab (Přehled, Nastavení, Inicializace, …), and "událost" for an
OpenCanary event — not "event" or "alert"; "upozornění" is a
notification. tests/test_czech_texts.py trips on the common slips.
docker compose up -d --buildis the only supported way — see Installation. No
uvicorn app.main:app --reload path yet (the app needs real Postgres/
Redis, and there's no hot-reload compose override); add one
(docker-compose.override.yml mounting ./app, running uvicorn --reload) if that workflow turns out to matter.