diff --git a/README.md b/README.md index e0cafedac9..dc3933fddb 100644 --- a/README.md +++ b/README.md @@ -899,6 +899,11 @@ In the following example using the template `sarif.tpl` [Sarif](https://docs.git $ trivy image --format template --template "@contrib/sarif.tpl" -o report.sarif golang:1.12-alpine ``` +You can also use the default SARIF format that's included with Trivy as follows +``` +$ trivy image --format=sarif golang:1.12-alpine +``` + ### Filter the vulnerabilities by severities ``` diff --git a/internal/app.go b/internal/app.go index 75006fe34e..f219d8afa8 100644 --- a/internal/app.go +++ b/internal/app.go @@ -38,7 +38,7 @@ var ( Name: "format", Aliases: []string{"f"}, Value: "table", - Usage: "format (table, json, template)", + Usage: "format (table, json, sarif, template)", EnvVars: []string{"TRIVY_FORMAT"}, } diff --git a/pkg/report/writer.go b/pkg/report/writer.go index 74641ffa9c..2390dff822 100644 --- a/pkg/report/writer.go +++ b/pkg/report/writer.go @@ -4,9 +4,11 @@ import ( "bytes" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "io/ioutil" + "net/http" "os" "strings" "text/template" @@ -19,6 +21,10 @@ import ( "github.com/olekukonko/tablewriter" ) +var ( + DefaultSarifTemplateURL = "https://raw.githubusercontent.com/aquasecurity/trivy/master/contrib/sarif.tpl" +) + type Results []Result type Result struct { @@ -65,6 +71,12 @@ func WriteResults(format string, output io.Writer, results Results, outputTempla return xerrors.Errorf("error parsing template: %w", err) } writer = &TemplateWriter{Output: output, Template: tmpl} + case "sarif": + template, err := getSarifTemplate(DefaultSarifTemplateURL) + if err != nil { + return err + } + return WriteResults("template", output, results, template, light) default: return xerrors.Errorf("unknown format: %v", format) } @@ -75,6 +87,15 @@ func WriteResults(format string, output io.Writer, results Results, outputTempla return nil } +func getSarifTemplate(url string) (string, error) { + r, err := http.Get(url) + if err != nil { + return "", errors.New(fmt.Sprintf("error fetching template: %s", err)) + } + b, _ := ioutil.ReadAll(r.Body) + return string(b), nil +} + type Writer interface { Write(Results) error } diff --git a/pkg/report/writer_test.go b/pkg/report/writer_test.go index c8de01bed6..9bbc62b130 100644 --- a/pkg/report/writer_test.go +++ b/pkg/report/writer_test.go @@ -3,6 +3,9 @@ package report_test import ( "bytes" "encoding/json" + "io" + "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" @@ -193,7 +196,6 @@ func TestReportWriter_JSON(t *testing.T) { assert.Equal(t, tc.expectedJSON, writtenResults, tc.name) }) } - } func TestReportWriter_Template(t *testing.T) { @@ -313,3 +315,263 @@ func TestReportWriter_Template(t *testing.T) { }) } } + +func TestReportWriter_Sarif(t *testing.T) { + testCases := []struct { + name string + url string + expectedOutput string + expectedError string + }{ + { + name: "happy path", + expectedOutput: `{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json", + "version": "2.1.0", + "runs": [{ + "tool": { + "driver": { + "name": "Trivy", + "fullName": "Trivy Vulnerability Scanner", + "rules": [{ + "id": "[MEDIUM] CVE-2019-0000", + "name": "dockerfile_scan", + "shortDescription": { + "text": "CVE-2019-0000 Package: foo" + }, + "fullDescription": { + "text": "." + }, + "help": { + "text": "Vulnerability CVE-2019-0000\nSeverity: MEDIUM\nPackage: foo\nInstalled Version: 1.2.3\nFixed Version: 1.2.4\nLink: [CVE-2019-0000](https://aquasecurity.github.io/avd/nvd/cve-2019-0000)", + "markdown": "**Vulnerability CVE-2019-0000**\n| Severity | Package | Installed Version | Fixed Version | Link |\n| --- | --- | --- | --- | --- |\n|MEDIUM|foo|1.2.3|1.2.4|[CVE-2019-0000](https://aquasecurity.github.io/avd/nvd/cve-2019-0000)|\n" + }, + "properties": { + "tags": [ + "vulnerability", + "MEDIUM", + "foo" + ], + "precision": "very-high" + } + }, + { + "id": "[HIGH] CVE-2019-0001", + "name": "dockerfile_scan", + "shortDescription": { + "text": "CVE-2019-0001 Package: bar" + }, + "fullDescription": { + "text": "." + }, + "help": { + "text": "Vulnerability CVE-2019-0001\nSeverity: HIGH\nPackage: bar\nInstalled Version: 2.3.4\nFixed Version: 2.3.5\nLink: [CVE-2019-0001](https://aquasecurity.github.io/avd/nvd/cve-2019-0001)", + "markdown": "**Vulnerability CVE-2019-0001**\n| Severity | Package | Installed Version | Fixed Version | Link |\n| --- | --- | --- | --- | --- |\n|HIGH|bar|2.3.4|2.3.5|[CVE-2019-0001](https://aquasecurity.github.io/avd/nvd/cve-2019-0001)|\n" + }, + "properties": { + "tags": [ + "vulnerability", + "HIGH", + "bar" + ], + "precision": "very-high" + } + } + ] + } + }, + "results": [{ + "ruleId": "[MEDIUM] CVE-2019-0000", + "ruleIndex": 0, + "level": "error", + "message": { + "text": "without period." + }, + "locations": [{ + "physicalLocation": { + "artifactLocation": { + "uri": "Dockerfile" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endColumn": 1 + } + } + }] + }, + { + "ruleId": "[HIGH] CVE-2019-0001", + "ruleIndex": 1, + "level": "error", + "message": { + "text": "with period." + }, + "locations": [{ + "physicalLocation": { + "artifactLocation": { + "uri": "Dockerfile" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endColumn": 1 + } + } + }] + } + ], + "columnKind": "utf16CodeUnits" + }] +}`, + }, + { + name: "sad path, bad url", + url: "http://foo/bar/baz", + expectedError: "no such host", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + oldDefaultSarifTemplateURL := report.DefaultSarifTemplateURL + + if tc.url == "" { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, `{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Trivy", + "fullName": "Trivy Vulnerability Scanner", + "rules": [ + {{- $t_first := true }} + {{- range . }} + {{- range .Vulnerabilities -}} + {{- if $t_first -}} + {{- $t_first = false -}} + {{ else -}} + , + {{- end }} + { + "id": "[{{ .Vulnerability.Severity }}] {{ .VulnerabilityID }}", + "name": "dockerfile_scan", + "shortDescription": { + "text": "{{ .VulnerabilityID }} Package: {{ .PkgName }}" + }, + "fullDescription": { + "text": "{{ endWithPeriod .Title }}" + }, + "help": { + "text": "Vulnerability {{ .VulnerabilityID }}\nSeverity: {{ .Vulnerability.Severity }}\nPackage: {{ .PkgName }}\nInstalled Version: {{ .InstalledVersion }}\nFixed Version: {{ .FixedVersion }}\nLink: [{{ .VulnerabilityID }}](https://aquasecurity.github.io/avd/nvd/{{ .VulnerabilityID | toLower}})", + "markdown": "**Vulnerability {{ .VulnerabilityID }}**\n| Severity | Package | Installed Version | Fixed Version | Link |\n| --- | --- | --- | --- | --- |\n|{{ .Vulnerability.Severity }}|{{ .PkgName }}|{{ .InstalledVersion }}|{{ .FixedVersion }}|[{{ .VulnerabilityID }}](https://aquasecurity.github.io/avd/nvd/{{ .VulnerabilityID | toLower }})|\n" + }, + "properties": { + "tags": [ + "vulnerability", + "{{ .Vulnerability.Severity }}", + "{{ .PkgName }}" + ], + "precision": "very-high" + } + } + {{- end -}} + {{- end -}} + ] + } + }, + "results": [ + {{- $t_first := true }} + {{- range . }} + {{- range $index, $vulnerability := .Vulnerabilities -}} + {{- if $t_first -}} + {{- $t_first = false -}} + {{ else -}} + , + {{- end }} + { + "ruleId": "[{{ $vulnerability.Vulnerability.Severity }}] {{ $vulnerability.VulnerabilityID }}", + "ruleIndex": {{ $index }}, + "level": "error", + "message": { + "text": {{ endWithPeriod $vulnerability.Description | printf "%q" }} + }, + "locations": [{ + "physicalLocation": { + "artifactLocation": { + "uri": "Dockerfile" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endColumn": 1 + } + } + }] + } + {{- end -}} + {{- end -}} + ], + "columnKind": "utf16CodeUnits" + } + ] +}`) + })) + report.DefaultSarifTemplateURL = ts.URL + defer func() { + ts.Close() + report.DefaultSarifTemplateURL = oldDefaultSarifTemplateURL + }() + } else { + oldDefaultSarifTemplateURL := report.DefaultSarifTemplateURL + report.DefaultSarifTemplateURL = tc.url + defer func() { + report.DefaultSarifTemplateURL = oldDefaultSarifTemplateURL + }() + } + + tmplWritten := bytes.Buffer{} + inputResults := report.Results{ + { + Target: "sariftesttarget", + Type: "test", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "foo", + Vulnerability: dbTypes.Vulnerability{ + Severity: "MEDIUM", + Description: "without period", + }, + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "bar", + Vulnerability: dbTypes.Vulnerability{ + Severity: "HIGH", + Description: "with period.", + }, + InstalledVersion: "2.3.4", + FixedVersion: "2.3.5", + }, + }, + }, + } + + err := report.WriteResults("sarif", &tmplWritten, inputResults, "", false) + switch { + case tc.expectedError != "": + assert.Contains(t, err.Error(), tc.expectedError, tc.name) + assert.Empty(t, tmplWritten.String(), tc.name) + default: + assert.NoError(t, err, tc.name) + assert.JSONEq(t, tc.expectedOutput, tmplWritten.String(), tc.name) + } + }) + } +}