From 78f1bf853c32f6a91784732b317fbd81e411cb0a Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Fri, 7 Aug 2026 13:29:15 +0100 Subject: [PATCH] fix: More fixes to vuln matching --- server/internal/vulndb/match.go | 57 ++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/server/internal/vulndb/match.go b/server/internal/vulndb/match.go index 76278f8..54bd055 100644 --- a/server/internal/vulndb/match.go +++ b/server/internal/vulndb/match.go @@ -47,7 +47,7 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka cache := make(map[string][]Advisory, len(pkgs)) var out []Result - for _, p := range pkgs { + for _, p := range newestPerSource(os.Family, pkgs) { // Debian and Ubuntu advisories are keyed on the source package: one // advisory against "openssl" covers libssl3, openssl and libssl-dev. srcName := p.SourceName @@ -111,6 +111,61 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka return out, nil } +// newestPerSource collapses the installed set to one binary package per source +// package: the one carrying the highest version. +// +// Advisories are keyed on the source, so every binary package of a source asks +// the same question. Normally they all carry the same version and the answer is +// the same, so collapsing is free. The kernel is the exception that makes it +// necessary: Ubuntu encodes the ABI in the binary name, so an upgrade INSTALLS +// linux-headers-6.8.0-137 beside linux-headers-6.8.0-124 rather than replacing +// it, and the old one lingers until an autoremove. Matched per binary package, +// a fully patched host reports every superseded ABI package as vulnerable — +// which is the noise this exists to stop — and reports it twice over, once for +// linux-headers-6.8.0-124 and again for its -generic sibling. +// +// The version, not the name, decides. There is no kernel special case here: a +// source's newest installed version is what the fix landed as, whatever the +// source is. +// +// A comparison that cannot be made keeps the incumbent rather than guessing; +// the loser is dropped either way, and dropping the parseable one would be the +// false-negative direction. +func newestPerSource(family string, pkgs []models.InstalledPackage) []models.InstalledPackage { + best := make(map[string]models.InstalledPackage, len(pkgs)) + order := make([]string, 0, len(pkgs)) + + for _, p := range pkgs { + src := p.SourceName + if src == "" { + src = p.Name + } + cur, seen := best[src] + if !seen { + best[src] = p + order = append(order, src) + continue + } + older, err := LessThan(family, cur.Version, p.Version) + if err != nil { + log.Printf("vulndb: newest for source %s: compare %s vs %s: %v", + src, cur.Version, p.Version, err) + continue + } + if older { + Debugf("source %s: %s %s supersedes %s %s", + src, p.Name, p.Version, cur.Name, cur.Version) + best[src] = p + } + } + + out := make([]models.InstalledPackage, 0, len(order)) + for _, src := range order { + out = append(out, best[src]) + } + return out +} + // actionable decides whether an advisory with no fixed version is a finding. // // trivy-db fills Status only when FixedVersion is empty, and Ubuntu publishes a