feat(report): add CVSS vectors in sarif report (#9157)

This commit is contained in:
Stepan
2025-07-14 12:22:56 +03:00
committed by GitHub
parent 153318f65f
commit 60723e6cfc
4 changed files with 179 additions and 15 deletions

View File

@@ -27,6 +27,10 @@
"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)."
},
"properties": {
"cvssv2_score": 5,
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
"cvssv3_baseScore": 5.3,
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"precision": "very-high",
"security-severity": "5.3",
"tags": [
@@ -54,6 +58,10 @@
"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)."
},
"properties": {
"cvssv2_score": 5,
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
"cvssv3_baseScore": 5.3,
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"precision": "very-high",
"security-severity": "5.3",
"tags": [

View File

@@ -67,6 +67,7 @@ type sarifData struct {
locationMessage string
message string
cvssScore string
cvssData map[string]any
locations []location
}
@@ -88,15 +89,7 @@ func (sw *SarifWriter) addSarifRule(data *sarifData) {
WithDefaultConfiguration(&sarif.ReportingConfiguration{
Level: toSarifErrorLevel(data.severity),
}).
WithProperties(sarif.Properties{
"tags": []string{
data.title,
"security",
data.severity,
},
"precision": "very-high",
"security-severity": data.cvssScore,
})
WithProperties(toProperties(data.title, data.severity, data.cvssScore, data.cvssData))
if data.url != nil && data.url.String() != "" {
r.WithHelpURI(data.url.String())
}
@@ -158,11 +151,13 @@ func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
if vuln.PkgPath != "" {
path = ToPathUri(vuln.PkgPath, res.Class)
}
cvssData, cvssScore := toCVSSData(vuln)
sw.addSarifResult(&sarifData{
title: "vulnerability",
vulnerabilityId: vuln.VulnerabilityID,
severity: vuln.Severity,
cvssScore: getCVSSScore(vuln),
cvssScore: cvssScore,
cvssData: cvssData,
url: toUri(vuln.PrimaryURL),
resourceClass: res.Class,
artifactLocation: toUri(path),
@@ -414,14 +409,28 @@ func (sw *SarifWriter) getLocations(name, version, path string, pkgs []ftypes.Pa
return locs
}
func getCVSSScore(vuln types.DetectedVulnerability) string {
// Take the vendor score
// toCVSSData extracts CVSS data from the vulnerability and returns it along with the score.
// If CVSS V3 Score is not available, it returns an empty CVSSData struct and a score based on severity.
func toCVSSData(vuln types.DetectedVulnerability) (map[string]any, string) {
score := severityToScore(vuln.Severity)
var data = make(map[string]any)
// Note: 'cvssv3_baseScore' uses a hybrid naming convention (snake_case + camelCase)
// Reference: https://docs.aws.amazon.com/codecatalyst/latest/userguide/test.sarif.html
if cvss, ok := vuln.CVSS[vuln.SeveritySource]; ok {
return fmt.Sprintf("%.1f", cvss.V3Score)
data["cvssv2_vector"] = cvss.V2Vector
data["cvssv2_score"] = cvss.V2Score
data["cvssv3_vector"] = cvss.V3Vector
data["cvssv3_baseScore"] = cvss.V3Score
data["cvssv40_vector"] = cvss.V40Vector
data["cvssv40_baseScore"] = cvss.V40Score
if cvss.V3Score != 0 {
score = fmt.Sprintf("%.1f", cvss.V3Score)
}
}
// Converts severity to score
return severityToScore(vuln.Severity)
return data, score
}
func severityToScore(severity string) string {
@@ -438,3 +447,31 @@ func severityToScore(severity string) string {
return "0.0"
}
}
func toProperties(title, severity, cvssScore string, cvssData map[string]any) sarif.Properties {
properties := sarif.Properties{
"tags": []string{
title,
"security",
severity,
},
"precision": "very-high",
"security-severity": cvssScore,
}
for key, value := range cvssData {
switch v := value.(type) {
case string:
if v == "" {
continue
}
case float64:
if v == 0 {
continue
}
}
properties[key] = value
}
return properties
}

View File

@@ -1,8 +1,11 @@
package report
import (
"bytes"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -57,3 +60,117 @@ func Test_clearURI(t *testing.T) {
})
}
}
func TestMakePropertiesMarshal(t *testing.T) {
tests := []struct {
name string
title string
severity string
cvssScore string
cvssData map[string]any
expected string
}{
{
name: "no CVSS data",
title: "test",
severity: "HIGH",
cvssScore: "5.0",
cvssData: make(map[string]any),
expected: `{
"precision": "very-high",
"security-severity": "5.0",
"tags": ["test", "security", "HIGH"]
}`,
},
{
name: "only CVSS v2",
title: "test",
severity: "CRITICAL",
cvssScore: "4.0",
cvssData: map[string]any{
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
"cvssv2_score": 5.0,
},
expected: `{
"cvssv2_score": 5,
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N",
"precision": "very-high",
"security-severity": "4.0",
"tags": ["test", "security", "CRITICAL"]
}`,
},
{
name: "only CVSS v3",
title: "test",
severity: "CRITICAL",
cvssScore: "9.8",
cvssData: map[string]any{
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"cvssv3_baseScore": 9.8,
},
expected: `{
"cvssv3_baseScore": 9.8,
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"precision": "very-high",
"security-severity": "9.8",
"tags": ["test", "security", "CRITICAL"]
}`,
},
{
name: "only CVSS v4",
title: "test",
severity: "LOW",
cvssScore: "3.5",
cvssData: map[string]any{
"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",
"cvssv40_baseScore": 3.5,
},
expected: `{
"cvssv40_baseScore": 3.5,
"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",
"precision": "very-high",
"security-severity": "3.5",
"tags": ["test", "security", "LOW"]
}`,
},
{
name: "all CVSS versions",
title: "test",
severity: "HIGH",
cvssScore: "8.1",
cvssData: map[string]any{
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"cvssv2_score": 7.5,
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"cvssv3_baseScore": 9.8,
"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",
"cvssv40_baseScore": 9.3,
},
expected: `{
"cvssv2_score": 7.5,
"cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"cvssv3_baseScore": 9.8,
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"cvssv40_baseScore": 9.3,
"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",
"precision": "very-high",
"security-severity": "8.1",
"tags": ["test", "security", "HIGH"]
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := toProperties(tt.title, tt.severity, tt.cvssScore, tt.cvssData)
actualJSON, err := json.Marshal(result)
require.NoError(t, err)
var expectedJSON bytes.Buffer
err = json.Compact(&expectedJSON, []byte(tt.expected))
require.NoError(t, err)
assert.JSONEq(t, expectedJSON.String(), string(actualJSON))
})
}
}

View File

@@ -118,6 +118,8 @@ func TestReportWriter_Sarif(t *testing.T) {
},
"precision": "very-high",
"security-severity": "7.5",
"cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"cvssv3_baseScore": 7.5,
},
Help: &sarif.MultiformatMessageString{
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"),