Findings & Triage
Raw findings produced by the audit stage are deduplicated by SharedTriageService
into stable bug records before backend receives them. This page covers the full
finding lifecycle from agent output through deduplication to backend persistence.
Location
src/triage/
Finding lifecycle
flowchart LR
A[Agent output] --> B[AuditStage parses finding]
B --> C[finding_updated event]
C --> D[ScanTransition.apply_output]
D --> E[SharedTriageService.ingest]
E --> F[DeduplicatedBugBatch]
F --> G[CoreResult.data.deduplicated_bugs]
G --> H[vega-backend persists bugs]
Raw findings (from audit)
AuditStage emits a finding_updated event for each finding it discovers.
These raw findings are also collected in StageOutput.data["raw_findings"] and
written to the vega.raw_findings artifact.
A raw finding shape (validated against stages/audit/schemas/findings.json):
{
"finding_id": "component_id/hash",
"title": "SQL Injection in query builder",
"description": "User input reaches the SQL query without sanitization.",
"severity": "critical",
"bug_class": "sql_injection",
"file_path": "src/db/query.py",
"line_start": 87,
"line_end": 94,
"component_id": "db/query_builder",
"evidence": ["affected code line"],
"attacker_profile": "unauthenticated",
"required_state": "any",
"required_services": "database"
}
SharedTriageService
Triage runs outside individual scan containers. Multiple audit workers can emit raw findings, but they all flow through one deduplication path before backend receives stable bug events.
class SharedTriageService:
def ingest(
self, context: RunContext, batch: RawFindingBatch
) -> DeduplicatedBugBatch: ...
def deduplicate(
self, context: RunContext, batch: RawFindingBatch
) -> DeduplicatedBugBatch: ...
ScanTransition.apply_output(...) calls triage.ingest(...) after the audit
stage completes. The result is stored in PipelineState.deduplicated_bugs and
returned in CoreResult.
RawFindingBatch
@dataclass(frozen=True)
class RawFindingBatch:
run_id: str
stage: StageName
findings: tuple[Mapping[str, Any], ...]
metadata: Mapping[str, Any] = field(default_factory=dict)
DeduplicatedBugBatch
@dataclass(frozen=True)
class DeduplicatedBugBatch:
bugs: tuple[Mapping[str, Any], ...]
artifacts: Mapping[str, ArtifactRef] = field(default_factory=dict)
metadata: Mapping[str, Any] = field(default_factory=dict)
bugs is the stable, deduplicated list. Each entry has a stable bug_id
derived from finding signature (file path, bug class, line range). Multiple raw
findings that map to the same bug are collapsed into one record.
Triage events
| Event kind | Payload |
|---|---|
raw_findings_batch_received |
{run_id, finding_count} |
finding_deduped |
{bug_id, finding_ids} |
Deduplicated bug shape
Backend receives deduplicated bugs from CoreResult.data["deduplicated_bugs"]
and from triage events. A deduplicated bug record includes:
{
"bug_id": "stable-hash-based-id",
"title": "SQL Injection in query builder",
"severity": "critical",
"bug_class": "sql_injection",
"file_path": "src/db/query.py",
"line_start": 87,
"component_id": "db/query_builder",
"finding_ids": ["component_id/hash1"],
"scan_id": "scan_xyz",
"verification_status": null,
"patch_status": null
}
Artifact artifacts
| Artifact | Content |
|---|---|
vega.raw_findings |
All raw audit findings across all components |
vega.audit_component_state |
Per-component status for audit resume |
vega.deduplicated_bugs |
Deduplicated bug batch from triage |
Verification status lifecycle
After verify_and_patch runs, each bug has a verification record:
{
"finding_id": "bug_id",
"status": "verified",
"triggerable": true,
"confidence": "high",
"root_cause": "...",
"evidence": ["..."]
}
Backend updates finding verification_status from finding_verified events.
Parallel deduplication
When multiple scan runner containers audit the same repository (sharding),
their raw findings independently reach SharedTriageService. The service must
be shared — not per-runner — to produce stable bug identities across shards.
In practice this means:
- In a single-container scan, triage runs in-process after audit completes.
- In a multi-shard scan, backend is responsible for aggregating shard results and running a final deduplication pass (or using a shared triage process).
The current SharedTriageService implementation is in-process. The model for
cross-shard deduplication is a backend responsibility.
Debugging missing findings
- Check
raw_findings_readyevent — was it emitted? What wasfinding_count? - Check
vega.raw_findingsartifact — are raw findings present? - Check
raw_findings_batch_receivedandfinding_dedupedevents. - Check
CoreResult.data["deduplicated_bugs"]length. - Check
finding_updatedevents — were findings emitted per component? - If no
finding_updatedevents appear, the audit stage may have had parse errors or empty agent output — checkagent_logevents for details.