Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions integration/testdata/alpine-310.sarif.golden
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
"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",
"purls": [
"pkg:apk/alpine/libcrypto1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2",
"pkg:apk/alpine/libssl1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2"
],
"security-severity": "5.3",
"tags": [
"vulnerability",
Expand Down Expand Up @@ -63,6 +67,10 @@
"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",
"purls": [
"pkg:apk/alpine/libcrypto1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2",
"pkg:apk/alpine/libssl1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2"
],
"security-severity": "5.3",
"tags": [
"vulnerability",
Expand Down
37 changes: 35 additions & 2 deletions pkg/report/sarif.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/url"
"path/filepath"
"regexp"
"sort"
"strings"

containerName "github.com/google/go-containerregistry/pkg/name"
Expand All @@ -15,6 +16,7 @@ import (

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/set"
"github.com/aquasecurity/trivy/pkg/types"
)

Expand Down Expand Up @@ -47,6 +49,7 @@ type SarifWriter struct {
Version string
run *sarif.Run
locationCache map[string][]location
rulePurls map[string]set.Set[string]
Target string
}

Expand All @@ -67,6 +70,7 @@ type sarifData struct {
cvssScore string
cvssData map[string]any
locations []location
purls []string
}

type location struct {
Expand All @@ -75,6 +79,7 @@ type location struct {
}

func (sw *SarifWriter) addSarifRule(data *sarifData) {
purls := sw.mergeRulePurls(data.vulnerabilityId, data.purls)
r := sw.run.AddRule(data.vulnerabilityId).
WithName(toSarifRuleName(data.resourceClass)).
WithDescription(data.vulnerabilityId).
Expand All @@ -87,12 +92,30 @@ func (sw *SarifWriter) addSarifRule(data *sarifData) {
WithDefaultConfiguration(&sarif.ReportingConfiguration{
Level: toSarifErrorLevel(data.severity),
}).
WithProperties(toProperties(data.title, data.severity, data.cvssScore, data.cvssData))
WithProperties(toProperties(data.title, data.severity, data.cvssScore, data.cvssData, purls))
if data.url != nil && data.url.String() != "" {
r.WithHelpURI(data.url.String())
}
}

// mergeRulePurls aggregates PURLs across multiple calls for the same vulnerability ID
// and returns the deduplicated, sorted slice. The same rule is shared by every
// package affected by the vulnerability, so we accumulate all of their PURLs.
func (sw *SarifWriter) mergeRulePurls(vulnerabilityId string, purls []string) []string {
s, ok := sw.rulePurls[vulnerabilityId]
if !ok {
s = set.New[string]()
sw.rulePurls[vulnerabilityId] = s
}
s.Append(purls...)
if s.Size() == 0 {
return nil
}
out := s.Items()
sort.Strings(out)
return out
}

func (sw *SarifWriter) addSarifResult(data *sarifData) {
sw.addSarifRule(data)

Expand Down Expand Up @@ -134,6 +157,7 @@ func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
sw.run.Tool.Driver.WithVersion(sw.Version)
sw.run.Tool.Driver.WithFullName("Trivy Vulnerability Scanner")
sw.locationCache = make(map[string][]location)
sw.rulePurls = make(map[string]set.Set[string])
if report.ArtifactType == ftypes.TypeContainerImage {
sw.run.Properties = sarif.Properties{
"imageName": report.ArtifactName,
Expand All @@ -157,6 +181,10 @@ func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
path = ToPathUri(vuln.PkgPath, res.Class)
}
cvssData, cvssScore := toCVSSData(vuln)
var purls []string
if vuln.PkgIdentifier.PURL != nil {
purls = []string{vuln.PkgIdentifier.PURL.String()}
}
sw.addSarifResult(&sarifData{
title: "vulnerability",
vulnerabilityId: vuln.VulnerabilityID,
Expand All @@ -171,6 +199,7 @@ func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
resultIndex: getRuleIndex(vuln.VulnerabilityID, ruleIndexes),
shortDescription: vuln.Title,
fullDescription: fullDescription,
purls: purls,
helpText: fmt.Sprintf("Vulnerability %v\nSeverity: %v\nPackage: %v\nFixed Version: %v\nLink: [%v](%v)\n%v",
vuln.VulnerabilityID, vuln.Severity, vuln.PkgName, vuln.FixedVersion, vuln.VulnerabilityID, vuln.PrimaryURL, vuln.Description),
helpMarkdown: fmt.Sprintf("**Vulnerability %v**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|%v|%v|%v|[%v](%v)|\n\n%v",
Expand Down Expand Up @@ -459,7 +488,7 @@ func severityToScore(severity string) string {
}
}

func toProperties(title, severity, cvssScore string, cvssData map[string]any) sarif.Properties {
func toProperties(title, severity, cvssScore string, cvssData map[string]any, purls []string) sarif.Properties {
properties := sarif.Properties{
"tags": []string{
title,
Expand All @@ -484,5 +513,9 @@ func toProperties(title, severity, cvssScore string, cvssData map[string]any) sa
properties[key] = value
}

if len(purls) > 0 {
properties["purls"] = purls
}

return properties
}
153 changes: 152 additions & 1 deletion pkg/report/sarif_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/owenrumney/go-sarif/v2/sarif"
"github.com/package-url/packageurl-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -72,6 +73,13 @@ func TestReportWriter_Sarif(t *testing.T) {
FixedVersion: "3.4.5",
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001",
SeveritySource: "redhat",
PkgIdentifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeDebian,
Name: "foo",
Version: "1.2.3",
},
},
Vulnerability: dbTypes.Vulnerability{
Title: "foobar",
Description: "baz",
Expand Down Expand Up @@ -127,6 +135,7 @@ func TestReportWriter_Sarif(t *testing.T) {
"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,
"purls": []any{"pkg:deb/foo@1.2.3"},
},
Help: &sarif.MultiformatMessageString{
Text: new("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"),
Expand Down Expand Up @@ -189,6 +198,148 @@ func TestReportWriter_Sarif(t *testing.T) {
},
},
},
{
name: "vulnerability rule aggregates PURLs across packages",
target: "/tmp/scan",
input: types.Report{
Results: types.Results{
{
Target: "go.mod",
Class: types.ClassLangPkg,
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2020-9999",
PkgName: "bar",
InstalledVersion: "1.0.0",
Vulnerability: dbTypes.Vulnerability{
Severity: "HIGH",
},
PkgIdentifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Name: "bar",
Version: "1.0.0",
},
},
},
{
VulnerabilityID: "CVE-2020-9999",
PkgName: "baz",
InstalledVersion: "2.0.0",
Vulnerability: dbTypes.Vulnerability{
Severity: "HIGH",
},
PkgIdentifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Name: "baz",
Version: "2.0.0",
},
},
},
},
},
},
},
want: &sarif.Report{
Version: "2.1.0",
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
Runs: []*sarif.Run{
{
Tool: sarif.Tool{
Driver: &sarif.ToolComponent{
FullName: new("Trivy Vulnerability Scanner"),
Name: "Trivy",
Version: new(""),
InformationURI: new("https://github.com/aquasecurity/trivy"),
Rules: []*sarif.ReportingDescriptor{
{
ID: "CVE-2020-9999",
Name: new("LanguageSpecificPackageVulnerability"),
ShortDescription: &sarif.MultiformatMessageString{Text: new("")},
FullDescription: &sarif.MultiformatMessageString{Text: new("")},
DefaultConfiguration: &sarif.ReportingConfiguration{
Level: "error",
},
Properties: map[string]any{
"tags": []any{
"vulnerability",
"security",
"HIGH",
},
"precision": "very-high",
"security-severity": "8.0",
"purls": []any{
"pkg:golang/bar@1.0.0",
"pkg:golang/baz@2.0.0",
},
},
Help: &sarif.MultiformatMessageString{
Text: new("Vulnerability CVE-2020-9999\nSeverity: HIGH\nPackage: baz\nFixed Version: \nLink: [CVE-2020-9999]()\n"),
Markdown: new("**Vulnerability CVE-2020-9999**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|HIGH|baz||[CVE-2020-9999]()|\n\n"),
},
},
},
},
},
Results: []*sarif.Result{
{
RuleID: new("CVE-2020-9999"),
RuleIndex: new(uint(0)),
Level: new("error"),
Message: sarif.Message{Text: new("Package: bar\nInstalled Version: 1.0.0\nVulnerability CVE-2020-9999\nSeverity: HIGH\nFixed Version: \nLink: [CVE-2020-9999]()")},
Locations: []*sarif.Location{
{
Message: &sarif.Message{Text: new("go.mod: bar@1.0.0")},
PhysicalLocation: &sarif.PhysicalLocation{
ArtifactLocation: &sarif.ArtifactLocation{
URI: new("go.mod"),
URIBaseId: new("ROOTPATH"),
},
Region: &sarif.Region{
StartLine: new(1),
EndLine: new(1),
StartColumn: new(1),
EndColumn: new(1),
},
},
},
},
},
{
RuleID: new("CVE-2020-9999"),
RuleIndex: new(uint(0)),
Level: new("error"),
Message: sarif.Message{Text: new("Package: baz\nInstalled Version: 2.0.0\nVulnerability CVE-2020-9999\nSeverity: HIGH\nFixed Version: \nLink: [CVE-2020-9999]()")},
Locations: []*sarif.Location{
{
Message: &sarif.Message{Text: new("go.mod: baz@2.0.0")},
PhysicalLocation: &sarif.PhysicalLocation{
ArtifactLocation: &sarif.ArtifactLocation{
URI: new("go.mod"),
URIBaseId: new("ROOTPATH"),
},
Region: &sarif.Region{
StartLine: new(1),
EndLine: new(1),
StartColumn: new(1),
EndColumn: new(1),
},
},
},
},
},
},
ColumnKind: "utf16CodeUnits",
OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{
"ROOTPATH": {
URI: new(tmpScanURI),
},
},
},
},
},
},
{
name: "report with misconfigurations",
target: "/tmp/scan",
Expand Down Expand Up @@ -964,7 +1115,7 @@ func TestMakePropertiesMarshal(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := report.ToProperties(tt.title, tt.severity, tt.cvssScore, tt.cvssData)
result := report.ToProperties(tt.title, tt.severity, tt.cvssScore, tt.cvssData, nil)

actualJSON, err := json.Marshal(result)
require.NoError(t, err)
Expand Down
Loading