BREAKING: migrate to a new JSON schema (#782)

* feat: introduce a new JSON schema

* test: update

* chore(mod): update fanal

* refactor: add a comment

* test(report): fix

* refactor(writer): add omitempty

* refactor: replace url

* test(scanner): fix
This commit is contained in:
Teppei Fukuda
2021-06-08 18:03:24 +03:00
committed by GitHub
parent 097b8d4881
commit e362843705
16 changed files with 171 additions and 85 deletions

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/aquasecurity/trivy
go 1.16
require (
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986

4
go.sum
View File

@@ -103,8 +103,8 @@ github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0/go.mod h1:BB1eHdMLYE
github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14=
github.com/KeisukeYamashita/go-vcl v0.4.0/go.mod h1:af2qGlXbsHDQN5abN7hyGNKtGhcFSaDdbLl4sfud+AU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=

View File

@@ -14,7 +14,7 @@ import (
"github.com/aquasecurity/trivy-db/pkg/db"
"github.com/aquasecurity/trivy/pkg/commands/operation"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report"
pkgReport "github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/utils"
@@ -62,21 +62,21 @@ func runWithTimeout(ctx context.Context, opt Option, initializeScanner Initializ
defer db.Close()
}
results, err := scan(ctx, opt, initializeScanner, cacheClient)
report, err := scan(ctx, opt, initializeScanner, cacheClient)
if err != nil {
return xerrors.Errorf("scan error: %w", err)
}
results, err = filter(ctx, opt, results)
report, err = filter(ctx, opt, report)
if err != nil {
return xerrors.Errorf("filter error: %w", err)
}
if err = report.WriteResults(opt.Format, opt.Output, opt.Severities, results, opt.Template, opt.Light); err != nil {
if err = pkgReport.Write(opt.Format, opt.Output, opt.Severities, report, opt.Template, opt.Light); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
exit(opt, results)
exit(opt, report.Results)
return nil
}
@@ -124,7 +124,7 @@ func initDB(c Option) error {
}
func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner, cacheClient cache.Cache) (
report.Results, error) {
pkgReport.Report, error) {
target := opt.Target
if opt.Input != "" {
target = opt.Input
@@ -154,32 +154,33 @@ func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner,
s, cleanup, err := initializeScanner(ctx, target, cacheClient, cacheClient, opt.Timeout,
disabledAnalyzers, configScannerOptions)
if err != nil {
return nil, xerrors.Errorf("unable to initialize a scanner: %w", err)
return pkgReport.Report{}, xerrors.Errorf("unable to initialize a scanner: %w", err)
}
defer cleanup()
results, err := s.ScanArtifact(ctx, scanOptions)
report, err := s.ScanArtifact(ctx, scanOptions)
if err != nil {
return nil, xerrors.Errorf("image scan failed: %w", err)
return pkgReport.Report{}, xerrors.Errorf("image scan failed: %w", err)
}
return results, nil
return report, nil
}
func filter(ctx context.Context, opt Option, results report.Results) (report.Results, error) {
func filter(ctx context.Context, opt Option, report pkgReport.Report) (pkgReport.Report, error) {
resultClient := initializeResultClient()
results := report.Results
for i := range results {
resultClient.FillInfo(results[i].Vulnerabilities, results[i].Type)
vulns, err := resultClient.Filter(ctx, results[i].Vulnerabilities,
opt.Severities, opt.IgnoreUnfixed, opt.IgnoreFile, opt.IgnorePolicy)
if err != nil {
return nil, xerrors.Errorf("unable to filter vulnerabilities: %w", err)
return pkgReport.Report{}, xerrors.Errorf("unable to filter vulnerabilities: %w", err)
}
results[i].Vulnerabilities = vulns
}
return results, nil
return report, nil
}
func exit(c Option, results report.Results) {
func exit(c Option, results pkgReport.Results) {
if c.ExitCode != 0 && results.Failed() {
os.Exit(c.ExitCode)
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/aquasecurity/fanal/analyzer/config"
"github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/report"
pkgReport "github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/rpc/client"
"github.com/aquasecurity/trivy/pkg/scanner"
"github.com/aquasecurity/trivy/pkg/types"
@@ -59,12 +59,13 @@ func runWithTimeout(ctx context.Context, opt Option) error {
}
log.Logger.Debugf("Vulnerability type: %s", scanOptions.VulnType)
results, err := s.ScanArtifact(ctx, scanOptions)
report, err := s.ScanArtifact(ctx, scanOptions)
if err != nil {
return xerrors.Errorf("error in image scan: %w", err)
}
resultClient := initializeResultClient()
results := report.Results
for i := range results {
vulns, err := resultClient.Filter(ctx, results[i].Vulnerabilities,
opt.Severities, opt.IgnoreUnfixed, opt.IgnoreFile, opt.IgnorePolicy)
@@ -74,7 +75,7 @@ func runWithTimeout(ctx context.Context, opt Option) error {
results[i].Vulnerabilities = vulns
}
if err = report.WriteResults(opt.Format, opt.Output, opt.Severities, results, opt.Template, false); err != nil {
if err = pkgReport.Write(opt.Format, opt.Output, opt.Severities, report, opt.Template, false); err != nil {
return xerrors.Errorf("unable to write results: %w", err)
}
@@ -135,7 +136,7 @@ func initializeScanner(ctx context.Context, opt Option) (scanner.Scanner, func()
return s, cleanup, nil
}
func exit(c Option, results report.Results) {
func exit(c Option, results pkgReport.Results) {
if c.ExitCode != 0 {
for _, result := range results {
if len(result.Vulnerabilities) > 0 {

View File

@@ -4,8 +4,11 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/log"
)
// JSONWriter implements result Writer
@@ -14,8 +17,16 @@ type JSONWriter struct {
}
// Write writes the results in JSON format
func (jw JSONWriter) Write(results Results) error {
output, err := json.MarshalIndent(results, "", " ")
func (jw JSONWriter) Write(report Report) error {
var v interface{} = report
if os.Getenv("TRIVY_NEW_JSON_SCHEMA") == "" {
// After migrating to the new JSON schema, TRIVY_NEW_JSON_SCHEMA will be removed.
log.Logger.Warnf("DEPRECATED: the current JSON schema is deprecated, check %s for more information.",
"https://github.com/aquasecurity/trivy/discussions/1050")
v = report.Results
}
output, err := json.MarshalIndent(v, "", " ")
if err != nil {
return xerrors.Errorf("failed to marshal json: %w", err)
}

View File

@@ -62,14 +62,16 @@ func TestReportWriter_JSON(t *testing.T) {
jsonWritten := bytes.Buffer{}
jw.Output = &jsonWritten
inputResults := report.Results{
{
Target: "foojson",
Vulnerabilities: tc.detectedVulns,
inputResults := report.Report{
Results: report.Results{
{
Target: "foojson",
Vulnerabilities: tc.detectedVulns,
},
},
}
err := report.WriteResults("json", &jsonWritten, nil, inputResults, "", false)
err := report.Write("json", &jsonWritten, nil, inputResults, "", false)
assert.NoError(t, err)
writtenResults := report.Results{}

View File

@@ -22,8 +22,8 @@ type TableWriter struct {
}
// Write writes the result on standard output
func (tw TableWriter) Write(results Results) error {
for _, result := range results {
func (tw TableWriter) Write(report Report) error {
for _, result := range report.Results {
// Skip zero vulnerabilities on Java archives (JAR/WAR/EAR)
if result.Type == ftypes.Jar && len(result.Vulnerabilities) == 0 {
continue

View File

@@ -138,7 +138,7 @@ func TestReportWriter_Table(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tableWritten := bytes.Buffer{}
assert.NoError(t, report.WriteResults("table", &tableWritten, nil, tc.results, "", tc.light), tc.name)
assert.NoError(t, report.Write("table", &tableWritten, nil, report.Report{Results: tc.results}, "", tc.light), tc.name)
assert.Equal(t, tc.expectedOutput, tableWritten.String(), tc.name)
})
}

View File

@@ -13,10 +13,10 @@ import (
"text/template"
"time"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
"github.com/Masterminds/sprig"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
)
// regex to extract file path in case string includes (distro:version)
@@ -83,8 +83,8 @@ func NewTemplateWriter(output io.Writer, outputTemplate string) (*TemplateWriter
}
// Write writes result
func (tw TemplateWriter) Write(results Results) error {
err := tw.Template.Execute(tw.Output, results)
func (tw TemplateWriter) Write(report Report) error {
err := tw.Template.Execute(tw.Output, report.Results)
if err != nil {
return xerrors.Errorf("failed to write with template: %w", err)
}

View File

@@ -166,15 +166,17 @@ func TestReportWriter_Template(t *testing.T) {
}
os.Setenv("AWS_ACCOUNT_ID", "123456789012")
tmplWritten := bytes.Buffer{}
inputResults := report.Results{
{
Target: "foojunit",
Type: "test",
Vulnerabilities: tc.detectedVulns,
inputReport := report.Report{
Results: report.Results{
{
Target: "foojunit",
Type: "test",
Vulnerabilities: tc.detectedVulns,
},
},
}
assert.NoError(t, report.WriteResults("template", &tmplWritten, nil, inputResults, tc.template, false))
assert.NoError(t, report.Write("template", &tmplWritten, nil, inputReport, tc.template, false))
assert.Equal(t, tc.expected, tmplWritten.String())
})
}
@@ -197,14 +199,16 @@ func TestReportWriter_Template_SARIF(t *testing.T) {
template, err := ioutil.ReadFile(templateFile)
require.NoError(t, err, tc.name)
inputResults := report.Results{
report.Result{
Target: tc.target,
Type: "footype",
Vulnerabilities: tc.detectedVulns,
inputReport := report.Report{
Results: report.Results{
{
Target: tc.target,
Type: "footype",
Vulnerabilities: tc.detectedVulns,
},
},
}
assert.NoError(t, report.WriteResults("template", &got, nil, inputResults, string(template), false), tc.name)
assert.NoError(t, report.Write("template", &got, nil, inputReport, string(template), false), tc.name)
assert.JSONEq(t, tc.want, got.String(), tc.name)
})
}

27
pkg/report/testdata/new.json.golden vendored Normal file
View File

@@ -0,0 +1,27 @@
{
"ImageID": "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e",
"RepoTags": [
"test:latest"
],
"RepoDigests": [
"test@sha256:0bd0e9e03a022c3b0226667621da84fc9bf562a9056130424b5bfbd8bcb0397f"
],
"Results": [
{
"Target": "foojson",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2020-0001",
"PkgName": "foo",
"InstalledVersion": "1.2.3",
"FixedVersion": "3.4.5",
"Layer": {},
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-0001",
"Title": "foobar",
"Description": "baz",
"Severity": "HIGH"
}
]
}
]
}

18
pkg/report/testdata/old.json.golden vendored Normal file
View File

@@ -0,0 +1,18 @@
[
{
"Target": "foojson",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2020-0001",
"PkgName": "foo",
"InstalledVersion": "1.2.3",
"FixedVersion": "3.4.5",
"Layer": {},
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-0001",
"Title": "foobar",
"Description": "baz",
"Severity": "HIGH"
}
]
}
]

View File

@@ -14,10 +14,18 @@ import (
// Now returns the current time
var Now = time.Now
// Report represents a scan result
type Report struct {
ArtifactID string `json:",omitempty"`
RepoTags []string `json:",omitempty"`
RepoDigests []string `json:",omitempty"`
Results Results `json:",omitempty"`
}
// Results to hold list of Result
type Results []Result
// Result to hold image scan results
// Result holds a target and detected vulnerabilities
type Result struct {
Target string `json:"Target"`
Type string `json:"Type,omitempty"`
@@ -35,8 +43,9 @@ func (results Results) Failed() bool {
return false
}
// WriteResults writes the result to output, format as passed in argument
func WriteResults(format string, output io.Writer, severities []dbTypes.Severity, results Results, outputTemplate string, light bool) error {
// Write writes the result to output, format as passed in argument
func Write(format string, output io.Writer, severities []dbTypes.Severity, report Report,
outputTemplate string, light bool) error {
var writer Writer
switch format {
case "table":
@@ -52,7 +61,7 @@ func WriteResults(format string, output io.Writer, severities []dbTypes.Severity
return xerrors.Errorf("unknown format: %v", format)
}
if err := writer.Write(results); err != nil {
if err := writer.Write(report); err != nil {
return xerrors.Errorf("failed to write results: %w", err)
}
return nil
@@ -60,5 +69,5 @@ func WriteResults(format string, output io.Writer, severities []dbTypes.Severity
// Writer defines the result write operation
type Writer interface {
Write(Results) error
Write(Report) error
}

View File

@@ -3,9 +3,10 @@ package report_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/aquasecurity/trivy/pkg/report"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/stretchr/testify/assert"
)
func TestResults_Failed(t *testing.T) {

View File

@@ -92,20 +92,25 @@ func NewScanner(driver Driver, ar artifact.Artifact) Scanner {
}
// ScanArtifact scans the artifacts and returns results
func (s Scanner) ScanArtifact(ctx context.Context, options types.ScanOptions) (report.Results, error) {
func (s Scanner) ScanArtifact(ctx context.Context, options types.ScanOptions) (report.Report, error) {
artifactInfo, err := s.artifact.Inspect(ctx)
if err != nil {
return nil, xerrors.Errorf("failed analysis: %w", err)
return report.Report{}, xerrors.Errorf("failed analysis: %w", err)
}
results, osFound, eosl, err := s.driver.Scan(artifactInfo.Name, artifactInfo.ID, artifactInfo.BlobIDs, options)
if err != nil {
return nil, xerrors.Errorf("scan failed: %w", err)
return report.Report{}, xerrors.Errorf("scan failed: %w", err)
}
if eosl {
log.Logger.Warnf("This OS version is no longer supported by the distribution: %s %s", osFound.Family, osFound.Name)
log.Logger.Warnf("The vulnerability detection may be insufficient because security updates are not provided")
}
return results, nil
return report.Report{
ArtifactID: artifactInfo.ID,
RepoTags: artifactInfo.RepoTags,
RepoDigests: artifactInfo.RepoDigests,
Results: results,
}, nil
}

View File

@@ -23,7 +23,7 @@ func TestScanner_ScanArtifact(t *testing.T) {
args args
inspectExpectation artifact.ArtifactInspectExpectation
scanExpectation DriverScanExpectation
wantResults report.Results
want report.Report
wantErr string
}{
{
@@ -37,9 +37,11 @@ func TestScanner_ScanArtifact(t *testing.T) {
},
Returns: artifact.ArtifactInspectReturns{
Reference: ftypes.ArtifactReference{
Name: "alpine:3.11",
ID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
BlobIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
Name: "alpine:3.11",
ID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
BlobIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"},
RepoTags: []string{"alpine:3.11"},
RepoDigests: []string{"alpine@sha256:0bd0e9e03a022c3b0226667621da84fc9bf562a9056130424b5bfbd8bcb0397f"},
},
},
},
@@ -87,33 +89,38 @@ func TestScanner_ScanArtifact(t *testing.T) {
Eols: true,
},
},
wantResults: report.Results{
{
Target: "alpine:3.11",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-9999",
PkgName: "vim",
InstalledVersion: "1.2.3",
FixedVersion: "1.2.4",
Layer: ftypes.Layer{
Digest: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10",
DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079",
want: report.Report{
ArtifactID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a",
RepoTags: []string{"alpine:3.11"},
RepoDigests: []string{"alpine@sha256:0bd0e9e03a022c3b0226667621da84fc9bf562a9056130424b5bfbd8bcb0397f"},
Results: report.Results{
{
Target: "alpine:3.11",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-9999",
PkgName: "vim",
InstalledVersion: "1.2.3",
FixedVersion: "1.2.4",
Layer: ftypes.Layer{
Digest: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10",
DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079",
},
},
},
},
},
{
Target: "node-app/package-lock.json",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-11358",
PkgName: "jquery",
InstalledVersion: "3.3.9",
FixedVersion: ">=3.4.0",
{
Target: "node-app/package-lock.json",
Vulnerabilities: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2019-11358",
PkgName: "jquery",
InstalledVersion: "3.3.9",
FixedVersion: ">=3.4.0",
},
},
Type: "npm",
},
Type: "npm",
},
},
},
@@ -172,7 +179,7 @@ func TestScanner_ScanArtifact(t *testing.T) {
mockArtifact.ApplyInspectExpectation(tt.inspectExpectation)
s := NewScanner(d, mockArtifact)
gotResults, err := s.ScanArtifact(context.Background(), tt.args.options)
got, err := s.ScanArtifact(context.Background(), tt.args.options)
if tt.wantErr != "" {
require.NotNil(t, err, tt.name)
require.Contains(t, err.Error(), tt.wantErr, tt.name)
@@ -181,7 +188,7 @@ func TestScanner_ScanArtifact(t *testing.T) {
require.NoError(t, err, tt.name)
}
assert.Equal(t, tt.wantResults, gotResults, tt.name)
assert.Equal(t, tt.want, got, tt.name)
})
}
}