fix: Fixed vuln score
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user