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,/statusand/admin/loginat 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/optionroles: 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 theuvecosystem — the oldpipentry 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/fontsinstead of downloaded unverified at build time. python-josereplaced by PyJWT, which dropsecdsa(PYSEC-2026-1325, no fix planned). Unusedauthlibandaiofilesremoved;cryptographyis 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_MODEno longer weakens a real installation. The static TOTP000000is 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
002addsattachments.encrypted).
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_FILTERpreviously 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_NONEbefore). Private CAs: setSSL_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_logran atwarn, and nginx prefixes rate-limit (429) and body-size (413) errors with the client address. It now logs atcritonly. - "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/dataunlessPGDATApoints there;docker-compose.prod.ymland 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 —
appVersionwas 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=backnever 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.
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_COLORis 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-facepointed at font files that did not exist, so the app silently fell back tosystem-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_COLORin 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.
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 anX-CSRF-Tokenheader (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_reqto 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).
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).
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_KEYmust now be at least 32 characters. The application refuses to start with a shorter key.SECRET_KEYis 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 withpython -c 'import secrets; print(secrets.token_urlsafe(48))'. Note: rotatingSECRET_KEYmakes 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
onclickhandlers; confirmation prompts moved to a safedata-confirmattribute. 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-OptionsandX-Frame-Optionsheaders. 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_atwas 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
UnicodeEncodeErrorand 500'd — the evidence became permanently undownloadable.Content-Dispositionnow 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).
acknowledgeis 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=falsenow 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/checkoutandcodecov/codecov-actionbumped to v7.
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_axehelper 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,AdminUserwith TOTP login inon_start,StatusChecker); configurable concurrency;tests/perf/README.mdwith 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 viatests/fixtures/openapi_snapshot.json - E2E CI workflow (
.github/workflows/e2e.yml): buildsopenwhistle:e2eimage, starts full Docker Compose stack withDEMO_MODE=true, waits for/health, runs Playwright tests with Chromium headless, uploads trace on failure - Performance CI workflow (
.github/workflows/perf.yml): manualworkflow_dispatchwith configurable users/run-time/host; uploads HTML + CSV Locust artifacts - Performance baseline (
docs/performance-baseline.md): SLO thresholds (/healthp95 < 50 ms,/statusp95 < 200 ms,/admin/dashboardp95 < 400 ms) and user mix ratios for reproducible benchmarks
Changed
pyproject.toml: new[e2e]and[perf]optional dependency groups;e2eandperfpytest markers registered; mypy overrides forplaywright.andlocust.; ruffper-file-ignoresextended to covertests/e2e/andtests/perf/
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_KEYusing 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=trueactivates a daily job (03:00 UTC) that permanently deletes closed reports older thanRETENTION_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=trueactivates multi-organisation support;Organisationmodel withname,slug,is_active, andbrandingJSON; all reports, users, categories, locations, and audit entries carry anorg_idforeign key; per-org unique constraints on category slugs and location codes; superadmin role manages organisations via/admin/organisations - Superadmin role: new
superadminrole aboveadmin;require_superadmindependency guards the organisation management endpoints; existingadminrole retains all previous permissions; role added toAdminRoleenum viaALTER 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
AsyncIOSchedulerinstance; previous per-feature scheduler creation eliminated ReportCategory.slugandLocation.codeunique 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
organisationstable; addsorg_idFK andencrypted_dekcolumn to all data-bearing tables; addssuperadmintoadminroleenum - 013 — Data migration: backfills
org_idwith default org; makesorg_idNOT NULL; encrypts all existing report descriptions and message bodies; makesencrypted_dekNOT NULL - 014 — Replaces global unique constraints on
report_categories.slugandlocations.codewith per-org composite unique constraints - 015 — Reverts
admin_users.org_idto nullable to support superadmin accounts (org_id = NULL means cross-organisation scope) and direct AdminUser creation in external tooling without a prior org lookup
0.5.0 — 2026-04-26
Added
- Health-check v2:
/healthendpoint 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(defaultINFO) andLOG_FORMAT(jsonortext, defaultjson) environment variables; JSON output viapython-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) firessend_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 withREMINDER_ENABLED=true - S3-compatible attachment storage:
STORAGE_BACKEND=s3routes new attachments to an S3-compatible bucket (AWS S3, MinIO, Hetzner Object Storage) via boto3 (sync calls wrapped inasyncio.to_thread);S3_ENDPOINT_URL,S3_BUCKET_NAME,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY,S3_REGION,S3_PREFIXconfigure the target; existing DB-backed attachments are unaffected (backward-compatible migration makesdatanullable, addsstorage_key VARCHAR(512)) - LDAP / Active Directory login:
LDAP_ENABLED=trueenables corporate directory authentication for admin accounts; two-phase bind (service account → user DN re-bind to verify password);ldap3runs synchronously in a thread pool; first LDAP login auto-provisions anAdminUserrecord; subsequent logins reuse the existing record;TOTPenrollment 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_EMAILconfigure 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 useropenwhistle; renders.env,nginx.conf, anddocker-compose.ymlfrom Jinja2 templates; installs systemd service unit; optionally obtains TLS certificate via Certbot with auto-renewal hook;ansible/deploy.ymlexample playbook;vault.yml.examplesecrets template
Changed
app_versionbumped to0.5.0- Admin login page shows a badge when
LDAP_ENABLED=true admin_users.password_hashis now nullable (migration 011) — LDAP-only accounts have no local passwordattachments.datais now nullable (migration 010) — S3-backed attachments store onlystorage_key
Database migrations
010_s3_attachment_storage.py: makesattachments.datanullable; addsstorage_key VARCHAR(512)column toattachments011_ldap_auth.py: makesadmin_users.password_hashnullable; addsldap_username VARCHAR(255) UNIQUEcolumn toadmin_users
Dependencies added
python-json-logger>=3.2.0— structured JSON log formatterapscheduler>=3.11.0— background job scheduler for SLA remindersboto3>=1.38.0— AWS S3-compatible object storage clientldap3>=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: addedadminroleto 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)
0.4.0 — 2026-04-26
Added
- Multi-step submission form: single-page
/submitreplaced with a guided 5–6 step wizard (mode → location → category → description → attachments → review); Redis session stores partial state undersubmission-session:{uuid}with a 2-hour TTL; back/next navigation throughout; progress indicator shows current step and total;ow-submission-sessioncookie - 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; newSUBMISSION_MODE_ENABLEDconfig toggle - Multi-location / branch selection (Step 2, conditional):
Locationmodel withid,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 toreportstable 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_emailnever 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.jsonwith full French translations for all keys;fradded to supported languages inapp/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-labelon all nav elements;aria-current="page"on active nav links;aria-liveregions;role="alert"on errors;aria-requiredon required fields;aria-describedbyon hints;sr-onlyutility; 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_versionbumped to0.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_messageacceptsnotify_whistleblower=Trueto trigger async secure-email notification when a secure email is on fileget_reports_paginatedaccepts optionallocation_idfilter- Health endpoint now returns current
app_version
Fixed
- Language switcher now correctly handles French (
fr) in redirect allowlist
Migration
- Migration
009_locations_confidential.py: createslocationstable,submissionmodeenum, addslocation_id,submission_mode,confidential_name,confidential_contact,secure_emailtoreports
0.3.0 — 2026-04-26
Added
- RBAC — Role-Based Access Control:
AdminRoleenum withadminandcase_managerroles;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 → closedreplaces the oldreceived → acknowledged → in_progress → closedflow;STATUS_TRANSITIONSdict 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:
AuditLogmodel with 18AuditActionconstants; every admin action is recorded; exportable as CSV from/admin/audit-log; last 20 entries shown per report - Custom DB-driven categories:
ReportCategorymodel replaces hard-coded Python enum; category management page at/admin/categories; existing reports preserve category as string - Case linking:
CaseLinkmodel with normalization constraint (smaller UUID always inreport_id_a); link/unlink cases from report detail page - Internal notes:
AdminNotemodel — admin-only notes never shown to whistleblower; add notes from report detail page - PDF export: full case export via
/admin/reports/{id}/export.pdfusingfpdf2(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/statspage 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
categoryfield migrated from PostgreSQL enum toVARCHAR(64)— stored as plain string at submit time for history immutability (migration 006) acknowledged_report()now transitions toin_reviewinstead ofacknowledged- Status labels updated throughout UI and i18n files
Database migrations
003_roles_status_assignment.py— addsadminroleenum,role/is_activeto admin_users, addsin_review/pending_feedbackto reportstatus enum, migrates old values, addsassigned_to_idFK to reports004_audit_log.py— createsaudit_logtable005_admin_notes.py— createsadmin_notestable006_custom_categories.py— createsreport_categoriestable, seeds 7 defaults, migratesreports.categoryfrom enum to VARCHAR007_deletion_requests.py— createsdeletion_requeststable with UNIQUE(report_id)008_case_links.py— createscase_linkstable 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
ReportStatusvalues (in_review,pending_feedback)
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/openwhistleeverywhere
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 fromdemo_seed.pyto enable direct session injection in tests
CI / CD
- Codecov integration: added
CODECOV_TOKENsecret and pinnedcodecov-action@v5 - GitHub org avatar (500×500) and repository social preview banner (1280×640) added under
docs/
0.2.1 — 2026-04-26
Fixed
- Case number generation now uses
MAX(case_number)instead ofCOUNT(*), 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_sessionscaused aUniqueViolationErroron 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_ALLOWLISTdict, severing any taint flow from user-supplied inputpy/cookie-injection(reply endpoint): session cookie always rotated to a freshsecrets.token_urlsafe()value on every reply, never derived from the inbound cookiepy/clear-text-logging×2 (reset_admin_password.py): replaced variable-based error messages with explicit if-chains where everyprint()argument is a string literal, eliminating any data-flow path from the password variable to a logging sink
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_andNOTIFY_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_URLenv 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) edgeDocker tag published on every push tomainfor 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 (removednovalidatefrom/submitand/statusforms) - 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 hardcodedFalse, 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
maxlengthattribute only - CSRF Double-Submit Cookie pattern extended to
/statusand/replywhistleblower endpoints
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