Skip to content

Commit

Permalink
osv: parse database_specific severity when no CVSS severity is defined
Browse files Browse the repository at this point in the history
Occasionally there are OSV advisories that don't include any severity
information in the `.severity` object but they do contain a severity in
the `.database_specific` object. This change attempts to parse that
severity if we don't get a severity from the native `.severity` object.

Signed-off-by: crozzy <[email protected]>
  • Loading branch information
crozzy committed Mar 20, 2024
1 parent 4fc28be commit 9c22ab8
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 0 deletions.
42 changes: 42 additions & 0 deletions updater/osv/osv.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ func (e *ecs) Insert(ctx context.Context, skipped *stats, name string, a *adviso
proto.Severity = s.Score
proto.NormalizedSeverity, err = fromCVSS2(s.Score)
default:
// We didn't get a severity from the CVSS scores
continue
}
if err != nil {
Expand All @@ -513,6 +514,19 @@ func (e *ecs) Insert(ctx context.Context, skipped *stats, name string, a *adviso
Msg("odd cvss mangling result")
}
}

if proto.Severity == "" {
// Try and extract a severity from the database_specific object
var databaseJSON map[string]json.RawMessage
if err := json.Unmarshal([]byte(a.Database), &databaseJSON); err == nil {
var severityString string
if err := json.Unmarshal(databaseJSON["severity"], &severityString); err == nil {
proto.Severity = severityString
proto.NormalizedSeverity = severityFromDBString(severityString)
}
}
}

for i, ref := range a.References {
if i != 0 {
b.WriteByte(' ')
Expand Down Expand Up @@ -660,6 +674,34 @@ func (e *ecs) Insert(ctx context.Context, skipped *stats, name string, a *adviso
return nil
}

// severityFromDBString takes a severity string defined in the
// database_specific object and parses it into a claircore.Severity.
// Known severities in the OSV data are (in varying cases):
// - CRITICAL
// - HIGH
// - LOW
// - MEDIUM
// - MODERATE
// - UNKNOWN
func severityFromDBString(s string) (sev claircore.Severity) {
sev = claircore.Unknown
switch {
case strings.EqualFold(s, "unknown"):
sev = claircore.Unknown
case strings.EqualFold(s, "negligible"):
sev = claircore.Negligible
case strings.EqualFold(s, "low"):
sev = claircore.Low
case strings.EqualFold(s, "moderate"), strings.EqualFold(s, "medium"):
sev = claircore.Medium
case strings.EqualFold(s, "high"):
sev = claircore.High
case strings.EqualFold(s, "critical"):
sev = claircore.Critical
}
return sev
}

// All the methods follow the same pattern: just reslice the slice if
// there's space, or use append to do an alloc+copy.

Expand Down
163 changes: 163 additions & 0 deletions updater/osv/osv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/quay/zlog"

"github.com/quay/claircore"
"github.com/quay/claircore/libvuln/driver"
)

Expand Down Expand Up @@ -148,3 +149,165 @@ func TestParse(t *testing.T) {
}
}
}

var severityTestCases = []struct {
name string
a *advisory
expectedNormalizedSeverity claircore.Severity
expectedSeverity string
}{
{
name: "CVSS V3 HIGH",
a: &advisory{
ID: "test1",
Severity: []severity{
{
Type: "CVSS_V3",
Score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
},
},
Affected: []affected{
{
Package: _package{
Ecosystem: "go",
Name: "something",
},
Ranges: []_range{
{
Type: "ECOSYSTEM",
Events: []rangeEvent{
{
Introduced: "0.1",
Fixed: "0.4",
},
},
},
},
},
},
},
expectedNormalizedSeverity: claircore.High,
expectedSeverity: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
},
{
name: "CVSS V2 MEDIUM",
a: &advisory{
ID: "test2",
Severity: []severity{
{
Type: "CVSS_V2",
Score: "AV:L/AC:H/Au:N/C:C/I:C/A:C",
},
},
Affected: []affected{
{
Package: _package{
Ecosystem: "go",
Name: "something",
},
Ranges: []_range{
{
Type: "ECOSYSTEM",
Events: []rangeEvent{
{
Introduced: "0.1",
Fixed: "0.4",
},
},
},
},
},
},
},
expectedNormalizedSeverity: claircore.Medium,
expectedSeverity: "AV:L/AC:H/Au:N/C:C/I:C/A:C",
},
{
name: "database_specific moderate",
a: &advisory{
ID: "test2",
Affected: []affected{
{
Package: _package{
Ecosystem: "go",
Name: "something",
},
Ranges: []_range{
{
Type: "ECOSYSTEM",
Events: []rangeEvent{
{
Introduced: "0.1",
Fixed: "0.4",
},
},
},
},
},
},
Database: json.RawMessage([]byte(`{"severity":"moderate"}`)),
},
expectedNormalizedSeverity: claircore.Medium,
expectedSeverity: "moderate",
},
{
name: "CVSS V3 HIGH and database_specific moderate",
a: &advisory{
ID: "test2",
Severity: []severity{
{
Type: "CVSS_V3",
Score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
},
},
Affected: []affected{
{
Package: _package{
Ecosystem: "go",
Name: "something",
},
Ranges: []_range{
{
Type: "ECOSYSTEM",
Events: []rangeEvent{
{
Introduced: "0.1",
Fixed: "0.4",
},
},
},
},
},
},
Database: json.RawMessage([]byte(`{"severity":"moderate"}`)),
},
expectedNormalizedSeverity: claircore.High,
expectedSeverity: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
},
}

func TestSeverityParsing(t *testing.T) {
ctx := zlog.Test(context.Background(), t)

for _, tt := range severityTestCases {
t.Run(tt.name, func(t *testing.T) {
ecs := newECS("test")

err := ecs.Insert(ctx, nil, "", tt.a)
if err != nil {
t.Error("got error Inserting advisory", err)
}
if len(ecs.Vulnerability) != 1 {
t.Errorf("should have one vulnerability but got %d", len(ecs.Vulnerability))
}
v := ecs.Vulnerability[0]
if v.NormalizedSeverity != tt.expectedNormalizedSeverity {
t.Errorf("expected severity %q but got %q", tt.expectedNormalizedSeverity, v.NormalizedSeverity)
}
if v.Severity != tt.expectedSeverity {
t.Errorf("expected severity %q but got %q", tt.expectedSeverity, v.Severity)
}

})
}
}

0 comments on commit 9c22ab8

Please sign in to comment.