Skip to content

Commit 1573bd9

Browse files
authored
More complete severity parsing for v6 DBs (#2431)
* more complete severity parsing Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update rounding to cvss spec Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * enforce severity string case Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
1 parent 154fd9f commit 1573bd9

8 files changed

Lines changed: 533 additions & 86 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ require (
5151
github.com/olekukonko/tablewriter v0.0.5
5252
github.com/openvex/go-vex v0.2.5
5353
github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554
54+
github.com/pandatix/go-cvss v0.6.2
5455
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
5556
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
5657
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,8 @@ github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKp
13461346
github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94=
13471347
github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
13481348
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
1349+
github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI=
1350+
github.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q=
13491351
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
13501352
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
13511353
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=

grype/db/v6/blobs.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,14 @@ type CVSSSeverity struct {
9292

9393
// Version is the CVSS version (e.g. "3.0")
9494
Version string `json:"version,omitempty"`
95-
96-
// Score is the evaluated CVSS vector as a scalar between 0 and 10
97-
Score float64 `json:"score"`
9895
}
9996

10097
func (c CVSSSeverity) String() string {
10198
vector := c.Vector
10299
if !strings.HasPrefix(strings.ToLower(c.Vector), "cvss:") && c.Version != "" {
103100
vector = fmt.Sprintf("CVSS:%s/%s", c.Version, c.Vector)
104101
}
105-
return fmt.Sprintf("%s (%1.1f)", vector, c.Score)
102+
return vector
106103
}
107104

108105
// AffectedPackageBlob represents a package affected by a vulnerability.

grype/db/v6/cvss.go

Lines changed: 0 additions & 37 deletions
This file was deleted.

grype/db/v6/severity.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package v6
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"strings"
7+
8+
gocvss20 "github.com/pandatix/go-cvss/20"
9+
gocvss30 "github.com/pandatix/go-cvss/30"
10+
gocvss31 "github.com/pandatix/go-cvss/31"
11+
gocvss40 "github.com/pandatix/go-cvss/40"
12+
13+
"github.com/anchore/grype/grype/vulnerability"
14+
"github.com/anchore/grype/internal/log"
15+
)
16+
17+
func extractSeverities(vuln *VulnerabilityHandle) (vulnerability.Severity, []vulnerability.Cvss, error) {
18+
if vuln.BlobValue == nil {
19+
return vulnerability.UnknownSeverity, nil, nil
20+
}
21+
sev := vulnerability.UnknownSeverity
22+
if len(vuln.BlobValue.Severities) > 0 {
23+
var err error
24+
// grype DB v6+ will order the set of severities by rank, so we can just take the first one
25+
sev, err = extractSeverity(vuln.BlobValue.Severities[0].Value)
26+
if err != nil {
27+
return vulnerability.UnknownSeverity, nil, fmt.Errorf("unable to extract severity: %w", err)
28+
}
29+
}
30+
return sev, toCvss(vuln.BlobValue.Severities...), nil
31+
}
32+
33+
func extractSeverity(severity any) (vulnerability.Severity, error) {
34+
switch sev := severity.(type) {
35+
case string:
36+
return vulnerability.ParseSeverity(sev), nil
37+
case CVSSSeverity:
38+
metrics, err := parseCVSS(sev.Vector)
39+
if err != nil {
40+
return vulnerability.UnknownSeverity, fmt.Errorf("unable to parse CVSS vector: %w", err)
41+
}
42+
if metrics == nil {
43+
return vulnerability.UnknownSeverity, nil
44+
}
45+
return interpretCVSS(metrics.BaseScore, sev.Version), nil
46+
default:
47+
return vulnerability.UnknownSeverity, nil
48+
}
49+
}
50+
51+
func parseCVSS(vector string) (*vulnerability.CvssMetrics, error) {
52+
switch {
53+
case strings.HasPrefix(vector, "CVSS:3.0"):
54+
cvss, err := gocvss30.ParseVector(vector)
55+
if err != nil {
56+
return nil, fmt.Errorf("unable to parse CVSS v3 vector: %w", err)
57+
}
58+
ex := roundScore(cvss.Exploitability())
59+
im := roundScore(cvss.Impact())
60+
return &vulnerability.CvssMetrics{
61+
BaseScore: roundScore(cvss.BaseScore()),
62+
ExploitabilityScore: &ex,
63+
ImpactScore: &im,
64+
}, nil
65+
case strings.HasPrefix(vector, "CVSS:3.1"):
66+
cvss, err := gocvss31.ParseVector(vector)
67+
if err != nil {
68+
return nil, fmt.Errorf("unable to parse CVSS v3.1 vector: %w", err)
69+
}
70+
ex := roundScore(cvss.Exploitability())
71+
im := roundScore(cvss.Impact())
72+
return &vulnerability.CvssMetrics{
73+
BaseScore: roundScore(cvss.BaseScore()),
74+
ExploitabilityScore: &ex,
75+
ImpactScore: &im,
76+
}, nil
77+
case strings.HasPrefix(vector, "CVSS:4.0"):
78+
cvss, err := gocvss40.ParseVector(vector)
79+
if err != nil {
80+
return nil, fmt.Errorf("unable to parse CVSS v4.0 vector: %w", err)
81+
}
82+
// there are no exploitability and impact scores in CVSS v4.0
83+
return &vulnerability.CvssMetrics{
84+
BaseScore: roundScore(cvss.Score()),
85+
}, nil
86+
default:
87+
// should be CVSS v2.0 or is invalid
88+
cvss, err := gocvss20.ParseVector(vector)
89+
if err != nil {
90+
return nil, fmt.Errorf("unable to parse CVSS v2 vector: %w", err)
91+
}
92+
ex := roundScore(cvss.Exploitability())
93+
im := roundScore(cvss.Impact())
94+
return &vulnerability.CvssMetrics{
95+
BaseScore: roundScore(cvss.BaseScore()),
96+
ExploitabilityScore: &ex,
97+
ImpactScore: &im,
98+
}, nil
99+
}
100+
}
101+
102+
// roundScore rounds the score to the nearest tenth based on first.org rounding rules
103+
// see https://www.first.org/cvss/v3.1/specification-document#Appendix-A---Floating-Point-Rounding
104+
func roundScore(score float64) float64 {
105+
intInput := int(math.Round(score * 100000))
106+
if intInput%10000 == 0 {
107+
return float64(intInput) / 100000.0
108+
}
109+
return (math.Floor(float64(intInput)/10000.0) + 1) / 10.0
110+
}
111+
112+
func interpretCVSS(score float64, version string) vulnerability.Severity {
113+
switch version {
114+
case "2.0":
115+
return interpretCVSSv2(score)
116+
case "3.0", "3.1", "4.0":
117+
return interpretCVSSv3Plus(score)
118+
default:
119+
return vulnerability.UnknownSeverity
120+
}
121+
}
122+
123+
func interpretCVSSv2(score float64) vulnerability.Severity {
124+
if score < 0 {
125+
return vulnerability.UnknownSeverity
126+
}
127+
if score == 0 {
128+
return vulnerability.NegligibleSeverity
129+
}
130+
if score < 4.0 {
131+
return vulnerability.LowSeverity
132+
}
133+
if score < 7.0 {
134+
return vulnerability.MediumSeverity
135+
}
136+
if score <= 10.0 {
137+
return vulnerability.HighSeverity
138+
}
139+
return vulnerability.UnknownSeverity
140+
}
141+
142+
func interpretCVSSv3Plus(score float64) vulnerability.Severity {
143+
if score < 0 {
144+
return vulnerability.UnknownSeverity
145+
}
146+
if score == 0 {
147+
return vulnerability.NegligibleSeverity
148+
}
149+
if score < 4.0 {
150+
return vulnerability.LowSeverity
151+
}
152+
if score < 7.0 {
153+
return vulnerability.MediumSeverity
154+
}
155+
if score < 9.0 {
156+
return vulnerability.HighSeverity
157+
}
158+
if score <= 10.0 {
159+
return vulnerability.CriticalSeverity
160+
}
161+
return vulnerability.UnknownSeverity
162+
}
163+
164+
func toCvss(severities ...Severity) []vulnerability.Cvss {
165+
//nolint:prealloc
166+
var out []vulnerability.Cvss
167+
for _, sev := range severities {
168+
switch sev.Scheme {
169+
case SeveritySchemeCVSS:
170+
default:
171+
// not a CVSS score
172+
continue
173+
}
174+
cvssSev, ok := sev.Value.(CVSSSeverity)
175+
if !ok {
176+
// not a CVSS score
177+
continue
178+
}
179+
var usedMetrics vulnerability.CvssMetrics
180+
// though the DB has the base score, we parse the vector for all metrics
181+
metrics, err := parseCVSS(cvssSev.Vector)
182+
if err != nil {
183+
log.WithFields("vector", cvssSev.Vector, "error", err).Warn("unable to parse CVSS vector")
184+
continue
185+
}
186+
if metrics != nil {
187+
usedMetrics = *metrics
188+
}
189+
190+
out = append(out, vulnerability.Cvss{
191+
Source: sev.Source,
192+
Type: string(sev.Scheme),
193+
Version: cvssSev.Version,
194+
Vector: cvssSev.Vector,
195+
Metrics: usedMetrics,
196+
})
197+
}
198+
return out
199+
}

0 commit comments

Comments
 (0)