Older releases

Every release before 1.5.0, newest first. Back to the current releases. Rendered from CHANGELOG.md, the file GitHub and the release tooling read.

1.4.0 — 2026-09-24

Attachments no longer identify the whistleblower, multi-tenant installs keep organisations apart, and every page passes an automated contrast and layout check on a phone and in both themes. Found and verified with a mutation audit of every guard this release adds (28 of 28 caught).

Upgrade notes: migration 002 runs on start. The default BRAND_PRIMARY_COLOR is now #0c7253 (was #0e7c5a, 4.4:1 on tinted backgrounds); installs that set their own colour are unaffected. E2E and demo logins for the demo accounts use the static code 000000; real TOTP codes are single-use for every account.

Fixed

  • Cleaned PDFs still contained their XMP metadata. Unlinking it from the catalog left the stream in the file as an orphaned object; orphans are now removed. PDFs restricted by an owner password only are accepted and cleaned instead of refused.
  • Every page passes axe at impact serious and critical, in both themes. Fixed: accent colour on tinted backgrounds (4.4 → 5.0:1), white on amber in the dark demo badges (2.3:1), dark-theme secondary text (3.9 → 4.8:1), role badges (2.1 and 3.9:1), footer links distinguishable only by colour, inactive categories and locations faded below AA, an unlabelled role selector and link field, scrollable tables unreachable by keyboard.
  • Six more admin pages scrolled sideways on a phone (users, categories, locations, statistics, retention, system — up to 569 px), and long audit entries on the report view.
  • No page scrolls sideways on a phone any more. At 390 px the navigation (12 items in the admin) now wraps instead of running off-screen, the dashboard toolbar and the report view (two columns, no breakpoint) fit the width. An E2E test checks /submit, /status and /admin/login at 390 px.
  • Confidential mode works without JavaScript (Tor Browser "Safest"): the name/contact fields are shown by CSS :has() instead of a script.
  • The language picker works without JavaScript and no longer misuses listbox/option roles: it is a native <details> list of forms.
  • Contrast: input borders 1.25:1 → ≥ 3:1; a visible focus outline on inputs and on the submission-mode cards (focused and selected looked identical); secondary text 4.37:1 → 4.98:1 on cards; alert titles no longer dimmed.

Changed

  • Stricter tests. The axe checks fail on serious violations, not only critical ones (contrast failures used to ship green). A new UI check visits every public and admin page in both themes at 390 and 1440 px and fails on axe violations, console errors or sideways scrolling. Every guard of the release is mutation-tested (scripts/mutation_audit.py).
  • Maintainer documentation moved to docs-tech/ (release procedure, invariants, performance baseline); a test keeps it out of the published site.
  • Images are built once and published identically to all three registries. Each platform builds on a native runner (arm64 no longer under QEMU) and is pushed to GHCR by digest; one multi-arch index is then written under every tag to GHCR, Docker Hub and Quay.io. Docker Hub and Quay now get the same provenance, SBOM and cosign signature as GHCR (before: separate unsigned builds). The job fails unless every tag and every platform manifest behind it is pullable. Quay.io stays best-effort with a warning.
  • Dependencies are locked in uv.lock. The image, CI and the E2E/perf workflows install exactly the locked versions; CI fails if the lock is out of date. Dependabot now uses the uv ecosystem — the old pip entry only saw >= floors and had never opened a pull request.
  • The production image carries runtime dependencies only (no pytest, mypy or ruff), uv is taken from its official image (0.6.0 → 0.12.18), and the fonts are copied from docs/fonts instead of downloaded unverified at build time.
  • python-jose replaced by PyJWT, which drops ecdsa (PYSEC-2026-1325, no fix planned). Unused authlib and aiofiles removed; cryptography is now a declared dependency instead of an accidental transitive one.

Security

  • Multi-tenancy: an org admin now sees and manages only their own organisation. The users page, role changes, (de)activation, the assignment picker and target, the audit log and its CSV export, and the dashboard and statistics counts were unscoped; new users now join the creator's organisation. Single-organisation installs are unaffected.
  • DEMO_MODE no longer weakens a real installation. The static TOTP 000000 is accepted only for the seeded demo accounts, and demo data is not seeded into a database that completed the setup wizard and has no demo account.
  • Internal admin notes are encrypted at rest with the report's data key, like descriptions and messages. Existing notes are shown as stored.
  • Attachments no longer carry identifying metadata. EXIF/GPS and camera data (JPEG, PNG, WebP, GIF), PDF document info and XMP, and DOCX/XLSX author and company properties are removed on upload — before the file reaches the draft store. A file that cannot be parsed for cleaning is refused instead of stored as-is. The upload step tells the whistleblower what is and is not cleaned.
  • Attachments are encrypted at rest with the report's own data key, in PostgreSQL and in S3. Rows from before this release are served as stored (migration 002 adds attachments.encrypted).

See the code changes between 1.3.1 and 1.4.0

1.3.1 — 2026-09-23

Security

  • OIDC logins now require TOTP. The OIDC callback issued a session directly, so SSO accounts skipped the mandatory second factor. All three login paths (local, LDAP, OIDC) now go through the same MFA step; SSO users enrol TOTP on their next login. Deactivated accounts are also rejected on the OIDC path.
  • LDAP first login provisions a Case Manager, not an Admin. Any directory entry matching LDAP_USER_FILTER previously got full admin access.
  • LDAP username is escaped before it is placed into the search filter (filter injection), and LDAPS now verifies the server certificate (CERT_NONE before). Private CAs: set SSL_CERT_FILE.
  • Deleting a report removes its S3 objects. Manual deletion, 4-eyes deletion and the retention job only removed database rows; attachment files in the bucket were kept forever.
  • nginx no longer logs client IPs. error_log ran at warn, and nginx prefixes rate-limit (429) and body-size (413) errors with the client address. It now logs at crit only.
  • "Start over" in the submission wizard is now CSRF-protected.

Fixed

  • Uploads over 1 MB failed behind the bundled nginx (default client_max_body_size). Set to 55 MB (5 × 10 MB attachments).
  • Fresh production installs could not start PostgreSQL 18. The 18 image refuses a volume at /var/lib/postgresql/data unless PGDATA points there; docker-compose.prod.yml and the Ansible template now set it. Existing data is unaffected.
  • GHCR images were unpullable (manifest unknown): the weekly cleanup job deleted the untagged per-platform manifests that every multi-arch tag points to. GHCR cleanup is removed; Docker Hub and Quay.io were not affected.
  • Helm chart deployed v0.5.0 by default — appVersion was never bumped. It now tracks the app version, enforced by a test.
  • "Start over" on the review step returned 405 (GET link to a POST-only route).
  • Footer text had 1.8:1 contrast on the dark footer; now 7.7:1.
  • Animations now respect prefers-reduced-motion.
  • HinSchG citations: the 3-year deletion rule is §11 Abs. 5, not §12 Abs. 3, and it is a deletion deadline, not a minimum retention period.
  • "Back" in the submission wizard no longer triggers validation. The double-submit guard disabled the form's first submit button — which on every wizard step is "Back" — while the browser was still building the submitted entry list. Disabled controls are excluded from that list, so action=back never reached the server and the step was processed as a "Next", rejecting an empty description instead of navigating back. The guard now targets the button the user actually clicked and applies after the form data is collected.

See the code changes between 1.3.0 and 1.3.1

1.3.0 — 2026-07-15

The "Signal" design system — a ground-up visual redesign that unifies the app and the public site under one identity, plus a documented design specification.

Added

  • DESIGN.md — a canonical design system ("Signal") in the google/design.md format: front-matter design tokens (colour, typography, spacing, radii, elevation) and prose covering every component, with first-class light and dark themes.
  • Design token foundation in the stylesheet: a --space- spacing scale, a --text- type scale, and shared .anim-in / .delay-* animation utilities.

Changed

  • Full "Signal" restyle of the application — a monochrome warm-neutral ground with a single emerald accent, Sora for display and body text, and JetBrains Mono for case numbers, PINs, timestamps and deadlines. Dark mode is now a warm near-black rather than a cold navy.
  • Self-hosted fonts reduced to two (Sora + JetBrains Mono); the Docker image downloads exactly those, replacing the previous three-font set. The default BRAND_PRIMARY_COLOR is now emerald (#0e7c5a).
  • Public marketing and documentation pages (openwhistle.net) converted from their separate serif/gold look onto the same Signal tokens.
  • Stylesheet and template cleanup: consolidated duplicated components (.info-banner → .alert, .env-table → table, three admin grids → .admin-split-grid, inline-form wrappers → .inline-form), rebuilt the admin report page's 56 numbered one-off classes into semantic classes on the type scale, and unified the scattered animation-delay helpers.

Fixed

  • The intended body typeface never loaded — its @font-face pointed at font files that did not exist, so the app silently fell back to system-ui. A real self-hosted font now ships.
  • Dark mode ignored the configured brand colour (the accent was hardcoded); it now derives from BRAND_PRIMARY_COLOR in both themes.
  • The public report-status page's status pills were unstyled (they referenced CSS classes that were never defined); they now use the themed badge styles.
  • Removed references to several undefined CSS custom properties.

See the code changes between 1.2.1 and 1.3.0

1.2.1 — 2026-07-14

Follow-up hardening release resolving the remaining bug-bounty findings (#42–#46).

Security

  • CSRF protection extended to the two remaining state-changing admin POST endpoints (/admin/ip-warning/dismiss, /admin/demo/reset). AJAX requests authenticate via an X-CSRF-Token header (read from a <meta> tag, since the double-submit cookie is HttpOnly) (#44).
  • Case numbers now use a random 5-digit suffix instead of a global sequence, so a new report no longer reveals aggregate cross-tenant report volume. Format is unchanged (OW-YYYY-NNNNN) and existing numbers stay valid (#42).
  • Attachment uploads are now verified by magic number: the file's leading bytes must match its extension (PDF, JPEG, PNG, GIF, WebP, DOCX/XLSX, DOC/XLS), so a file cannot lie about its type (e.g. HTML bytes disguised as a .png). Text formats have no signature and are unaffected (#43).
  • Reverse-proxy flood protection for the submission channel: the bundled nginx configs now apply a per-IP limit_req to dynamic endpoints. The IP is used only for in-memory throttling — never logged or forwarded upstream — so whistleblower anonymity is preserved (#46).
  • Removed a dead, unreachable "this account uses Single Sign-On" login branch; SSO-only accounts already receive the generic "invalid credentials" error, which avoids leaking account existence / auth method (#46).

Fixed

  • Downloading an attachment whose S3 object is missing now returns 404 instead of an unhandled 500; genuine backend errors still surface as 5xx (#45).

See the code changes between 1.2.0 and 1.2.1

1.2.0 — 2026-07-13

Added

  • Admin System page + opt-in update check: a new Admin → System page shows the installed version and, when UPDATE_CHECK_ENABLED=true, whether a newer release is available on GitHub. The check is off by default, runs as a daily background job (result cached in Redis, ETag-conditional), and sends no instance data to GitHub — only a standard request. The installed version is also shown in the footer.
  • File integrity check on the Admin → System page: verifies the shipped application files against a SHA-256 manifest generated at Docker build time and reports any missing, modified, or unexpected files. Purely local (no external calls); detects accidental modification, incomplete deployments and corruption (not tamper-proof against an attacker who can also rewrite the manifest — the manifest's own hash is shown for optional out-of-band verification).

See the code changes between 1.1.1 and 1.2.0

1.1.1 — 2026-07-13

Security release: four privately-reported advisories plus an internal adversarial "bug-bounty" audit that fixed ~25 further edge-case defects. All users of 1.1.0 should upgrade.

Breaking

  • SECRET_KEY must now be at least 32 characters. The application refuses to start with a shorter key. SECRET_KEY is the root secret for admin authentication and for encrypting confidential whistleblower identities, so a weak key undermines the platform's core protection. Generate a strong one with python -c 'import secrets; print(secrets.token_urlsafe(48))'. Note: rotating SECRET_KEY makes previously-encrypted confidential fields unreadable — set a strong key from the start.

Security

  • Report deanonymization / IDOR (GHSA-q3v3-5xf4-xjqr, High): every /admin/reports/{id}* endpoint now enforces object-level authorization. Case managers can only access reports assigned to them; admins are scoped to their own organisation (superadmins span all) when multi-tenancy is enabled. The dashboard list and the confidential-identity block are scoped the same way, so an unassigned case manager can no longer read a confidential whistleblower's identity.
  • Privilege escalation (GHSA-g3xj-3929-r45h, High): the role-assignment endpoints now enforce privilege tiers. Only a superadmin may grant or modify the superadmin role, an account can no longer change its own role, and the last active administrator can no longer be demoted away.
  • Stored XSS (GHSA-24hg-pf84-jj7x, High): admin usernames and organisation names are no longer interpolated into inline onclick handlers; confirmation prompts moved to a safe data-confirm attribute. Locally-created usernames are validated against a strict allowlist.
  • Weak / duplicated HTTP security headers (GHSA-gh23-4h5j-cqj8, Medium): security headers are now emitted by a single authoritative layer (the application middleware); the bundled nginx template no longer re-emits them, removing the duplicated/conflicting Strict-Transport-Security, X-Content-Type-Options and X-Frame-Options headers. The Content-Security- Policy no longer uses 'unsafe-inline': it is now a strict, per-response nonce-based policy for both scripts and styles.

Reported by @openblow.

Fixed (internal bug-bounty audit)

Real defects found by an adversarial audit, each covered by a regression test in tests/test_bug_bounty_v111.py:

  • Retention could delete reopened cases early: closed_at was never refreshed when a case was reopened and re-closed, so the auto-deletion job could remove reports far before the statutory retention period had elapsed since their actual closure. It is now cleared on reopen and re-stamped on re-close.
  • Superadmin lockout: a plain admin could deactivate a superadmin, and the last active privileged account could be deactivated/demoted, leaving no one able to administer the instance. Both are now blocked.
  • Attachment downloads with non-Latin-1 filenames (CJK, Cyrillic, emoji) raised UnicodeEncodeError and 500'd — the evidence became permanently undownloadable. Content-Disposition now uses RFC 5987 encoding. PDF export no longer crashes on non-Latin-1 note authors either.
  • MFA brute-force: TOTP guessing is now rate-limited, and a valid code is one-time-use within its window (blocks AiTM replay into a second session).
  • acknowledge is now idempotent so the statutory feedback deadline cannot be pushed out by re-invoking it.
  • Concurrent submissions no longer 500 on a case-number collision (retry).
  • Reports can no longer be assigned to a deactivated user (orphaned cases).
  • Linked-report metadata is filtered through the object-level authz check.
  • SLA reminder de-duplication now covers the full warn window (was re-firing every ~hour for days); per-report failures are isolated.
  • SUBMISSION_MODE_ENABLED=false now actually forces anonymous submissions, and confidential PII is purged from the session when switching to anonymous.
  • The whistleblower PIN lockout is keyed on the case number, so it can no longer be bypassed by fetching a fresh anonymous session token before each guess.
  • Login now runs a constant dummy password hash for unknown users (removes a username-enumeration timing side-channel).
  • Background scheduler jobs take a Redis lock so a scaled/stateless deployment does not run them once per replica (duplicate audit entries / notifications).
  • 4-eyes delete confirmation re-checks the request under a row lock, so a concurrent cancel cannot be raced into deleting a withdrawn report.
  • The submission wizard rejects out-of-order steps (blocks jumping straight to the attachment step to stash blobs in Redis) and no longer adopts a client-supplied session id with no server-side state (session fixation).
  • Case-insensitive duplicate-username check; empty decrypted bodies no longer fall back to raw ciphertext; oversized uploads are rejected without buffering the whole body; relinking already-linked cases returns 409 instead of 500; decryption failures are logged rather than silently shown as blank.

Remaining lower-severity findings are tracked in GitHub issues #42–#46.

Changed

  • All Python dependency floors raised to their current major versions (notably redis 8, bcrypt 5, SQLAlchemy 2.0.51, uvicorn 0.51, FastAPI 0.139, mypy 2, pytest 9, pytest-asyncio 1.x). GitHub Actions actions/checkout and codecov/codecov-action bumped to v7.

See the code changes between 1.1.0 and 1.1.1

1.1.0 — 2026-04-28

Added

  • Playwright E2E test suite (tests/e2e/): 13 test modules covering every critical user journey — admin login (incl. MFA), setup wizard redirect behaviour, whistleblower anonymous/confidential/file-attachment submissions, status page with deadline display, admin workflow (acknowledge → reply → status transitions), 4-eyes deletion flow, language switcher persistence, PDF export download, session expiry, user management RBAC, category and location management lifecycle
  • Automated accessibility tests (tests/e2e/test_accessibility.py): axe-core injected into 8 pages; run_axe helper filters to critical/serious violations and fails on any finding; CDN-unavailable skips gracefully; keyboard navigation smoke-test (skip link, tab order, form labels)
  • Locust performance test suite (tests/perf/locustfile.py): three user classes (WhistleblowerUser, AdminUser with TOTP login in on_start, StatusChecker); configurable concurrency; tests/perf/README.md with thresholds and run instructions
  • OpenAPI contract tests (tests/test_openapi_contract.py): validates OpenAPI 3.x structure, required paths (/health, /status, /submit), admin route auth enforcement (7 routes assert 3xx for unauthenticated requests), and snapshot regression detection via tests/fixtures/openapi_snapshot.json
  • E2E CI workflow (.github/workflows/e2e.yml): builds openwhistle:e2e image, starts full Docker Compose stack with DEMO_MODE=true, waits for /health, runs Playwright tests with Chromium headless, uploads trace on failure
  • Performance CI workflow (.github/workflows/perf.yml): manual workflow_dispatch with configurable users/run-time/host; uploads HTML + CSV Locust artifacts
  • Performance baseline (docs/performance-baseline.md): SLO thresholds (/health p95 < 50 ms, /status p95 < 200 ms, /admin/dashboard p95 < 400 ms) and user mix ratios for reproducible benchmarks

Changed

  • pyproject.toml: new [e2e] and [perf] optional dependency groups; e2e and perf pytest markers registered; mypy overrides for playwright. and locust.; ruff per-file-ignores extended to cover tests/e2e/ and tests/perf/

See the code changes between 1.0.0 and 1.1.0

1.0.0 — 2026-04-27

Added

  • Envelope encryption at rest: every new report is encrypted on write with a per-report Data Encryption Key (DEK) wrapped via AES-256 (Fernet); the DEK is encrypted with a Master Encryption Key (MEK) derived from SECRET_KEY using HKDF-SHA256; MEK is never stored; report description and all message bodies are encrypted; pre-encryption rows are readable without decryption (backward compat)
  • Data retention (GDPR / HinSchG): RETENTION_ENABLED=true activates a daily job (03:00 UTC) that permanently deletes closed reports older than RETENTION_DAYS (default 1095 = 3 years — HinSchG §12 Abs. 3 minimum); each deletion writes an immutable audit-log entry (report.auto_deleted) recording the case number, closure date, and legal basis
  • Multi-tenancy: MULTI_TENANCY_ENABLED=true activates multi-organisation support; Organisation model with name, slug, is_active, and branding JSON; all reports, users, categories, locations, and audit entries carry an org_id foreign key; per-org unique constraints on category slugs and location codes; superadmin role manages organisations via /admin/organisations
  • Superadmin role: new superadmin role above admin; require_superadmin dependency guards the organisation management endpoints; existing admin role retains all previous permissions; role added to AdminRole enum via ALTER TYPE adminrole ADD VALUE IF NOT EXISTS 'superadmin'
  • Telephone reporting channel guide (/admin/telephone-channel): compliance page covering HinSchG §16 requirements, implementation options (internal hotline vs. external ombudsman), §10 recording prohibition, and a compliance checklist
  • Data retention admin page (/admin/retention): shows current retention config, next scheduled run, legal basis (GDPR Art. 5/17, HinSchG §12), and configuration reference table
  • Organisation management page (/admin/organisations): superadmin-only page to create and deactivate organisations (default org cannot be deactivated)

Changed

  • Report description and message content are now stored encrypted; existing plaintext rows are transparently decrypted on first read (backward compat via decrypt_field_safe)
  • Admin report detail page and whistleblower status page now render decrypted content instead of raw ciphertext
  • Scheduler refactored: both SLA reminders and retention cleanup share a single AsyncIOScheduler instance; previous per-feature scheduler creation eliminated
  • ReportCategory.slug and Location.code unique constraints changed from global to per-organisation composite (slug + org_id, code + org_id)
  • Nav bar in all admin templates updated with links to Telephone Channel, Retention, and Organisations pages

Migrations

  • 012 — Creates organisations table; adds org_id FK and encrypted_dek column to all data-bearing tables; adds superadmin to adminrole enum
  • 013 — Data migration: backfills org_id with default org; makes org_id NOT NULL; encrypts all existing report descriptions and message bodies; makes encrypted_dek NOT NULL
  • 014 — Replaces global unique constraints on report_categories.slug and locations.code with per-org composite unique constraints
  • 015 — Reverts admin_users.org_id to nullable to support superadmin accounts (org_id = NULL means cross-organisation scope) and direct AdminUser creation in external tooling without a prior org lookup

See the code changes between 0.5.0 and 1.0.0

0.5.0 — 2026-04-26

Added

  • Health-check v2: /health endpoint now queries the database (SELECT 1) and Redis (PING) and reports per-component status; returns HTTP 200 with {"status":"ok"} when all healthy, HTTP 503 with {"status":"degraded"} on any failure; suitable for Kubernetes liveness and readiness probes
  • Structured JSON logging: LOG_LEVEL (default INFO) and LOG_FORMAT (json or text, default json) environment variables; JSON output via python-json-logger; all uvicorn loggers reconfigured uniformly at startup
  • Slack / Teams webhook formatter: NOTIFY_WEBHOOK_TYPE (generic, slack, teams) selects the payload format; Slack uses Block Kit (header + fields + action button); Teams uses Adaptive Cards (v1.4, FactSet + OpenUrl action); both new-report and SLA-reminder notifications respect the setting
  • SLA reminder system: background scheduler (APScheduler, interval 30 min) fires send_sla_reminders(); checks all non-closed reports for approaching 7-day acknowledgement deadline (REMINDER_ACK_WARN_DAYS, default 2 days before expiry) and 3-month feedback deadline (REMINDER_FEEDBACK_WARN_DAYS, default 30 days before expiry); Redis dedup keys (reminder:ack:{case}, reminder:feedback:{case}) with 1-hour TTL prevent duplicate notifications; enabled with REMINDER_ENABLED=true
  • S3-compatible attachment storage: STORAGE_BACKEND=s3 routes new attachments to an S3-compatible bucket (AWS S3, MinIO, Hetzner Object Storage) via boto3 (sync calls wrapped in asyncio.to_thread); S3_ENDPOINT_URL, S3_BUCKET_NAME, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION, S3_PREFIX configure the target; existing DB-backed attachments are unaffected (backward-compatible migration makes data nullable, adds storage_key VARCHAR(512))
  • LDAP / Active Directory login: LDAP_ENABLED=true enables corporate directory authentication for admin accounts; two-phase bind (service account → user DN re-bind to verify password); ldap3 runs synchronously in a thread pool; first LDAP login auto-provisions an AdminUser record; subsequent logins reuse the existing record; TOTP enrollment still required after first login; LDAP_SERVER, LDAP_PORT, LDAP_USE_SSL, LDAP_BIND_DN, LDAP_BIND_PASSWORD, LDAP_BASE_DN, LDAP_USER_FILTER, LDAP_ATTR_USERNAME, LDAP_ATTR_EMAIL configure the connection
  • Helm chart: charts/openwhistle/ — production-grade Helm chart for Kubernetes deployments; Chart.yaml, values.yaml, 8 templates (deployment.yaml, service.yaml, ingress.yaml, hpa.yaml, configmap.yaml, secret.yaml, _helpers.tpl, NOTES.txt); all v0.5.0 env vars exposed as chart values; supports existing-secret pattern for credentials; liveness/readiness probes wire to /health
  • Ansible role: ansible/roles/openwhistle/ — official Ansible role for bare-metal / VM deployments (Debian/Ubuntu); installs Docker CE + Compose plugin; creates system user openwhistle; renders .env, nginx.conf, and docker-compose.yml from Jinja2 templates; installs systemd service unit; optionally obtains TLS certificate via Certbot with auto-renewal hook; ansible/deploy.yml example playbook; vault.yml.example secrets template

Changed

  • app_version bumped to 0.5.0
  • Admin login page shows a badge when LDAP_ENABLED=true
  • admin_users.password_hash is now nullable (migration 011) — LDAP-only accounts have no local password
  • attachments.data is now nullable (migration 010) — S3-backed attachments store only storage_key

Database migrations

  • 010_s3_attachment_storage.py: makes attachments.data nullable; adds storage_key VARCHAR(512) column to attachments
  • 011_ldap_auth.py: makes admin_users.password_hash nullable; adds ldap_username VARCHAR(255) UNIQUE column to admin_users

Dependencies added

  • python-json-logger>=3.2.0 — structured JSON log formatter
  • apscheduler>=3.11.0 — background job scheduler for SLA reminders
  • boto3>=1.38.0 — AWS S3-compatible object storage client
  • ldap3>=2.9.0 — LDAP / Active Directory authentication

Tests

  • Added tests/test_v050.py — 68 tests covering all new v0.5.0 features
  • Fixed conftest.py: added adminrole to the enum drop list so re-runs don't fail with "type already exists" on migration 003
  • Coverage maintained at ≥90% (90.36% with full test suite)

See the code changes between 0.4.0 and 0.5.0

0.4.0 — 2026-04-26

Added

  • Multi-step submission form: single-page /submit replaced with a guided 5–6 step wizard (mode → location → category → description → attachments → review); Redis session stores partial state under submission-session:{uuid} with a 2-hour TTL; back/next navigation throughout; progress indicator shows current step and total; ow-submission-session cookie
  • Anonymous vs. confidential mode (Step 1): whistleblowers choose anonymous (no personal data) or confidential (optional name, contact info, secure email); confidential data encrypted with Fernet symmetric encryption derived from SECRET_KEY; decrypted only on the assigned admin's report detail view; new SUBMISSION_MODE_ENABLED config toggle
  • Multi-location / branch selection (Step 2, conditional): Location model with id, name, code (unique), description, is_active, sort_order, created_at; location selector shown only when active locations exist; admin management at /admin/locations
  • Confidential fields on reports: submission_mode (enum), location_id (FK), confidential_name (encrypted text), confidential_contact (encrypted text), secure_email (encrypted text) added to reports table via migration 009; all nullable for zero-downtime deploy
  • Optional secure contact email: whistleblower can provide an anonymous email address in confidential mode; when admin posts a reply, a brief notification (no report content) is sent; secure_email never appears in logs
  • HinSchG deadline display for whistleblowers: status page shows 7-day acknowledgement deadline with days remaining (or confirmed date) and 3-month feedback deadline with days left / pending acknowledgement indicator
  • French language (fr): app/locales/fr.json with full French translations for all keys; fr added to supported languages in app/i18n.py; language picker in nav bar shows English / Deutsch / Français dropdown
  • Location filter on admin dashboard: filter reports by location; location shown in report detail sidebar
  • WCAG 2.1 AA accessibility improvements: skip-to-content link in base.html; aria-label on all nav elements; aria-current="page" on active nav links; aria-live regions; role="alert" on errors; aria-required on required fields; aria-describedby on hints; sr-only utility; visible focus indicators; language picker keyboard-accessible
  • New CSS components: submit progress indicator, mode-selection cards with :has() focus handling, step-action row, review table, skip link, lang picker dropdown

Changed

  • app_version bumped to 0.4.0
  • Admin nav in all templates updated to include "Locations" link
  • Demo seed creates two demo locations (HQ, Remote) and one confidential demo report
  • PDF export includes submission mode, location, and confidential fields (secure email noted as "on file — not printed" for privacy)
  • add_admin_message accepts notify_whistleblower=True to trigger async secure-email notification when a secure email is on file
  • get_reports_paginated accepts optional location_id filter
  • Health endpoint now returns current app_version

Fixed

  • Language switcher now correctly handles French (fr) in redirect allowlist

Migration

  • Migration 009_locations_confidential.py: creates locations table, submissionmode enum, adds location_id, submission_mode, confidential_name, confidential_contact, secure_email to reports

See the code changes between 0.3.0 and 0.4.0

0.3.0 — 2026-04-26

Added

  • RBAC — Role-Based Access Control: AdminRole enum with admin and case_manager roles; require_role() FastAPI dependency factory; role shown in dashboard nav and report detail
  • Case assignment: admins can assign reports to any active staff member; "My Cases" filter tab on dashboard; assignee column in reports table
  • Status workflow overhaul: received → in_review → pending_feedback → closed replaces the old received → acknowledged → in_progress → closed flow; STATUS_TRANSITIONS dict enforces valid transitions server-side; only valid next-states shown in UI
  • 4-eyes deletion principle: report deletion now requires two different admins — one requests, a different one confirms; same-admin confirm returns HTTP 409
  • Immutable audit log: AuditLog model with 18 AuditAction constants; every admin action is recorded; exportable as CSV from /admin/audit-log; last 20 entries shown per report
  • Custom DB-driven categories: ReportCategory model replaces hard-coded Python enum; category management page at /admin/categories; existing reports preserve category as string
  • Case linking: CaseLink model with normalization constraint (smaller UUID always in report_id_a); link/unlink cases from report detail page
  • Internal notes: AdminNote model — admin-only notes never shown to whistleblower; add notes from report detail page
  • PDF export: full case export via /admin/reports/{id}/export.pdf using fpdf2 (pure Python, no system packages); includes SLA compliance section per HinSchG §17
  • Admin user management: create, deactivate, reactivate, and change roles of admin users at /admin/users; last-active-admin protection prevents lockout
  • Dashboard statistics: /admin/stats page with status distribution bar charts, category breakdown, total count, and 7-day SLA compliance rate
  • Demo seed improvements: case manager demo user (case_manager/demo); 4 demo reports covering all statuses; demo internal notes, case links, and audit entries
  • New admin navigation: persistent links to Stats, Categories, Users, Audit Log from all admin pages

Changed

  • Report category field migrated from PostgreSQL enum to VARCHAR(64) — stored as plain string at submit time for history immutability (migration 006)
  • acknowledged_report() now transitions to in_review instead of acknowledged
  • Status labels updated throughout UI and i18n files

Database migrations

  • 003_roles_status_assignment.py — adds adminrole enum, role/is_active to admin_users, adds in_review/pending_feedback to reportstatus enum, migrates old values, adds assigned_to_id FK to reports
  • 004_audit_log.py — creates audit_log table
  • 005_admin_notes.py — creates admin_notes table
  • 006_custom_categories.py — creates report_categories table, seeds 7 defaults, migrates reports.category from enum to VARCHAR
  • 007_deletion_requests.py — creates deletion_requests table with UNIQUE(report_id)
  • 008_case_links.py — creates case_links table with normalization CHECK constraint

Tests

  • Added test_v030_services.py — 35 service-layer tests for new features
  • Added test_v030_api.py — 25 API-level tests for new admin endpoints
  • Added test_pdf_service.py — PDF generation tests
  • Updated existing tests to use new ReportStatus values (in_review, pending_feedback)

See the code changes between 0.2.2 and 0.3.0

0.2.2 — 2026-04-26

Changed

  • Logo redesigned: new "Protected Signal" concept — navy shield with gradient depth, amber glow, and three-arc signal mark; consistent across app favicon, docs favicon, apple-touch-icon, and all inline SVG nav logos
  • README trimmed to overview + quick start; full documentation lives exclusively at openwhistle.net/docs.html (single source of truth, no duplication)
  • docs.html nav CSS aligned with index.html: SVG circle selector, border-color transition on theme-toggle hover, and light-mode stroke overrides for the logo

Fixed

  • Quay.io image reference corrected to quay.io/jp1337/openwhistle everywhere

Tests

  • Added 128 new test cases across auth, admin, reports, misc, and demo seed modules
  • Coverage increased from ~75 % to 91 %
  • Resolved all CI test failures caused by DEMO\_MODE=true and function-scoped event loop conflicts
  • Extracted _seed(db) helper from demo_seed.py to enable direct session injection in tests

CI / CD

  • Codecov integration: added CODECOV_TOKEN secret and pinned codecov-action@v5
  • GitHub org avatar (500×500) and repository social preview banner (1280×640) added under docs/

See the code changes between 0.2.1 and 0.2.2

0.2.1 — 2026-04-26

Fixed

  • Case number generation now uses MAX(case_number) instead of COUNT(*), preventing a previously-issued case number from being reused after a report is hard-deleted
  • Test isolation: orphaned report in test_delete_report_only_removes_matching_sessions caused a UniqueViolationError on CI; the test now cleans up all created reports

Security

  • Resolved 4 additional CodeQL code scanning alerts:
    • py/url-redirection (set-language endpoint): redirect target resolved via a static _NEXT_ALLOWLIST dict, severing any taint flow from user-supplied input
    • py/cookie-injection (reply endpoint): session cookie always rotated to a fresh secrets.token_urlsafe() value on every reply, never derived from the inbound cookie
    • py/clear-text-logging ×2 (reset_admin_password.py): replaced variable-based error messages with explicit if-chains where every print() argument is a string literal, eliminating any data-flow path from the password variable to a logging sink

See the code changes between 0.2.0 and 0.2.1

0.2.0 — 2026-04-26

Added

  • Admin session expiry warning: a non-intrusive banner appears 5 minutes before the session expires with a live countdown and a one-click "Extend Session" button that silently refreshes the JWT and Redis TTL without losing work (GET /admin/session/ttl, POST /admin/session/refresh)
  • Admin dashboard pagination with configurable page size (10 / 25 / 50 / 100), server-side
  • Admin dashboard column sorting (submitted date, case number, category, status)
  • Admin dashboard status filtering with clickable stat cards
  • File attachment support: whistleblowers can upload evidence files (PDF, images, Word, Excel, CSV, TXT — up to 10 MB each, 5 files per report); admins can download attachments from the report detail page
  • Email and webhook notifications when a new report is submitted (NOTIFY_EMAIL_ and NOTIFY_WEBHOOK_ environment variables)
  • CSRF Double-Submit Cookie protection extended to all whistleblower POST endpoints (/submit, /status, /reply)
  • scripts/reset_admin_password.py: interactive CLI to reset any admin user's password without direct database access; supports --list, --username, --password; enforces password strength requirements; does not touch the TOTP secret
  • HTML error page for form validation errors (422) instead of raw JSON API response
  • Company branding: BRAND_PRIMARY_COLOR, BRAND_SECONDARY_COLOR, BRAND_LOGO_URL env vars allow organisations to customise the UI with their own colours and logo
  • OIDC Authorization Code Flow: admins can log in via any OpenID Connect provider when OIDC_ENABLED=true (authlib 1.7+, state stored in Redis with 5-minute TTL)
  • Docker image cleanup workflow (GHCR, Docker Hub, Quay.io — runs weekly, retains 10 most recent sha- tagged images per registry)
  • edge Docker tag published on every push to main for tracking the latest unreleased state
  • Complete UI redesign: "Trusted Institution" aesthetic (Sora + Nunito Sans typography, white navigation bar, institutional blue + teal accent palette, elevation shadows, rounded corners)
  • Professional dark mode with warm blue-gray palette (#111827)
  • Submit-page sidebar redesigned with brand-colour background and subtle radial gradient
  • SSO button on admin login page (shown only when OIDC_ENABLED=true)
  • GitHub Pages website deployed from docs/ directory

Fixed

  • Whistleblower status-session Redis keys are now cleaned up immediately when a report is hard-deleted (previously persisted for up to 2 hours as orphaned entries)
  • SLA "days remaining" dashboard column no longer renders a double unit (e.g. "89d Tage verbleibend")
  • Session cookie deletion now passes matching security attributes (httponly, samesite, secure) so browsers reliably remove the cookie on logout
  • Theme toggle button now inherits the correct body font instead of falling back to the system font
  • Public forms no longer bypass browser required-attribute validation (removed novalidate from /submit and /status forms)
  • Empty reply content and oversized descriptions now return 422 with server-side length enforcement (previously validated by HTML attribute only, bypassable via direct HTTP requests)

Security

  • All whistleblower-facing cookies now use secure=not settings.demo_mode (was hardcoded False, meaning cookies were sent over HTTP even in production)
  • Server-side max-length validation added for report description (≤ 10 000 chars) and reply content (≤ 5 000 chars) — previously enforced by HTML maxlength attribute only
  • CSRF Double-Submit Cookie pattern extended to /status and /reply whistleblower endpoints

See the code changes between 0.1.0 and 0.2.0

0.1.0 — 2026-04-21

Added

  • Complete rewrite from C# ASP.NET Core to Python 3.14 / FastAPI
  • Whistleblower report submission with category and description
  • Two-factor whistleblower access: case number (OW-YYYY-NNNNN) + UUID4 secret PIN
  • Bidirectional communication thread between whistleblower and reporting office (HinSchG §17)
  • First-run setup wizard for admin account creation with TOTP enrollment
  • Mandatory TOTP (RFC 6238) MFA for all administrator accounts
  • Optional OIDC login for administrators (authlib 1.7+)
  • HinSchG SLA tracking: 7-day acknowledgement deadline (§17 Abs. 1) and 3-month feedback deadline (§17 Abs. 2)
  • IP anonymity: nginx configured with access_log off, application never reads or stores IP addresses
  • IP leakage detection: admin dashboard warning when upstream proxies forward IP headers
  • Redis-based bruteforce protection with no IP tracking (session-token-based rate limiting)
  • Hard deletion of reports (DSGVO Art. 17 right to erasure)
  • Demo mode with seed data (DEMO_MODE=true)
  • Automatic database migration check on every startup (alembic upgrade head)
  • DSGVO-compliant: all fonts and static assets self-hosted (Spectral, Source Serif 4, JetBrains Mono)
  • Light / dark mode with localStorage persistence and CSS media query fallback
  • Security headers: CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy
  • Multi-registry Docker publishing: ghcr.io, Docker Hub, quay.io
  • Image signing with Cosign
  • GitHub Actions CI: mypy --strict, ruff, pytest with coverage, docker build
  • HinSchG reference document (docs/hinschg_reference.md)
  • PostgreSQL 18 + Redis 8 support

Technical Decisions

  • Python 3.14 over Go/Rust: team familiarity with Python; mypy --strict provides compile-like type safety guarantees in CI
  • FastAPI for async performance and Pydantic validation
  • SQLAlchemy 2.0 async for type-safe database access
  • Authlib 1.7.0+ required due to CVE-2026-28498 in earlier versions
  • SSR with Jinja2 over SPA: simpler security model, no client-side secrets, works without JavaScript
  • Session tokens in Redis for instant revocation without database lookups
  • Rate limiting by session token (not IP) to maintain full anonymity
  • alembic upgrade head on every startup to guarantee migration consistency

See the code as first released