Skip to content

Commit 60723e6

Browse files
authored
feat(report): add CVSS vectors in sarif report (#9157)
1 parent 153318f commit 60723e6

File tree

4 files changed

+179
-15
lines changed

4 files changed

+179
-15
lines changed

integration/testdata/alpine-310.sarif.golden

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
"markdown": "**Vulnerability CVE-2019-1549**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|MEDIUM|libssl1.1|1.1.1d-r0|[CVE-2019-1549](https://avd.aquasec.com/nvd/cve-2019-1549)|\n\nOpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c)."
2828
},
2929
"properties": {
30+
"cvssv2_score": 5,
31+
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
32+
"cvssv3_baseScore": 5.3,
33+
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
3034
"precision": "very-high",
3135
"security-severity": "5.3",
3236
"tags": [
@@ -54,6 +58,10 @@
5458
"markdown": "**Vulnerability CVE-2019-1551**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|MEDIUM|libssl1.1|1.1.1d-r2|[CVE-2019-1551](https://avd.aquasec.com/nvd/cve-2019-1551)|\n\nThere is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t)."
5559
},
5660
"properties": {
61+
"cvssv2_score": 5,
62+
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
63+
"cvssv3_baseScore": 5.3,
64+
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
5765
"precision": "very-high",
5866
"security-severity": "5.3",
5967
"tags": [

pkg/report/sarif.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type sarifData struct {
6767
locationMessage string
6868
message string
6969
cvssScore string
70+
cvssData map[string]any
7071
locations []location
7172
}
7273

@@ -88,15 +89,7 @@ func (sw *SarifWriter) addSarifRule(data *sarifData) {
8889
WithDefaultConfiguration(&sarif.ReportingConfiguration{
8990
Level: toSarifErrorLevel(data.severity),
9091
}).
91-
WithProperties(sarif.Properties{
92-
"tags": []string{
93-
data.title,
94-
"security",
95-
data.severity,
96-
},
97-
"precision": "very-high",
98-
"security-severity": data.cvssScore,
99-
})
92+
WithProperties(toProperties(data.title, data.severity, data.cvssScore, data.cvssData))
10093
if data.url != nil && data.url.String() != "" {
10194
r.WithHelpURI(data.url.String())
10295
}
@@ -158,11 +151,13 @@ func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
158151
if vuln.PkgPath != "" {
159152
path = ToPathUri(vuln.PkgPath, res.Class)
160153
}
154+
cvssData, cvssScore := toCVSSData(vuln)
161155
sw.addSarifResult(&sarifData{
162156
title: "vulnerability",
163157
vulnerabilityId: vuln.VulnerabilityID,
164158
severity: vuln.Severity,
165-
cvssScore: getCVSSScore(vuln),
159+
cvssScore: cvssScore,
160+
cvssData: cvssData,
166161
url: toUri(vuln.PrimaryURL),
167162
resourceClass: res.Class,
168163
artifactLocation: toUri(path),
@@ -414,14 +409,28 @@ func (sw *SarifWriter) getLocations(name, version, path string, pkgs []ftypes.Pa
414409
return locs
415410
}
416411

417-
func getCVSSScore(vuln types.DetectedVulnerability) string {
418-
// Take the vendor score
412+
// toCVSSData extracts CVSS data from the vulnerability and returns it along with the score.
413+
// If CVSS V3 Score is not available, it returns an empty CVSSData struct and a score based on severity.
414+
func toCVSSData(vuln types.DetectedVulnerability) (map[string]any, string) {
415+
score := severityToScore(vuln.Severity)
416+
var data = make(map[string]any)
417+
418+
// Note: 'cvssv3_baseScore' uses a hybrid naming convention (snake_case + camelCase)
419+
// Reference: https://docs.aws.amazon.com/codecatalyst/latest/userguide/test.sarif.html
419420
if cvss, ok := vuln.CVSS[vuln.SeveritySource]; ok {
420-
return fmt.Sprintf("%.1f", cvss.V3Score)
421+
data["cvssv2_vector"] = cvss.V2Vector
422+
data["cvssv2_score"] = cvss.V2Score
423+
data["cvssv3_vector"] = cvss.V3Vector
424+
data["cvssv3_baseScore"] = cvss.V3Score
425+
data["cvssv40_vector"] = cvss.V40Vector
426+
data["cvssv40_baseScore"] = cvss.V40Score
427+
428+
if cvss.V3Score != 0 {
429+
score = fmt.Sprintf("%.1f", cvss.V3Score)
430+
}
421431
}
422432

423-
// Converts severity to score
424-
return severityToScore(vuln.Severity)
433+
return data, score
425434
}
426435

427436
func severityToScore(severity string) string {
@@ -438,3 +447,31 @@ func severityToScore(severity string) string {
438447
return "0.0"
439448
}
440449
}
450+
451+
func toProperties(title, severity, cvssScore string, cvssData map[string]any) sarif.Properties {
452+
properties := sarif.Properties{
453+
"tags": []string{
454+
title,
455+
"security",
456+
severity,
457+
},
458+
"precision": "very-high",
459+
"security-severity": cvssScore,
460+
}
461+
462+
for key, value := range cvssData {
463+
switch v := value.(type) {
464+
case string:
465+
if v == "" {
466+
continue
467+
}
468+
case float64:
469+
if v == 0 {
470+
continue
471+
}
472+
}
473+
properties[key] = value
474+
}
475+
476+
return properties
477+
}

pkg/report/sarif_private_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package report
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"testing"
57

8+
"github.com/stretchr/testify/assert"
69
"github.com/stretchr/testify/require"
710
)
811

@@ -57,3 +60,117 @@ func Test_clearURI(t *testing.T) {
5760
})
5861
}
5962
}
63+
64+
func TestMakePropertiesMarshal(t *testing.T) {
65+
tests := []struct {
66+
name string
67+
title string
68+
severity string
69+
cvssScore string
70+
cvssData map[string]any
71+
expected string
72+
}{
73+
{
74+
name: "no CVSS data",
75+
title: "test",
76+
severity: "HIGH",
77+
cvssScore: "5.0",
78+
cvssData: make(map[string]any),
79+
expected: `{
80+
"precision": "very-high",
81+
"security-severity": "5.0",
82+
"tags": ["test", "security", "HIGH"]
83+
}`,
84+
},
85+
{
86+
name: "only CVSS v2",
87+
title: "test",
88+
severity: "CRITICAL",
89+
cvssScore: "4.0",
90+
cvssData: map[string]any{
91+
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
92+
"cvssv2_score": 5.0,
93+
},
94+
expected: `{
95+
"cvssv2_score": 5,
96+
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
97+
"precision": "very-high",
98+
"security-severity": "4.0",
99+
"tags": ["test", "security", "CRITICAL"]
100+
}`,
101+
},
102+
{
103+
name: "only CVSS v3",
104+
title: "test",
105+
severity: "CRITICAL",
106+
cvssScore: "9.8",
107+
cvssData: map[string]any{
108+
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
109+
"cvssv3_baseScore": 9.8,
110+
},
111+
expected: `{
112+
"cvssv3_baseScore": 9.8,
113+
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
114+
"precision": "very-high",
115+
"security-severity": "9.8",
116+
"tags": ["test", "security", "CRITICAL"]
117+
}`,
118+
},
119+
{
120+
name: "only CVSS v4",
121+
title: "test",
122+
severity: "LOW",
123+
cvssScore: "3.5",
124+
cvssData: map[string]any{
125+
"cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
126+
"cvssv40_baseScore": 3.5,
127+
},
128+
expected: `{
129+
"cvssv40_baseScore": 3.5,
130+
"cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
131+
"precision": "very-high",
132+
"security-severity": "3.5",
133+
"tags": ["test", "security", "LOW"]
134+
}`,
135+
},
136+
{
137+
name: "all CVSS versions",
138+
title: "test",
139+
severity: "HIGH",
140+
cvssScore: "8.1",
141+
cvssData: map[string]any{
142+
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
143+
"cvssv2_score": 7.5,
144+
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
145+
"cvssv3_baseScore": 9.8,
146+
"cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
147+
"cvssv40_baseScore": 9.3,
148+
},
149+
expected: `{
150+
"cvssv2_score": 7.5,
151+
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
152+
"cvssv3_baseScore": 9.8,
153+
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
154+
"cvssv40_baseScore": 9.3,
155+
"cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
156+
"precision": "very-high",
157+
"security-severity": "8.1",
158+
"tags": ["test", "security", "HIGH"]
159+
}`,
160+
},
161+
}
162+
163+
for _, tt := range tests {
164+
t.Run(tt.name, func(t *testing.T) {
165+
result := toProperties(tt.title, tt.severity, tt.cvssScore, tt.cvssData)
166+
167+
actualJSON, err := json.Marshal(result)
168+
require.NoError(t, err)
169+
170+
var expectedJSON bytes.Buffer
171+
err = json.Compact(&expectedJSON, []byte(tt.expected))
172+
require.NoError(t, err)
173+
assert.JSONEq(t, expectedJSON.String(), string(actualJSON))
174+
})
175+
}
176+
}

pkg/report/sarif_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ func TestReportWriter_Sarif(t *testing.T) {
118118
},
119119
"precision": "very-high",
120120
"security-severity": "7.5",
121+
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
122+
"cvssv3_baseScore": 7.5,
121123
},
122124
Help: &sarif.MultiformatMessageString{
123125
Text: lo.ToPtr("Vulnerability CVE-2020-0001\nSeverity: HIGH\nPackage: foo\nFixed Version: 3.4.5\nLink: [CVE-2020-0001](https://avd.aquasec.com/nvd/cve-2020-0001)\nbaz"),

0 commit comments

Comments
 (0)