Skip to content

Auto-Fix Reconciliation

When you ship a fix, Vygl notices automatically: a previously-seen fingerprint that doesn’t reappear in a full scan is marked auto_fixed. Reconciliation is per-branch and per-scan-type, so each branch evolves independently and a SAST-only scan can’t accidentally close SCA findings.

After a scan ingest, reconciliation runs when:

  1. full_scan=true — the scan covered the entire repository, not a PR diff or a partial run.
  2. The ingest specifies a branch — any branch, not just the project’s default. Each branch reconciles its own row state.
  3. Eligible scan types — only findings whose scan_type is in the ingest’s scan_types are considered. A SAST-only full scan won’t close SCA, Secrets, IaC, or Malware rows.
  4. Same originorigin=scan ingests reconcile origin=scan rows; origin=cve_watch ingests reconcile their own. The two never cross.
  5. Eligible status — only findings in open, acknowledged, or fix_claimed are reconciled. Sticky decisions (fixed, false_positive, ignored, suppressed, auto_fixed) survive.

Per-row, the rule is simple: if the fingerprint isn’t in the new scan’s results, the row flips to auto_fixed. CVE Watch hourly ingests run their own reconciliation pass for origin=cve_watch rows, so an OSV advisory that’s withdrawn or a package that’s upgraded out of the vulnerable range closes its CVE-watch finding the same way.

Each branch has its own row state for a given fingerprint, so reconciliation is naturally scoped: a feature-branch full scan auto-fixes its own findings without touching main, and a main full scan only acts on main’s rows. There’s no cross-branch flapping when developers branch and merge.

Each engine knows what it scanned. Marking a finding auto_fixed because the scan that ran “didn’t include it” — when the engine for that scan type didn’t even run — would be a false positive on the platform. Per-scan-type gating fixes that.

When a fix_claimed row’s fingerprint is missing from a qualifying scan, it’s promoted to auto_fixed and decorated with metadata.claim_attribution (claimed_by_user_id, claimed_by_actor_kind, claimed_at, verified_by_scan_id, verified_at). A finding.status_change.claim_verified audit event is emitted alongside the promotion — your fix-claim got verified by the next eligible scan.

If a fix_claimed row’s fingerprint does reappear, the row flips back to open with claim_disproven_at set and a finding.status_change.claim_disproven audit event. See Triage for the full lifecycle.

When a finding flips to auto_fixed:

  • Its history stays — first-seen scan, last-seen scan, every prior status transition.
  • Its AI verdict stays in metadata.ai_verification.
  • Its fingerprint stays — if the same issue is reintroduced, this row is the one that resurrects rather than a duplicate being created.
  • metadata.claim_attribution stays for verified fix_claimed → auto_fixed rows.

If an auto_fixed finding’s fingerprint reappears, the row is automatically flipped back to open:

  • status_reason is set to "Re-detected by scan {short_scan_id}".
  • status_changed_by is cleared (the system, not a user, did this).
  • A finding.status_change.auto_fixed_resurrected audit entry is emitted.
  • Prior triage history and AI verdict survive on the row’s metadata.

This handles the regression case cleanly — the same finding appears in the timeline as first_detected → auto_fixed → resurrected → ..., all on one row.