Security

Secure development

How DFIRe is designed, built, verified and released, and the gates that enforce each stage.

Overview

All code quality and security gates run on continuous integration. All releases must pass all security and quality gates.

For the security architecture of the running application, see Application Security. For how third-party components are pinned and verified, see Supply Chain Security.

1. Design

Threat model

DFIRe is self-hosted software. One organisation operates each deployment, and its users are members of that organisation.

The boundary that matters is inside the deployment: case boundaries and need-to-know. A user who can reach a case they were not assigned to is the failure the design targets.

The superuser is trusted. The superuser owns the deployment and holds the container, the database and the logs, so a control that only stops them stops nothing. This shapes which analyser findings count as defects. It does not extend to the DFIRe Admin role, which administers the application under the same permission model as every other role.

Accidental data loss carries the same weight as a boundary crossing. An ordinary action that irreversibly destroys evidence is a defect.

Enforced invariants

Tests enforce these properties rather than convention:

  • Every API endpoint resolves a permission before acting.
  • Queries returning case data are scoped to the cases the requesting user may see.
  • Attachment contents are encrypted before reaching storage.
  • Stored credentials use encrypted fields.
  • A failed audit write prevents the operation it was recording.

2. Implementation

Coding standards

Ruff lints and formats the entire repository. The lint gate requires zero findings. There is no warning tier. The format gate requires the tree to match the formatter output, so formatting never appears in a review diff.

ESLint holds the frontend to the same standard, with warnings failing the build alongside errors. A production build runs on every change and includes the TypeScript compiler, so type errors fail the build.

Security linting

Ruff runs a security rule set covering dynamic code execution, shell and SQL injection, unsafe deserialization, weak and disabled cryptography, disabled certificate verification, and XML entity expansion.

The gate requires zero findings. There is no baseline of pre-accepted ones, so a finding fails the build rather than joining a list.

A scanner cannot evaluate intent. Where one flags a deliberate design decision, that finding is recorded as an exception with its reasoning rather than suppressed in place.

Secrets

All code passes secret scanning. Push protection rejects a commit containing a credential at push time, using the scanner's standard patterns together with custom patterns for DFIRe's own credential formats. Credentials DFIRe stores for integrations use encrypted fields and are masked in API responses.

Dependency intake

New dependency versions wait out an adoption cooldown before entering a build. Every dependency, direct and transitive, is pinned to an exact version and verified by hash at install time. See Supply Chain Security.

3. Verification

Test gates

Gate What it confirms
Backend unit tests Domain logic, services and models.
Backend API tests Endpoints exercised through the request stack: permission enforcement, case scoping, serialization and status codes.
Frontend unit suite Component and logic behaviour, with coverage minimums enforced as build failures.
Browser suite Drives a freshly installed instance through complete investigator journeys: case creation, evidence handling, custody transfer, reporting, indicator import and export, and permission denials.
Changed-line coverage Lines a change introduces meet a higher bar than the codebase average.
Migration drift The schema described by the models matches the migrations committed with them.
Clean worktree Tests and builds modify nothing.
Gate self-tests The gate scripts have their own regression tests.

Both backend layers run against real PostgreSQL and Redis, so behaviour depending on transactions, constraints and connection pooling is exercised as deployed.

The browser suite also gates release candidates. No candidate image is built from a revision that fails it.

Static analysis

Analyser Scope and pass condition
Ruff Pattern checks over Python. Requires zero findings across the repository, for both the security rules and the lint and format rules.
Semgrep Structural pattern matching over the backend, the browser interface and the build scripts. Requires zero findings. Also requires that no file has become unreadable to the parser: Semgrep exits successfully on a partially parsed file, so a second check compares unparseable files against a recorded list and fails if that list grows.
CodeQL Data-flow analysis for Python, TypeScript and the build workflows. Traces untrusted input across function and file boundaries.

Semgrep runs at a pinned image digest, against rules stored in version control rather than fetched at build time.

Contract checks

These tests enumerate the application rather than sampling it:

  • Permission resolution. Every routed endpoint resolves a permission. An endpoint added without one fails the build.
  • Anonymous surface. The set of endpoints reachable without authentication is fixed. Adding one fails a test.
  • Assistant interface. The AI tooling interface reimplements domain operations, so tests enumerate its tool registry against the same permission model.
  • API schema. The published schema is generated and validated on every change. Outstanding schema issues are frozen.
  • Production configuration. Django deployment security checks run under production settings. Every finding carries a written classification. None is silenced.

Review

Every change reaches a release through a pull request carrying the full gate set. Branch protection rejects direct pushes. On a pull request that changes code, the maintainer also runs a separate AI code review. The pull request records each finding and what was done about it.

4. Ratchets

Coverage, changed-line coverage, skipped tests, outstanding schema issues and frontend warnings are ratchets. They move toward stricter only.

Version control holds the current state. The build fails if the state worsens, and also when it improves, until the record is updated.

Lowering a floor is not an accepted response to a failing build. The accepted responses are to write the test or fix the change. A skipped test counts as a defect.

5. Accepted findings

Every accepted finding is recorded in version control with a written reason. The build regenerates the register and compares it. Adding, removing or rewording an entry fails the build until the register is committed, so the record cannot drift from the code.

Three forms are refused:

  • A suppression with no written reason.
  • A suppression naming no specific rule, which would hide rules introduced later.
  • A file-level suppression, which covers code not yet written.

Analysers that record dismissals in a hosted database have each dismissal mirrored into the same register, so the reasoning is not held only by the vendor.

The registers are available to customers and auditors for review on request.

6. Release

A release image is built only from a revision that passed every gate in scope. The build records a software bill of materials and build provenance for each image, retained with the release.

Building and publishing are separate. A candidate is built and tested under a disposable tag. Promoting it to a released version is a later, deliberate action.

API contract verification

Before every release, an API suite is run against a deployed staging instance. It exercises the API over the network as a client reaches it, through the same proxy, authentication and serialization path, and checks the wire contract: response fields and their types, validation and sanitization rules, and status codes.

A change altering any part of that contract updates this suite in the same branch. The suite provisions and removes its own data and asserts the teardown, so a failed run cannot leave state behind that makes a later run pass for the wrong reason.

A published version number identifies one build and is never reissued. A rebuild becomes a new patch version, so a version cited in an investigation report names one artefact.

Upgrade safety

The upgrade path runs a test migration before applying the live one. On startup DFIRe applies every pending migration inside a single transaction and rolls it back, and a failure at that stage stops the container before the real migration runs.

The test migration cannot cover every case. A migration marked non-atomic escapes the enclosing transaction. The check refuses to run it, and the container stops until an operator sets a bypass variable for that upgrade. A migration that succeeds against your schema can also produce results you did not intend. Test an upgrade against a copy of your data before applying it to a production instance, and take a backup first.

Migration history is forward-only and retained, so any earlier published release upgrades directly to the current one. The release immediately preceding the current one is the version-tested path.

7. Vulnerability response

Dependency advisories are checked against the released version, not only against the branch under development. A finding triggers the decision on whether to issue a patch release.

An advisory-driven dependency update can skip the adoption cooldown only through an explicit waiver naming the package and version, with an expiry date.

Security reports from outside the project are covered by the vulnerability disclosure policy, which states what is in scope, what DFIRe commits to, and the conditions under which it will not pursue legal action. The security contact is also published at /.well-known/security.txt as defined by RFC 9116.

Limits

  • Analysers do not understand authorisation. They find injection, unsafe data handling and weak cryptography. No analyser determines whether a user should reach a given case. The permission model and the contract tests hold that boundary.
  • One analyser cannot fully parse a small number of files. Where its parser lacks a recent language feature, those files are listed and the build fails if the list grows.
  • Coverage measures execution, not assertion. A line can be covered by a test that never checks its result.
  • Gates constrain the code, not the deployment. Configuration, patching and access control of a running instance belong to the operating organisation.
← Supply chain Third-party licences →