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:
- Drive the live scan feed in the dashboard (polling-based)
- Provide a full debug trace for operators after the scan finishes
- 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.py — DomainEvent 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.py — FindingRecord 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:
- Confirm vega-core emitted
finding_updatedevents — checkvega-core-events.jsonlin scan artifacts, or querydomain_events WHERE event_type='finding_updated' AND aggregate_id='<scan_id>' - Check
NormalizeAndUpsertFindingsUseCaseinapplication/findings/— look for errors in runner logs - Query findings directly:
SELECT payload FROM findings WHERE payload->>'scan_id' = '<scan_id>' - Check the frontend findings page filters — severity and status filters may be hiding results
Findings exist but the UI shows nothing:
- Clear all filters on the findings page
- Check the API response directly:
GET /api/findings/scans/:scan_id - Confirm the frontend is using the correct scan/repository/project ID