Development¶
For changing the code rather than running it: what to install, where things live, which gates to pass, and the two workflows that bite people.
Toolchain¶
| Tool | Version | Used by |
|---|---|---|
| Docker + Compose | any with docker compose |
the whole stack |
| just | any recent | every recipe below |
| uv | 0.11.12 in the images | api/, ingestor/ |
| pnpm | 10.33.0, pinned via packageManager |
dashboard/ |
| Python | 3.13 minimum; images and CI use 3.14 | api/, ingestor/ |
| Node | 26 in CI and the dashboard image | dashboard/ |
uv fetches its own Python. Ruff and pyright target 3.13 in both
pyproject.toml files, so code that only works on 3.14 is still linted
against 3.13.
Repository Layout¶
| Path | What it is |
|---|---|
api/ |
Flask API, SQLAlchemy models, Alembic migrations, committed openapi.json |
ingestor/ |
Tails Cowrie's JSON log into Postgres |
dashboard/ |
Vue 3 SPA plus the nginx image that serves it |
cowrie/ |
Honeypot persona: config, credentials, filesystem pickle, canned output, egress proxy |
postgres/ |
Role-split init scripts and pg_hba.conf, used by docker-compose.prod.yml |
proxy/ |
nginx config for the production compose file |
k8s/ |
Manifests for one public instance. See Operating This Instance |
scripts/ |
fetch-mmdb.sh and a Cowrie fixture regenerator |
docs/ |
This site |
justfile holds the top-level recipes; db.just is a module, reached as
just db <recipe>.
Tests¶
The Python tests use a honeywatch_test database on the dev Postgres
container. just db test-init creates it; just db test-reset recreates it.
| Command | What it does |
|---|---|
just test |
The full local gate, below |
just test-api |
pytest in api/ |
just test-ingestor |
pytest in ingestor/ |
just pnpm test |
vitest run; add --coverage for the thresholds |
just pnpm typecheck |
vue-tsc --noEmit |
just pnpm e2e |
Playwright plus axe, previewing the built SPA on 4173 |
just check |
just test then just openapi-check |
just test runs just db test-init; ruff check, ruff format --check and
pyright in both Python trees; both pytest suites; then
pnpm install --frozen-lockfile, lint, build and test --coverage.
just test is not the whole of CI
It skips pnpm typecheck and pnpm e2e, and both are CI gates. Run
them before you push. Trivy and CodeQL are CI-only too.
pnpm lint is eslint with zero warnings allowed plus an ASCII check that
rejects any non-ASCII byte in src/**. Symbols go in as HTML entities or
inline SVG. Generated client code is exempt.
The OpenAPI Drift Gate¶
api/openapi.json and the generated client in dashboard/src/api/generated/
are both committed and must match what the routes produce.
just api-openapi # dump the spec
just openapi-regen # the above, then regenerate the TS client
just openapi-check # regen, then fail if either output drifted
The gate stages files
just openapi-check runs git add on those two paths and then
git diff --cached --exit-code, so it can never pass while you have
uncommitted changes in them, including changes you made on purpose.
Commit the regenerated files first, then run the check.
CI runs the same sequence on the api leg of the Python matrix, so a spec
mismatch reports as a failure of python (api). A separate spec-lint job
runs Spectral against the spec.
The spec served at /api/v1/openapi.json is built once at boot and cached, so
a running container keeps serving the old spec after a route change until
you restart it.
Migrations¶
docker compose up runs api-migrate before the API and ingestor start, so
the dev database is always at head. The recipes below are for working on
migrations themselves.
| Command | What it does |
|---|---|
just db upgrade |
alembic upgrade head, without recreating containers |
just db downgrade |
One revision back |
just db heads |
The head revision in api/alembic/versions/, not what the database has applied |
just db revision "message" |
Autogenerate from the model diff |
just db revision-empty "message" |
Empty revision, for data migrations and CONCURRENTLY work |
just db shell |
psql into the dev database |
All of these run docker compose exec against the dev stack, so bring it up
first. Read an autogenerated revision before applying it; Alembic emits drops
for anything it does not recognise.
Index migrations wrap CREATE INDEX CONCURRENTLY in an autocommit block so
the ingestor can keep writing. On a large table that looks like a hung
migration. Let it finish.
Seeding Data¶
just seed # synthetic data, WIPES every table first
just seed-live # drips new sessions so the live feed and map move
Both run on the host through uv and connect to localhost:5433. just attack
opens an SSH session against the local Cowrie instead; use a credential
userdb.txt accepts, such as root / changeme, for a successful login.
Geolocation needs the MaxMind databases: just fetch-mmdb with
MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY in .env. To re-run enrichment
over existing IPs, use the container:
The host recipe just reclassify-geoip needs GEOIP_DATA_DIR=data (relative
to ingestor/, where it runs), otherwise it looks for the files at the
container path and finds nothing.
CI¶
ci.yml runs on pushes and pull requests to master, plus a weekly cron.
| Job | What it runs |
|---|---|
python (api), python (ingestor) |
ruff check, ruff format --check, pyright, pytest against a Postgres service. The api leg also runs the OpenAPI gate |
spec-lint |
Spectral against api/openapi.json |
dashboard |
install --frozen-lockfile, lint, typecheck, build, test --coverage, Playwright e2e |
image-scan |
Builds each image and scans it with Trivy; exits 1 on any CRITICAL or HIGH |
The weekly cron runs only image-scan, so a red build on a Sunday is usually
a newly published CVE in a base image, not your code.
codeql.yml analyses Python and TypeScript. docs.yml builds this site with
mkdocs build --strict when docs/** or mkdocs.yml change. A page
missing from the nav is only an INFO message, so add new pages to
mkdocs.yml yourself.
Releases are triggered by a v*.*.* tag. just bump-version <version>
writes the version into both pyproject.toml files and package.json,
re-locks, and opens a commit.