Skip to content

Commit

Permalink
feat: add vulnerability status (#328)
Browse files Browse the repository at this point in the history
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
  • Loading branch information
knqyf263 and DmitriyLewen authored Jul 26, 2023
1 parent 05620b0 commit 167ba4f
Show file tree
Hide file tree
Showing 10 changed files with 724 additions and 125 deletions.
70 changes: 70 additions & 0 deletions pkg/types/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package types

import "encoding/json"

type Status int

var (
// Statuses is a list of statuses.
// VEX has 4 statuses: not-affected, affected, fixed, and under_investigation.
// cf. https://www.cisa.gov/sites/default/files/2023-04/minimum-requirements-for-vex-508c.pdf
//
// In addition to them, Red Hat has "will_not_fix" and "fix_deferred".
// cf. https://access.redhat.com/blogs/product-security/posts/2066793
Statuses = []string{
"unknown",
"not_affected",
"affected",
"fixed",
"under_investigation",
"will_not_fix",
"fix_deferred",
"end_of_life",
}
)

const (
StatusUnknown Status = iota
StatusNotAffected
StatusAffected
StatusFixed
StatusUnderInvestigation
StatusWillNotFix // Red Hat specific
StatusFixDeferred
StatusEndOfLife
)

func NewStatus(status string) Status {
for i, s := range Statuses {
if status == s {
return Status(i)
}
}
return StatusUnknown
}

func (s *Status) String() string {
idx := s.Index()
if idx < 0 || idx >= len(Statuses) {
idx = 0 // unknown
}
return Statuses[idx]
}

func (s *Status) Index() int {
return int(*s)
}

func (s *Status) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

func (s *Status) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}

*s = NewStatus(str)
return nil
}
35 changes: 34 additions & 1 deletion pkg/types/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

import (
"encoding/json"
"fmt"
"time"
)
Expand Down Expand Up @@ -102,7 +103,7 @@ type Advisory struct {

// It is filled only when FixedVersion is empty since it is obvious the state is "Fixed" when FixedVersion is not empty.
// e.g. Will not fix and Affected
State string `json:",omitempty"`
Status Status `json:"-"`

// Trivy DB has "vulnerability" bucket and severities are usually stored in the bucket per a vulnerability ID.
// In some cases, the advisory may have multiple severities depending on the packages.
Expand All @@ -127,6 +128,38 @@ type Advisory struct {
Custom interface{} `json:",omitempty"`
}

// _Advisory is an internal struct for Advisory to avoid infinite MarshalJSON loop.
type _Advisory Advisory

type dbAdvisory struct {
_Advisory
IntStatus int `json:"Status,omitempty"`
}

// MarshalJSON customizes how an Advisory is marshaled to JSON.
// It is used when saving the Advisory to the BoltDB database.
// To reduce the size of the database, the Status field is converted to an integer before being saved,
// while the status is normally exported as a string in JSON.
// This is done by creating an anonymous struct that has all the same fields as Advisory,
// but with the Status field replaced by an IntStatus field of type int.
func (a *Advisory) MarshalJSON() ([]byte, error) {
advisory := dbAdvisory{
_Advisory: _Advisory(*a),
IntStatus: int(a.Status),
}
return json.Marshal(advisory)
}

func (a *Advisory) UnmarshalJSON(data []byte) error {
var advisory dbAdvisory
if err := json.Unmarshal(data, &advisory); err != nil {
return err
}
advisory._Advisory.Status = Status(advisory.IntStatus)
*a = Advisory(advisory._Advisory)
return nil
}

// Advisories saves fixed versions for each arches/vendorIDs
// e.g. this is required when CVE has different fixed versions for different arches
type Advisories struct {
Expand Down
20 changes: 18 additions & 2 deletions pkg/vulnsrc/debian/debian.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,12 +458,12 @@ func defaultPut(dbc db.Operation, tx *bolt.Tx, advisory interface{}) error {

detail := types.Advisory{
VendorIDs: adv.VendorIDs,
State: adv.State,
Status: newStatus(adv.State),
Severity: severityFromUrgency(adv.Severity),
FixedVersion: adv.FixedVersion,
}

if err := dbc.PutAdvisoryDetail(tx, adv.VulnerabilityID, adv.PkgName, []string{adv.Platform}, detail); err != nil {
if err := dbc.PutAdvisoryDetail(tx, adv.VulnerabilityID, adv.PkgName, []string{adv.Platform}, &detail); err != nil {
return xerrors.Errorf("failed to save Debian advisory: %w", err)
}

Expand Down Expand Up @@ -660,3 +660,19 @@ func compareVersions(v1, v2 string) (int, error) {

return ver1.Compare(ver2), nil
}

func newStatus(s string) types.Status {
switch strings.ToLower(s) {
// "end-of-life" is considered as vulnerable
// e.g. https://security-tracker.debian.org/tracker/CVE-2022-1488
case "no-dsa", "unfixed":
return types.StatusAffected
case "ignored":
return types.StatusWillNotFix
case "postponed":
return types.StatusFixDeferred
case "end-of-life":
return types.StatusEndOfLife
}
return types.StatusUnknown
}
107 changes: 84 additions & 23 deletions pkg/vulnsrc/debian/debian_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ func TestVulnSrc_Update(t *testing.T) {
dir: filepath.Join("testdata", "happy"),
wantValues: []vulnsrctest.WantValues{
{
Key: []string{"data-source", "debian 9"},
Key: []string{
"data-source",
"debian 9",
},
Value: types.DataSource{
ID: vulnerability.Debian,
Name: "Debian Security Tracker",
Expand All @@ -32,79 +35,137 @@ func TestVulnSrc_Update(t *testing.T) {
},
// Ref. https://security-tracker.debian.org/tracker/CVE-2021-33560
{
Key: []string{"advisory-detail", "CVE-2021-33560", "debian 9", "libgcrypt20"},
Value: types.Advisory{
Key: []string{
"advisory-detail",
"CVE-2021-33560",
"debian 9",
"libgcrypt20",
},
Value: &types.Advisory{
VendorIDs: []string{"DLA-2691-1"},
FixedVersion: "1.7.6-2+deb9u4",
},
},
{
Key: []string{"advisory-detail", "CVE-2021-33560", "debian 10", "libgcrypt20"},
Value: types.Advisory{
Key: []string{
"advisory-detail",
"CVE-2021-33560",
"debian 10",
"libgcrypt20",
},
Value: &types.Advisory{
FixedVersion: "1.8.4-5+deb10u1",
},
},
{
Key: []string{"advisory-detail", "CVE-2021-33560", "debian 11", "libgcrypt20"},
Value: types.Advisory{
Key: []string{
"advisory-detail",
"CVE-2021-33560",
"debian 11",
"libgcrypt20",
},
Value: &types.Advisory{
FixedVersion: "1.8.7-6",
},
},
{
Key: []string{"advisory-detail", "CVE-2021-29629", "debian 10", "dacs"},
Value: types.Advisory{
State: "ignored",
Key: []string{
"advisory-detail",
"CVE-2021-29629",
"debian 10",
"dacs",
},
Value: &types.Advisory{
Severity: types.SeverityLow,
Status: types.StatusWillNotFix,
},
},
{
Key: []string{"advisory-detail", "DSA-3714-1", "debian 8", "akonadi"},
Value: types.Advisory{
Key: []string{
"advisory-detail",
"DSA-3714-1",
"debian 8",
"akonadi",
},
Value: &types.Advisory{
VendorIDs: []string{"DSA-3714-1"},
FixedVersion: "1.13.0-2+deb8u2",
},
},
{
// wrong no-dsa
Key: []string{"advisory-detail", "CVE-2020-8631", "debian 11", "cloud-init"},
Value: types.Advisory{
Key: []string{
"advisory-detail",
"CVE-2020-8631",
"debian 11",
"cloud-init",
},
Value: &types.Advisory{
FixedVersion: "19.4-2",
},
},
{
Key: []string{"vulnerability-detail", "CVE-2021-33560", string(vulnerability.Debian)},
Key: []string{
"vulnerability-detail",
"CVE-2021-33560",
string(vulnerability.Debian),
},
Value: types.VulnerabilityDetail{
Title: "Libgcrypt before 1.8.8 and 1.9.x before 1.9.3 mishandles ElGamal encry ...",
},
},
{
Key: []string{"vulnerability-detail", "CVE-2021-29629", string(vulnerability.Debian)},
Key: []string{
"vulnerability-detail",
"CVE-2021-29629",
string(vulnerability.Debian),
},
Value: types.VulnerabilityDetail{
Title: "In FreeBSD 13.0-STABLE before n245765-bec0d2c9c841, 12.2-STABLE before ...",
},
},
{
Key: []string{"vulnerability-detail", "DSA-3714-1", string(vulnerability.Debian)},
Key: []string{
"vulnerability-detail",
"DSA-3714-1",
string(vulnerability.Debian),
},
Value: types.VulnerabilityDetail{
Title: "akonadi - update",
},
},
{
Key: []string{"vulnerability-id", "CVE-2021-33560"},
Key: []string{
"vulnerability-id",
"CVE-2021-33560",
},
Value: map[string]interface{}{},
},
{
Key: []string{"vulnerability-id", "CVE-2021-29629"},
Key: []string{
"vulnerability-id",
"CVE-2021-29629",
},
Value: map[string]interface{}{},
},
{
Key: []string{"vulnerability-id", "DSA-3714-1"},
Key: []string{
"vulnerability-id",
"DSA-3714-1",
},
Value: map[string]interface{}{},
},
},
noBuckets: [][]string{
{"advisory-detail", "CVE-2021-29629", "debian 9"}, // not-affected in debian stretch
{"advisory-detail", "CVE-2016-4606"}, // not-affected in sid
{
"advisory-detail",
"CVE-2021-29629",
"debian 9",
}, // not-affected in debian stretch
{
"advisory-detail",
"CVE-2016-4606",
}, // not-affected in sid
},
},
{
Expand Down Expand Up @@ -162,7 +223,7 @@ func TestVulnSrc_Get(t *testing.T) {
},
{
VulnerabilityID: "CVE-2021-38370",
State: "no-dsa",
Status: types.StatusAffected,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/vulnsrc/debian/testdata/fixtures/debian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- key: CVE-2021-38370
value:
FixedVersion: ""
State: "no-dsa"
Status: 2
- key: CVE-2008-5514
value:
FixedVersion: "2.02-3.1"
Loading

0 comments on commit 167ba4f

Please sign in to comment.