Skip to content

fix(watch): enrich re-parsed SBOMs to prevent false resolved-vuln alerts#220

Closed
matrosov wants to merge 1 commit into
fix/osv-hydrationfrom
fix/watch-false-resolved
Closed

fix(watch): enrich re-parsed SBOMs to prevent false resolved-vuln alerts#220
matrosov wants to merge 1 commit into
fix/osv-hydrationfrom
fix/watch-false-resolved

Conversation

@matrosov

@matrosov matrosov commented Jun 13, 2026

Copy link
Copy Markdown
Member

Note

Stacked on #219 (fix/osv-hydration, OSV querybatch hydration). Merge that PR first; this one targets its branch.

Summary

Root cause 1 — false resolved/new alerts on every file touch. process_sbom_change parsed modified files without enrichment (the _config parameter was unused, src/watch/loop_impl.rs:240-312 pre-fix) and diffed the bare parse against the enriched previous snapshot. Resolved vulnerabilities are computed as in-old-not-new (src/diff/changes/vulnerabilities.rs:145-150), so every file touch alerted all OSV-enriched vulnerabilities as resolved, and the next enrichment cycle re-alerted them as new.

Root cause 2 — vuln-count inflation. run_enrichment_cycle re-enriches already-enriched SBOMs, and OsvEnricher::enrich extended component.vulnerabilities without dedup (src/enrichment/osv/mod.rs, both the cached-results and batch-fetch apply sites), so counts grew on every cycle.

Fix

  • process_sbom_change now enriches the re-parsed SBOM when config.enrichment.enabled, through a shared enrich_watched_sbom helper extracted from the previously duplicated OSV/EOL/VEX blocks in process_initial and run_enrichment_cycle (src/watch/loop_impl.rs:181-209). Periodic cycles keep bypassing caches; initial scans and re-parses use the cache.
  • OSV enrichment applies results via merge_vulnerabilities, which skips vulnerability ids the component already carries, making repeated enrichment idempotent (src/enrichment/osv/mod.rs:262-276).
  • EnrichmentConfig gains an optional api_base override wired through pipeline::build_enrichment_config so the watch loop's enrichment path can target a mock (or private mirror) OSV endpoint — required to drive the end-to-end test, and the only knob the watch path lacked compared to OsvEnricherConfig.

Test plan

  • New tests/watch_tests.rs::enrichment_loop::test_watch_loop_enriched_reparse_no_false_resolved_alerts: drives run_watch_loop in a thread against a tempdir SBOM with httpmock OSV endpoints (querybatch + per-vuln hydration + health check) and an NDJSON AlertSink. The first cycle enriches; the file is then touched with a component version bump. Asserts: no resolved_vulns/new_vulns in any change event, no new_vulns enrichment events, and vulns stays at 1 in every status event across >=3 enrichment passes (verified via mock hit counts). Ran 6x locally with no flakes.
  • New tests/enrichment_http_tests.rs::repeated_enrichment_does_not_duplicate_vulnerabilities: enriching the same component twice (second pass served from cache) yields exactly one vulnerability.
  • cargo fmt --all -- --check, cargo clippy -- -D warnings, cargo clippy --all-features -- -D warnings (toolchain 1.88): clean.
  • Full cargo test: all suites pass.

🤖 Generated with Claude Code

process_sbom_change parsed modified files without enrichment and diffed
the bare parse against the enriched previous snapshot. Since resolved
vulnerabilities are computed as in-old-not-new, every file touch alerted
all OSV-enriched vulnerabilities as resolved, and the next enrichment
cycle re-alerted them as new. Separately, run_enrichment_cycle
re-enriched already-enriched SBOMs while OsvEnricher::enrich extended
component.vulnerabilities without dedup, inflating vulnerability counts
on every cycle.

Fix:
- process_sbom_change now enriches the re-parsed SBOM (OSV/EOL/VEX) when
  config.enrichment.enabled, via a shared enrich_watched_sbom helper
  extracted from process_initial/run_enrichment_cycle. Periodic cycles
  keep bypassing caches; initial scans and re-parses use the cache.
- OSV enrichment merges results by vulnerability id, skipping ids the
  component already carries, so repeated enrichment is idempotent.
- EnrichmentConfig gains an optional api_base override (wired through
  build_enrichment_config) so watch-loop enrichment can target a mock or
  private OSV endpoint.

Tests: end-to-end watch loop test (tempdir SBOM + httpmock OSV + NDJSON
sink) asserting no resolved/new-vuln alerts on a file touch while the
vulnerability is still present and a stable vuln count across multiple
enrichment cycles; enricher-level re-enrichment idempotency test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Alex Matrosov <alex.matrosov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant