Skip to content

Events and Findings

Events and findings are the two key outputs of a scan. They serve different purposes: domain events are an append-only log of everything that happened, findings are durable structured security issues for human review.

Domain Events

A domain event is an append-only record emitted during a scan. Events are never edited or deleted — they are a log. They exist to:

  1. Drive the live scan feed in the dashboard (polling-based)
  2. Provide a full debug trace for operators after the scan finishes
  3. Materialize into read-model artifacts (activity log, worker-components log)

Events are stored in the domain_events table via EventStore (a port implemented by PostgresEventStore in production or JsonEventStore locally). Each event has a monotonically increasing sequence number per scan, enabling the frontend to request events since a known position.

Event types emitted during scanning:

Event type Description
scan_started The scan engine started the audit loop
scan_progress Progress update (stage name, component N of M)
scan_log A debug log line from vega-core or Codex
finding_updated A security issue was found or updated; payload contains a BugRecord list
component_worker_started A component audit worker started
component_worker_completed A component audit worker finished
component_worker_failed A component audit worker failed
scan_completed The scan finished successfully
scan_failed The scan encountered an unrecoverable error
scan_cancelled A user or operator stopped the scan
finding_verified Verification result for one finding

Key files: - app/domain/events/records.pyDomainEvent model - app/adapters/events/postgres/PostgresEventStore - app/adapters/events/json/JsonEventStore - app/adapters/engine/vega_core/EngineEventSink (converts vega-core events to DomainEvents)

Findings

A finding (FindingRecord) is a structured, durable record of a specific security issue. Unlike events (which are logs), findings are meant for triage — users review them, move them through triage states, and track them over time.

Each finding includes:

Field Description
severity critical, high, medium, low, or info
title Short description of the issue
buggy_locations File paths and line ranges where the issue was found
status (triage) candidate, triaging, confirmed, dismissed, needs_more_evidence
confidence The scan engine's confidence level
verification_fields Verification status, timing, and results if verification ran
scan_id The scan that produced this finding
repository_id The repository it belongs to

Key files: - app/domain/findings/records.pyFindingRecord model - app/application/findings/ — all finding use cases

How findings are created

Findings are automatically extracted from finding_updated domain events by NormalizeAndUpsertFindingsUseCase:

vega-core scan engine
    ↓ emits VegaCoreEvent(kind="finding_updated", payload=BugRecord list)
EngineEventSink (adapters/engine/vega_core/)
    ↓ converts to DomainEvent, stores in EventStore
    ↓ calls NormalizeAndUpsertFindingsUseCase
NormalizeAndUpsertFindingsUseCase
    ↓ normalizes BugRecord → FindingRecord
    ↓ upserts into FindingStore (creates or updates)
FindingStore (Postgres or JSON)
    ↓ persisted
GET /api/findings/scans/:scan_id
    ↓ frontend reads findings

Upsert semantics — if the scan engine reports the same logical finding twice (e.g., with updated confidence or additional evidence), the backend updates the existing record. The deduplication key is based on the finding's content hash within a scan.

Finding triage

Users triage findings through MutateFindingUseCase:

Action Route New status
Triage as pending POST /api/findings/scans/:scan_id/:finding_id/triage triaging
Confirm same, body {status: "confirmed"} confirmed
Dismiss same, body {status: "dismissed"} dismissed
Mark removed POST /api/findings/scans/:scan_id/:finding_id/remove removed

Finding verification

When verification is enabled (max_verification_runners_per_scan > 0), each new finding gets verification_status=queued. The scan worker launches a dedicated ECS runner that calls VegaCoreEngineAdapter.run_verification(). Verification transitions through:

queued → running → completed
                 → failed

Where findings appear in the API

Canonical route Description
GET /api/findings/scans/:scan_id All findings for a specific scan
GET /api/findings/scans/:scan_id/:finding_id Single finding with full detail
GET /api/findings/repositories/:repo_id All findings for a repository (across scans)
GET /api/findings/projects/:project_id All findings across all repos in a project
GET /api/findings/:finding_id Global finding lookup by ID

These canonical routes are supplemented by legacy aliases under /api/repositories/:repo_id/findings and /api/projects/:project_id/findings.

Debugging

Events appear in logs but no findings show up:

  1. Confirm vega-core emitted finding_updated events — check vega-core-events.jsonl in scan artifacts, or query domain_events WHERE event_type='finding_updated' AND aggregate_id='<scan_id>'
  2. Check NormalizeAndUpsertFindingsUseCase in application/findings/ — look for errors in runner logs
  3. Query findings directly: SELECT payload FROM findings WHERE payload->>'scan_id' = '<scan_id>'
  4. Check the frontend findings page filters — severity and status filters may be hiding results

Findings exist but the UI shows nothing:

  1. Clear all filters on the findings page
  2. Check the API response directly: GET /api/findings/scans/:scan_id
  3. Confirm the frontend is using the correct scan/repository/project ID