diff --git a/go.mod b/go.mod index 56cdd5a3971..7ecac673fab 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/sirupsen/logrus v1.7.0 github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 + github.com/spiegel-im-spiegel/go-cvss v0.4.0 github.com/stretchr/testify v1.7.0 github.com/yuin/goldmark v1.3.1 golang.org/x/net v0.0.0-20201224014010-6772e930b67b diff --git a/go.sum b/go.sum index b9276047a8f..67fbbbfd4e6 100644 --- a/go.sum +++ b/go.sum @@ -676,6 +676,10 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spiegel-im-spiegel/errs v1.0.2 h1:v4amEwRDqRWjKHOILQnJSovYhZ4ZttEnBBXNXEzS6Sc= +github.com/spiegel-im-spiegel/errs v1.0.2/go.mod h1:UoasJYYujMcdkbT9USv8dfZWoMyaY3btqQxoLJImw0A= +github.com/spiegel-im-spiegel/go-cvss v0.4.0 h1:A+S9I01wkiEo6e66xFbxtz9LfXdsGUr5FppYozUAmmI= +github.com/spiegel-im-spiegel/go-cvss v0.4.0/go.mod h1:VrcmtyfJ7OJF3/O08MQnxq+kqyPMjstExKdv02TPx+M= github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/pkg/notes/document/template.go b/pkg/notes/document/template.go index 477cd709099..fe305374bcd 100644 --- a/pkg/notes/document/template.go +++ b/pkg/notes/document/template.go @@ -72,8 +72,11 @@ This release contains changes that address the following vulnerabilities: {{.Description}} -**CVSS Rating:** {{.CVSSRating}} ({{.CVSSScore}}) [{{.CVSSVector}}]({{.CalcLink}})
+**CVSS Rating:** {{.CVSSRating}} ({{.CVSSScore}}) [{{.CVSSVector}}]({{.CalcLink}}) +{{- if .TrackingIssue -}} +
**Tracking Issue:** {{.TrackingIssue}} +{{- end }} {{ end }} {{- end -}} diff --git a/pkg/notes/notes.go b/pkg/notes/notes.go index 6ba79bb7fdc..182166e51ca 100644 --- a/pkg/notes/notes.go +++ b/pkg/notes/notes.go @@ -33,6 +33,7 @@ import ( "github.com/nozzle/throttler" "github.com/pkg/errors" "github.com/sirupsen/logrus" + cvss "github.com/spiegel-im-spiegel/go-cvss/v3/metric" "gopkg.in/yaml.v2" "k8s.io/release/pkg/github" @@ -51,6 +52,9 @@ const ( // maxParallelRequests is the maximum parallel requests we shall make to the // GitHub API maxParallelRequests = 10 + + // Regexp to check CVE IDs + cveIDRegExp = `^CVE-\d{4}-\d+$` ) type ( @@ -63,12 +67,12 @@ type CVEData struct { ID string `json:"id"` // CVE ID, eg CVE-2019-1010260 Title string `json:"title"` // Title of the vulnerability Description string `json:"description"` // Description text of the vulnerability - TrackingIssue string `json:"issue"` // Link to the vulnerability tracking issue + TrackingIssue string `json:"issue"` // Link to the vulnerability tracking issue (url, optional) CVSSVector string `json:"vector"` // Full CVSS vector string, CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:H CVSSScore float32 `json:"score"` // Numeric CVSS score (eg 6.2) CVSSRating string `json:"rating"` // Severity bucket (eg Medium) CalcLink string // Link to the CVE calculator (automatic) - LinkedPRs []int `json:"linkedPRs"` // List of linked PRs (to remove them from the release notes doc) + LinkedPRs []int `json:"pullrequests"` // List of linked PRs (to remove them from the release notes doc) } const ( @@ -1090,31 +1094,51 @@ func (rn *ReleaseNote) ContentHash() (string, error) { // Validate checks the data defined in a CVE map is complete and valid func (cve *CVEData) Validate() error { + // Verify that rating is defined and a known string if cve.CVSSRating == "" { return errors.New("CVSS rating missing from CVE data") } // Check rating is a valid string - if cve.CVSSRating != "None" && - cve.CVSSRating != "Low" && - cve.CVSSRating != "Medium" && - cve.CVSSRating != "High" && - cve.CVSSRating != "Critical" { + if _, ok := map[string]bool{ + "None": true, "Low": true, "Medium": true, "High": true, "Critical": true, + }[cve.CVSSRating]; !ok { return errors.New("Invalid CVSS rating") } + // Check vector string is not empty if cve.CVSSVector == "" { return errors.New("CVSS vector string missing from CVE data") } + // Parse the vector string to make sure it is well formed + bm, err := cvss.NewBase().Decode(cve.CVSSVector) + if err != nil { + return errors.Wrap(err, "parsing CVSS vector string") + } + cve.CalcLink = fmt.Sprintf( + "https://www.first.org/cvss/calculator/%s#%s", bm.Ver.String(), cve.CVSSVector, + ) + if cve.CVSSScore == 0 { return errors.New("CVSS score missing from CVE data") } + if cve.CVSSScore < 0 || cve.CVSSScore > 10 { + return errors.New("CVSS score pit of range, should be 0-10") + } + // Check that the CVE ID is not empty if cve.ID == "" { return errors.New("ID missing from CVE data") } + // Verify that the CVE ID is well formed + cvsre := regexp.MustCompile(cveIDRegExp) + if !cvsre.MatchString(cve.ID) { + return errors.New("CVS ID is not well formed") + } + + // Title and description must not be empty if cve.Title == "" { return errors.New("Title missing from CVE data") } @@ -1123,14 +1147,5 @@ func (cve *CVEData) Validate() error { return errors.New("CVE description missing from CVE data") } - // Since we're checking the vector string with a regex, use the effort to - // add a link to the CVE calculator - re := regexp.MustCompile(`^CVSS:(\d+\.\d+)/`) - cvssVer := re.FindStringSubmatch(cve.CVSSVector) - if len(cvssVer) == 0 { - return errors.New("CVSS vector in not properly formed: version missing") - } - cve.CalcLink = fmt.Sprintf("https://www.first.org/cvss/calculator/%s#%s", cvssVer[1], cve.CVSSVector) - return nil } diff --git a/pkg/notes/notes_test.go b/pkg/notes/notes_test.go index 1b81c224067..4c9e226bd93 100644 --- a/pkg/notes/notes_test.go +++ b/pkg/notes/notes_test.go @@ -430,3 +430,74 @@ func TestApplyMap(t *testing.T) { } } } + +func TestCVEDataValidation(t *testing.T) { + var sut CVEData + cve := CVEData{ + ID: "CVE-2020-8559", + Title: "Privilege escalation from compromised node to cluster", + Description: "If an attacker is able to intercept certain requests to the Kubelet, they", + TrackingIssue: "https://github.com/kubernetes/kubernetes/issues/92914", + CVSSVector: "CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:U/C:H/I:H/A:H", + CVSSScore: 6.4, + CVSSRating: "Medium", + LinkedPRs: []int{92941, 92969, 92970, 92971}, + } + + // As is, the CVE should validate + require.Nil(t, cve.Validate()) + + // Check each value + sut = cve + sut.ID = "CVE-123" + require.NotNil(t, sut.Validate(), "checking cve id") + + sut = cve + sut.Title = "" + require.NotNil(t, sut.Validate(), "checking title") + + sut = cve + sut.Description = "" + require.NotNil(t, sut.Validate(), "checking description") + + sut = cve + for _, testVector := range []string{ + "CVSS:3.1/AV:N/AC:H/P", // too short + "", // empty + "CVSS:3.1/AV:N/AC:Á/PR:H/UI:R/S:U/C:H/I:H/A:H", // invalid value + } { + sut.CVSSVector = testVector // too short + require.NotNil(t, sut.Validate(), "checking vector string") + } + + sut = cve + for _, testScore := range []float32{ + 0, // cannot be 0 (nil value) + -1, // under + 10.1, // over + } { + sut.CVSSScore = testScore + require.NotNil(t, sut.Validate(), "checking vector string") + } + + sut = cve + for _, tc := range []struct { + Valid bool + Value string + }{ + {true, "None"}, + {true, "Low"}, + {true, "Medium"}, + {true, "High"}, + {true, "Critical"}, + {false, ""}, + {false, "Superbad"}, + } { + sut.CVSSRating = tc.Value // too short + if tc.Valid { + require.Nil(t, sut.Validate(), "checking valid rating string") + } else { + require.NotNil(t, sut.Validate(), "checking invalid rating string") + } + } +}