mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
sarif: Make sarif easier to use with a default template
This will help us use Trivy in places like GitHub Actions where we cannot specify a template as input. $ trivy image --format=sarif alpine:3.10.1 Signed-off-by: Simarpreet Singh <simar@linux.com>
This commit is contained in:
@@ -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
|
$ 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
|
### Filter the vulnerabilities by severities
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ var (
|
|||||||
Name: "format",
|
Name: "format",
|
||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Value: "table",
|
Value: "table",
|
||||||
Usage: "format (table, json, template)",
|
Usage: "format (table, json, sarif, template)",
|
||||||
EnvVars: []string{"TRIVY_FORMAT"},
|
EnvVars: []string{"TRIVY_FORMAT"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
@@ -19,6 +21,10 @@ import (
|
|||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultSarifTemplateURL = "https://raw.githubusercontent.com/aquasecurity/trivy/master/contrib/sarif.tpl"
|
||||||
|
)
|
||||||
|
|
||||||
type Results []Result
|
type Results []Result
|
||||||
|
|
||||||
type Result struct {
|
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)
|
return xerrors.Errorf("error parsing template: %w", err)
|
||||||
}
|
}
|
||||||
writer = &TemplateWriter{Output: output, Template: tmpl}
|
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:
|
default:
|
||||||
return xerrors.Errorf("unknown format: %v", format)
|
return xerrors.Errorf("unknown format: %v", format)
|
||||||
}
|
}
|
||||||
@@ -75,6 +87,15 @@ func WriteResults(format string, output io.Writer, results Results, outputTempla
|
|||||||
return nil
|
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 {
|
type Writer interface {
|
||||||
Write(Results) error
|
Write(Results) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package report_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -193,7 +196,6 @@ func TestReportWriter_JSON(t *testing.T) {
|
|||||||
assert.Equal(t, tc.expectedJSON, writtenResults, tc.name)
|
assert.Equal(t, tc.expectedJSON, writtenResults, tc.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReportWriter_Template(t *testing.T) {
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user