From 0684d846092901656baccd0161e67d0116b553d9 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Fri, 7 Aug 2026 15:33:50 +0100 Subject: [PATCH] fix: Fixed vuln score --- server/internal/services/findings.go | 6 +++ server/internal/vulndb/enrich.go | 64 ++++++++++++++++++++++++++++ server/internal/vulndb/match.go | 7 +++ server/internal/vulnsched/sched.go | 13 +++++- 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 server/internal/vulndb/enrich.go diff --git a/server/internal/services/findings.go b/server/internal/services/findings.go index d8f4968..be43e96 100644 --- a/server/internal/services/findings.go +++ b/server/internal/services/findings.go @@ -52,6 +52,9 @@ func DiffFindings(existing []models.VulnFinding, results []vulndb.Result, now ti Installed: r.Installed, FixedIn: r.FixedIn, Severity: r.Severity, + CVSSScore: r.CVSSScore, + Title: r.Title, + References: r.References, State: models.FindingOpen, FirstSeen: now, LastSeen: now, @@ -349,6 +352,9 @@ func ApplyFindingDiff(ctx context.Context, instanceID, serverID string, d Findin "installed_version": f.Installed, "fixed_in": f.FixedIn, "severity": f.Severity, + "cvss_score": f.CVSSScore, + "title": f.Title, + "references": f.References, "state": models.FindingOpen, "last_seen": now, }, diff --git a/server/internal/vulndb/enrich.go b/server/internal/vulndb/enrich.go new file mode 100644 index 0000000..32edc8d --- /dev/null +++ b/server/internal/vulndb/enrich.go @@ -0,0 +1,64 @@ +package vulndb + +// VulnSource is the CVE-metadata lookup the enricher needs. *Store satisfies it. +type VulnSource interface { + Vulnerability(cveID string) (VulnInfo, error) +} + +// MetaCache enriches match results with the CVE's own metadata. +// +// This is not an optimisation, it is where severity comes from. Debian, Ubuntu +// and Alpine advisories carry no severity of their own — trivy-db leaves +// Advisory.Severity zero for those buckets and keeps the rating in the +// vulnerability bucket's VendorSeverity map instead. Taking the advisory's +// value alone reported an entire fleet as "unknown". +// +// One cache per tick, shared across servers: a CVE affects every host running +// the package, and the bolt read is the same read every time. +type MetaCache struct { + src VulnSource + seen map[string]VulnInfo +} + +func NewMetaCache(src VulnSource) *MetaCache { + return &MetaCache{src: src, seen: make(map[string]VulnInfo)} +} + +// Enrich fills severity, title, score and references in place. +// +// The advisory's severity is kept when the vulnerability bucket has nothing +// better to say — RHEL does publish it per advisory — so this can only raise +// the quality of the answer, never lower it. +func (m *MetaCache) Enrich(results []Result) { + if m == nil { + return + } + for i := range results { + info, ok := m.lookup(results[i].CVEID) + if !ok { + continue + } + if info.Severity != "" && info.Severity != "unknown" { + results[i].Severity = info.Severity + } + results[i].Title = info.Title + results[i].CVSSScore = info.CVSSScore + results[i].References = info.References + } +} + +func (m *MetaCache) lookup(cveID string) (VulnInfo, bool) { + if info, ok := m.seen[cveID]; ok { + return info, true + } + info, err := m.src.Vulnerability(cveID) + if err != nil { + // A CVE with an advisory but no vulnerability document is a real state + // in trivy-db, not a fault. Cache the miss so it is asked once. + Debugf("no vulnerability record for %s: %v", cveID, err) + m.seen[cveID] = VulnInfo{} + return VulnInfo{}, false + } + m.seen[cveID] = info + return info, true +} diff --git a/server/internal/vulndb/match.go b/server/internal/vulndb/match.go index 54bd055..3382f74 100644 --- a/server/internal/vulndb/match.go +++ b/server/internal/vulndb/match.go @@ -14,12 +14,19 @@ type AdvisorySource interface { } // Result is one vulnerable package on one server, before it becomes a finding. +// +// Severity, Title, CVSSScore and References are only as good as the advisory +// until MetaCache.Enrich has run over them — for the Debian-family buckets the +// advisory carries no severity at all, so an unenriched Result reads "unknown". type Result struct { CVEID string PackageName string // the BINARY package, which is what is installed Installed string FixedIn string Severity string + Title string + CVSSScore float64 + References []string } // Match returns every advisory that the installed packages do not satisfy. diff --git a/server/internal/vulnsched/sched.go b/server/internal/vulnsched/sched.go index d5e6fb5..1372456 100644 --- a/server/internal/vulnsched/sched.go +++ b/server/internal/vulnsched/sched.go @@ -181,13 +181,17 @@ func (s *scheduler) scanPending(ctx context.Context) { // channel muted, and either way the alerts stop being read. newly := map[string][]models.VulnFinding{} + // One metadata cache for the whole tick: a CVE affects every host running + // the package, and the lookup is the same read each time. + meta := vulndb.NewMetaCache(s.store) + for _, sp := range pending { if ctx.Err() != nil { // Leadership lost. scan_pending is still set, so the next leader // picks these up — which is why it lives on the document. return } - opened := s.scanOne(ctx, sp) + opened := s.scanOne(ctx, sp, meta) newly[sp.InstanceID] = append(newly[sp.InstanceID], opened...) } @@ -203,7 +207,7 @@ func (s *scheduler) scanPending(ctx context.Context) { ) } -func (s *scheduler) scanOne(ctx context.Context, sp models.ServerPackages) []models.VulnFinding { +func (s *scheduler) scanOne(ctx context.Context, sp models.ServerPackages, meta *vulndb.MetaCache) []models.VulnFinding { now := time.Now() log.Printf("vulnsched: scanning server %s (instance %s, os %s %s, %d packages)", sp.ServerID, sp.InstanceID, sp.OS.Family, sp.OS.VersionID, len(sp.Packages)) @@ -226,6 +230,11 @@ func (s *scheduler) scanOne(ctx context.Context, sp models.ServerPackages) []mod return nil } + // Severity for the Debian-family buckets lives on the CVE, not the + // advisory. Without this every finding is stored as "unknown", which also + // silences every alert rule with a minimum severity. + meta.Enrich(results) + existing, err := services.ListFindings(ctx, sp.InstanceID, sp.ServerID) if err != nil { log.Printf("vulnsched: list findings %s: %v", sp.ServerID, err)