Skip to content

Findings Persistence

A finding starts as a raw event emitted by the scan engine and ends as a durable record in the database. This page traces that journey.

From scan event to database record

flowchart TD
    A["v16 emits finding_updated event\n(kind='finding_updated', payload={title, severity, file_path, evidence, ...})"]
    B["event_sink callback in\napp/projects/v16_adapter.py"]
    C["Maps raw v16 payload to\nVega Finding model"]
    D["Calls ProjectService.upsert_finding()\napp/projects/service.py"]
    E["Also appends event record\nfor the live scan feed"]
    F["PostgresStore.upsert_finding()\nor JSON file write"]
    G["Finding record in DB\n(id, scan_id, repo_id, severity, title,\nfile_path, status='open', evidence)"]
    H["GET /v1/repositories/:id/findings\nFrontend findings page"]

    A --> B --> C --> D
    D --> E
    D --> F --> G --> H

Upsert semantics

Findings are upserted, not inserted. This is important because v16 may emit multiple finding_updated events for the same logical finding (e.g., as it refines its confidence). The backend deduplicates based on a combination of the finding's title and file path within a scan.

If the same finding is reported twice: - The first event creates the finding record - Subsequent events update the record with any new or revised fields

This means you won't see duplicate findings for the same issue.

Finding fields

After persistence, a finding has:

Field Source
id Generated by the backend
scan_id The scan that produced it
repository_id The repository it belongs to
severity From v16 event payload: critical, high, medium, low, or info
title Short description from v16
file_path Affected file in the source tree
status Starts as open; users can change to confirmed or dismissed
evidence v16's supporting reasoning for the finding
created_at Timestamp of first upsert
updated_at Timestamp of last upsert

Partial findings on cancellation

If a scan is cancelled while running, partial findings are preserved. Any finding_updated events that were processed before the cancellation result in finding records. Users can review these partial results even though the scan didn't complete.

Debugging findings persistence

v16 emits events but no findings appear: 1. Check v16-events.jsonl in the scan artifacts to confirm finding_updated events were emitted. 2. Check the runner logs for upsert errors. 3. If using Postgres, confirm migration 004_findings_columns.sql has been applied: look at the schema_migrations table.

Findings appear but are missing fields (e.g., no severity): The v16 version being used may emit events with different payload fields. Check app/projects/v16_adapter.py and the event mapping logic. Ensure the adapter handles optional fields gracefully (defaults rather than errors on missing keys).

Findings from old scans appearing in current results: The findings API filters by repository by default. If you're seeing findings from other scans, check whether you're filtering by scan_id as well. The frontend findings page should show all findings for the repository across all scans; the scan-specific view filters by scan_id.

Relevant test files

  • tests/test_scan_persistence.py — scan lifecycle and persistence tests
  • tests/test_projects_flow.py — end-to-end project/repository/scan/finding flow