Skip to content

Commit

Permalink
Merge pull request #26 from cultureamp/support-cvss3
Browse files Browse the repository at this point in the history
feat: support CVSS3 scores in rendered annotation
  • Loading branch information
jamestelfer authored Nov 30, 2023
2 parents 9b12f26 + 7f19863 commit d2634fc
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 44 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,31 @@ behaviour.
> "ignore" configuration file instead: see the [ignore
> findings](./docs/ignore-findings.md) documentation.
## Rendering

The plugin shows a detailed summary of the vulnerability findings in the scanned image using data pulled from AWS ECR. The summary is rendered as a Buildkite [build annotation](https://buildkite.com/docs/agent/v3/cli-annotate).

<figure>
<figcaption>
The default view summarizes the number of findings in the scan, hiding details behind an expanding element.
</figcaption>
<img src="docs/img/eg-success-collapsed.png" alt="example of successful check annotation with collapsed results table">
</figure>

<figure>
<figcaption>
When a threshold is exceeded, the annotation is rendered as an error.
</figcaption>
<img src="docs/img/eg-failed-collapsed.png" alt="example of failed check annotation with collapsed results table" width="80%" align="center">
</figure>

<figure>
<figcaption>
The details view can be expanded, showing a table of the vulnerability findings from the scan. Findings link to the CVE database in most cases. The CVSS vector links to the <a href="https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV%3AN%2FAC%3AL%2FPR%3AN%2FUI%3AN%2FS%3AU%2FC%3AN%2FI%3AN%2FA%3AH&version=3.1">CVSS calculator</a>, allowing for exploration of the potential impact and enabling environmental scoring.
</figcaption>
<img src="docs/img/eg-success-expanded.png" alt="example of successful check annotation with collapsed results table" width="80%" align="center">
</figure>

## Example

Add the following lines to your `pipeline.yml`:
Expand Down
Binary file added docs/img/eg-failed-collapsed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/eg-success-collapsed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/eg-success-expanded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 87 additions & 7 deletions src/finding/summary.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package finding

import (
"net/url"
"regexp"
"slices"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
Expand All @@ -18,12 +21,18 @@ type Detail struct {

PackageName string
PackageVersion string
CVSS2Score string
CVSS2Vector string
CVSS2 CVSSScore
CVSS3 CVSSScore

Ignore *findingconfig.Ignore
}

type CVSSScore struct {
Score string
Vector string
VectorURL string
}

type SeverityCount struct {
// Included is the number of findings that count towards the threshold for this severity.
Included int32
Expand Down Expand Up @@ -103,16 +112,34 @@ func Summarize(findings *types.ImageScanFindings, ignoreConfig []findingconfig.I
}

func findingToDetail(finding types.ImageScanFinding) Detail {
name := aws.ToString(finding.Name)
uri := aws.ToString(finding.Uri)

uri = fixFindingURI(name, uri)

cvss2Vector := findingAttributeValue(finding, "CVSS2_VECTOR")
cvss2VectorURL := cvss2VectorURL(cvss2Vector)

cvss3Vector := findingAttributeValue(finding, "CVSS3_VECTOR")
cvss3Vector, cvss3VectorURL := cvss3VectorURL(cvss3Vector)

return Detail{
Name: aws.ToString(finding.Name),
URI: aws.ToString(finding.Uri),
Name: name,
URI: uri,
Description: aws.ToString(finding.Description),
Severity: finding.Severity,
PackageName: findingAttributeValue(finding, "package_name"),
PackageVersion: findingAttributeValue(finding, "package_version"),
CVSS2Score: findingAttributeValue(finding, "CVSS2_SCORE"),
CVSS2Vector: findingAttributeValue(finding, "CVSS2_VECTOR"),
}
CVSS2: CVSSScore{
Score: findingAttributeValue(finding, "CVSS2_SCORE"),
Vector: cvss2Vector,
VectorURL: cvss2VectorURL,
},
CVSS3: CVSSScore{
Score: findingAttributeValue(finding, "CVSS3_SCORE"),
Vector: cvss3Vector,
VectorURL: cvss3VectorURL,
}}
}

func findingAttributeValue(finding types.ImageScanFinding, name string) string {
Expand All @@ -123,3 +150,56 @@ func findingAttributeValue(finding types.ImageScanFinding, name string) string {
}
return ""
}

const legacyCVEURL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name="
const updatedCVEURL = "https://www.cve.org/CVERecord?id="

func fixFindingURI(name string, uri string) string {
correctedURI := uri

// transition from the old CVE site that is deprecated
if strings.HasPrefix(correctedURI, legacyCVEURL) {
correctedURI = strings.Replace(correctedURI, legacyCVEURL, updatedCVEURL, 1)
}

// sometimes links are published that are not valid: in this case point to a
// GH vuln search as a way to provide some value
if strings.HasPrefix(correctedURI, updatedCVEURL) && !strings.HasPrefix(name, "CVE-") {
correctedURI = "https://github.com/advisories?query=" + url.QueryEscape(name)
}

return correctedURI
}

func cvss2VectorURL(cvss2Vector string) string {
if cvss2Vector == "" {
return ""
}

return "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?vector=" +
url.QueryEscape("("+cvss2Vector+")")
}

// CVSS3 vector have their version at the front: we need to split this out to
// pass to the calculator URL
var cvss3VectorPattern = regexp.MustCompile(`^CVSS:([\d.]+)/(.+)$`)

func cvss3VectorURL(versionedVector string) (string, string) {
vector := versionedVector
vectorURL := ""

if versionedVector != "" {
version := "3.1"

if matches := cvss3VectorPattern.FindStringSubmatch(versionedVector); matches != nil {
version = matches[1]
vector = matches[2]
}

vectorURL = "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator" +
"?vector=" + url.QueryEscape(vector) +
"&version=" + url.QueryEscape(version)
}

return vector, vectorURL
}
112 changes: 112 additions & 0 deletions src/finding/summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package finding_test
import (
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/cultureamp/ecrscanresults/finding"
"github.com/cultureamp/ecrscanresults/findingconfig"
Expand All @@ -28,6 +29,87 @@ func TestSummarize(t *testing.T) {
Ignored: []finding.Detail{},
}),
},
{
name: "findings with links",
data: types.ImageScanFindings{
Findings: []types.ImageScanFinding{
fu("CVE-2019-5188", "HIGH", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5188"),
fu("INVALID-CVE", "CRITICAL", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1234"),
fu("CVE-2019-5189", "HIGH", "https://notamitre.org.site/search?name=CVE-2019-5189"),
},
},
expected: autogold.Expect(finding.Summary{
Counts: map[types.FindingSeverity]finding.SeverityCount{
types.FindingSeverity("CRITICAL"): {Included: 1},
types.FindingSeverity("HIGH"): {Included: 2},
},
Details: []finding.Detail{
{
Name: "CVE-2019-5188",
URI: "https://www.cve.org/CVERecord?id=CVE-2019-5188",
Severity: types.FindingSeverity("HIGH"),
},
{
Name: "INVALID-CVE",
URI: "https://github.com/advisories?query=INVALID-CVE",
Severity: types.FindingSeverity("CRITICAL"),
},
{
Name: "CVE-2019-5189",
URI: "https://notamitre.org.site/search?name=CVE-2019-5189",
Severity: types.FindingSeverity("HIGH"),
},
},
Ignored: []finding.Detail{},
}),
},
{
name: "findings with CVSS2 and CVSS3 scores",
data: types.ImageScanFindings{
Findings: []types.ImageScanFinding{
fscore("CVE-2019-5188", "HIGH", "1.2", "AV:L/AC:L/Au:N/C:P/I:P/A:P"),
fscore("INVALID-CVE", "CRITICAL", "", ""),
fscore("CVE-2019-5189", "HIGH", "6", ""),
fscore3("CVE-2019-5189", "HIGH", "9", "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"),
},
},
expected: autogold.Expect(finding.Summary{
Counts: map[types.FindingSeverity]finding.SeverityCount{
types.FindingSeverity("CRITICAL"): {Included: 1},
types.FindingSeverity("HIGH"): {Included: 3},
},
Details: []finding.Detail{
{
Name: "CVE-2019-5188",
Severity: types.FindingSeverity("HIGH"),
CVSS2: finding.CVSSScore{
Score: "1.2",
Vector: "AV:L/AC:L/Au:N/C:P/I:P/A:P",
VectorURL: "https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?vector=%28AV%3AL%2FAC%3AL%2FAu%3AN%2FC%3AP%2FI%3AP%2FA%3AP%29",
},
},
{
Name: "INVALID-CVE",
Severity: types.FindingSeverity("CRITICAL"),
},
{
Name: "CVE-2019-5189",
Severity: types.FindingSeverity("HIGH"),
CVSS2: finding.CVSSScore{Score: "6"},
},
{
Name: "CVE-2019-5189",
Severity: types.FindingSeverity("HIGH"),
CVSS3: finding.CVSSScore{
Score: "9",
Vector: "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
VectorURL: "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV%3AN%2FAC%3AL%2FPR%3AN%2FUI%3AN%2FS%3AU%2FC%3AH%2FI%3AH%2FA%3AN&version=3.1",
},
},
},
Ignored: []finding.Detail{},
}),
},
{
name: "findings with no ignores",
data: types.ImageScanFindings{
Expand Down Expand Up @@ -118,6 +200,36 @@ func f(name string, severity types.FindingSeverity) types.ImageScanFinding {
}
}

func fu(name string, severity types.FindingSeverity, uri string) types.ImageScanFinding {
return types.ImageScanFinding{
Name: &name,
Uri: &uri,
Severity: severity,
}
}

func fscore(name string, severity types.FindingSeverity, cvss2 string, vector string) types.ImageScanFinding {
return types.ImageScanFinding{
Name: &name,
Severity: severity,
Attributes: []types.Attribute{
{Key: aws.String("CVSS2_SCORE"), Value: &cvss2},
{Key: aws.String("CVSS2_VECTOR"), Value: &vector},
},
}
}

func fscore3(name string, severity types.FindingSeverity, score string, vector string) types.ImageScanFinding {
return types.ImageScanFinding{
Name: &name,
Severity: severity,
Attributes: []types.Attribute{
{Key: aws.String("CVSS3_SCORE"), Value: &score},
{Key: aws.String("CVSS3_VECTOR"), Value: &vector},
},
}
}

func i(id string) findingconfig.Ignore {
return findingconfig.Ignore{ID: id}
}
18 changes: 12 additions & 6 deletions src/report/annotation.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,33 @@ be wrapped in <p> tag by the Markdown renderer in Buildkite.
{{ define "findingName" }}{{ if .Description }}<details><summary>{{ template "findingNameLink" . }}</summary><div>{{ .Description }}</div></details>{{ else }}{{ template "findingNameLink" . }}{{ end }}{{ end }}
{{ define "findingIgnoreUntil" }}{{ if .Until | hasUntilValue }}{{ .Until }}{{ else }}<div class="italic">(indefinitely)</div>{{ end }}{{ end }}
{{ define "findingIgnore"}}{{ if .Reason }}<details><summary>{{ template "findingIgnoreUntil" . }}</summary><div>{{ .Reason }}</div></details>{{ else }}{{ template "findingIgnoreUntil" . }}{{ end }}{{ end }}
{{ define "cvssScore" }}{{ .Score | nbsp}}{{ end }}
{{ define "cvssVector" }}{{ if .Vector }}<a href="{{ .VectorURL }}">{{ .Vector }}</a>{{ else }}&nbsp;{{end}}{{ end }}
{{ define "cvssCells" }}
{{ if .CVSS3.Score }}<td>{{ template "cvssScore" .CVSS3 }}</td><td>{{ template "cvssVector" .CVSS3 }}</td>{{
else
}}<td>{{ if .CVSS2.Score }}{{ template "cvssScore" .CVSS2 }} <em>(*CVSS2)</em>{{ end }}</td><td>{{ template "cvssVector" .CVSS2 }}</td>{{ end }}
{{ end }}
{{ if (or .FindingSummary.Details .FindingSummary.Ignored) }}
<details>
<summary>Vulnerability details</summary>
<div>
<p>All listed scores are CVSS3 unless otherwise noted.</p>
{{ if .FindingSummary.Details }}
<table>
<tr>
<th>CVE</th>
<th>Severity</th>
<th>Affects</th>
<th>CVSS score</th>
<th>Vector</th>
<th>CVSS vector</th>
</tr>
{{ range $f := .FindingSummary.Details | sortFindings }}
<tr>
<td>{{ template "findingName" . }}</td>
<td>{{ $f.Severity | string | lowerCase | titleCase }}</td>
<td>{{ $f.PackageName | nbsp }} {{ $f.PackageVersion | nbsp }}</td>
<td>{{ $f.CVSS2Score | nbsp}}</td>
<td>{{ if $f.CVSS2Vector }}<a href="https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?vector=({{ $f.CVSS2Vector }})">{{ $f.CVSS2Vector }}</a>{{ else }}&nbsp;{{ end }}</td>
{{ template "cvssCells" $f }}
</tr>
{{ end }}
</table>
Expand All @@ -77,16 +84,15 @@ be wrapped in <p> tag by the Markdown renderer in Buildkite.
<th>Ignored until</th>
<th>Affects</th>
<th>CVSS score</th>
<th>Vector</th>
<th>CVSS vector</th>
</tr>
{{ range $f := .FindingSummary.Ignored | sortFindings }}
<tr>
<td>{{ template "findingName" . }}</td>
<td>{{ $f.Severity | string | lowerCase | titleCase }}</td>
<td>{{ template "findingIgnore" $f.Ignore }}</td>
<td>{{ $f.PackageName | nbsp }} {{ $f.PackageVersion | nbsp }}</td>
<td>{{ $f.CVSS2Score | nbsp}}</td>
<td>{{ if $f.CVSS2Vector }}<a href="https://nvd.nist.gov/vuln-metrics/cvss/v2-calculator?vector=({{ $f.CVSS2Vector }})">{{ $f.CVSS2Vector }}</a>{{ else }}&nbsp;{{ end }}</td>
{{ template "cvssCells" $f }}
</tr>
{{ end }}
</table>
Expand Down
Loading

0 comments on commit d2634fc

Please sign in to comment.