Skip to content

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

  1. Check raw_findings_ready event — was it emitted? What was finding_count?
  2. Check vega.raw_findings artifact — are raw findings present?
  3. Check raw_findings_batch_received and finding_deduped events.
  4. Check CoreResult.data["deduplicated_bugs"] length.
  5. Check finding_updated events — were findings emitted per component?
  6. If no finding_updated events appear, the audit stage may have had parse errors or empty agent output — check agent_log events for details.