Data Model¶
PostgreSQL in production, SQLite for the test suite. Portable column types (GUID, PortableJSON)
map to native UUID/JSONB on PostgreSQL. The schema is defined in backend/app/db/models.py and
created by Alembic migrations 0001_initial → 0002_warden_x → 0003_scan_environment. A test
upgrades a fresh database to head and compares it with the models; CI also runs
upgrade → downgrade → upgrade against a real PostgreSQL 16.
Entities in use today¶
erDiagram
USERS ||--o{ REFRESH_TOKENS : holds
USERS ||--o{ SCANS : requests
USERS ||--o{ AUDIT_EVENTS : performs
USERS ||--o{ POLICY_EXCEPTIONS : "requests / decides"
POLICIES ||--o{ SCANS : evaluates
POLICIES ||--o{ POLICY_EXCEPTIONS : scopes
SCANS ||--o{ SIGNALS : "has findings"
SCANS ||--o{ SECURITY_EVENTS : raises
| Table | Purpose | Notes |
|---|---|---|
users |
Accounts | Role stored as a string: admin, security_analyst, developer, auditor, read_only. The v1 names analyst and viewer were migrated. |
refresh_tokens |
Opaque refresh tokens | Only a sha256 of the token is stored. A revoked token presented again revokes the user's whole family. |
policies |
Enforcement policies | Legacy columns (thresholds, capability lists, allow/deny lists) plus an optional policy-as-code document, an environment, and a version. One active policy per environment. |
policy_exceptions |
Time-boxed waivers | Package (normalised), optional version range, optional code/category scope, environment, justification, requester, approver, status, expiry. Expiry is evaluated when read; the approver must differ from the requester. |
scans |
One verdict per package version, analyzer version and environment | Scores (final, rule, ML, malicious, vulnerability), severity, decision, the risk breakdown, attack chains, analyzer runs, package intelligence, provenance, vulnerabilities, intelligence status, model version, policy reasons, feature vector. |
signals |
Findings of a scan | v1 columns plus finding id, confidence, category, title, analyzer and version, capability, location, CWE, ATT&CK, remediation, references, provenance, related findings. Evidence is already sanitised and redacted when stored. |
audit_events |
Append-only audit trail | seq, prev_hash and event_hash form a sha256 chain over a canonical encoding of each event. On PostgreSQL a trigger rejects UPDATE and DELETE; appends take an advisory lock so the chain cannot fork. |
security_events |
Operational security events | Type, severity, title, package and version, optional scan and project, sanitised details, acknowledgement. The row is the durable record; a Redis stream carries a best-effort copy. |
Projects, diffs, containers and monitoring¶
| Table | Written by | Notes |
|---|---|---|
projects |
POST /projects |
Unique name. |
project_scans |
POST /projects/{id}/scans |
Manifests (file, type, sha256), counts, decision, risk, findings summary, graph and the CycloneDX document. The submitted manifest text itself is not stored. |
project_components |
project scans | One row per component, with declaration positions and the id, risk and decision of the newest stored verdict for the same name, version and environment. |
dependency_edges |
project scans | Parent and child bom_ref with the specifier. |
release_diffs |
POST /diffs, the monitoring worker |
One row per package, version pair and analyzer version; summary and the findings new in the newer release. |
container_scans |
POST /containers/scans |
Label, config digest, completed / incomplete, tool status, summary, findings and the CycloneDX component list. The image archive is not stored. |
monitored_packages |
/monitoring routes, the worker |
Baseline (approved_version), last seen version and risk, snapshot, schedule, consecutive failures. A partial unique index covers rows without a project. |
vulnerability_records |
GET /vulnerabilities/lookup |
Advisory cache keyed by OSV id with KEV and EPSS enrichment. |
scan_jobs |
— | Reserved for queued work; nothing writes to it yet. |
Design notes¶
- Findings are denormalised onto the scan as JSON as well as stored in
signals: the JSON keeps a scan's explanation self-contained and immutable, whilesignalssupports aggregation (the dashboard's most frequent findings). - Idempotent verdicts. A rescan of the same package version under the same analyzer version and environment updates that row instead of piling up duplicates; changing the analyzer version keeps history separate.
- Nothing sensitive at rest. Passwords are argon2id hashes, refresh tokens are hashes, and secrets found inside packages are stored only as redacted previews and keyed fingerprints.
- Indexes cover the lookups the API makes: package name and creation time on scans, decision and risk, finding code and category, event type, severity and time, audit actor and action, exception package and expiry.