Skip to content

Commit

Permalink
feat(outputs): allow to set multiple outputs (anchore#648)
Browse files Browse the repository at this point in the history
review

Signed-off-by: Olivier Boudet <[email protected]>
  • Loading branch information
olivierboudet committed Jul 3, 2023
1 parent 645def2 commit 21e25b9
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 82 deletions.
69 changes: 0 additions & 69 deletions cmd/writer.go

This file was deleted.

2 changes: 2 additions & 0 deletions grype/presenter/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ var AvailableFormats = []id{
templateFormat,
}

var DefaultFormat = tableFormat

// DeprecatedFormats TODO: remove in v1.0
var DeprecatedFormats = []id{
embeddedVEXJSON,
Expand Down
7 changes: 5 additions & 2 deletions grype/presenter/json/presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/file"
"github.com/anchore/grype/internal/log"
"github.com/spf13/afero"
)

// Presenter is a generic struct for holding fields needed for reporting
Expand All @@ -22,10 +23,11 @@ type Presenter struct {
appConfig interface{}
dbStatus interface{}
outputFilePath string
fs afero.Fs
}

// NewPresenter creates a new JSON presenter
func NewPresenter(pb models.PresenterConfig, outputFilePath string) *Presenter {
func NewPresenter(fs afero.Fs, pb models.PresenterConfig, outputFilePath string) *Presenter {
return &Presenter{
matches: pb.Matches,
ignoredMatches: pb.IgnoredMatches,
Expand All @@ -35,12 +37,13 @@ func NewPresenter(pb models.PresenterConfig, outputFilePath string) *Presenter {
appConfig: pb.AppConfig,
dbStatus: pb.DBStatus,
outputFilePath: outputFilePath,
fs: fs,
}
}

// Present creates a JSON-based reporting
func (pres *Presenter) Present(defaultOutput io.Writer) error {
output, closer, err := file.GetWriter(defaultOutput, pres.outputFilePath)
output, closer, err := file.GetWriter(pres.fs, defaultOutput, pres.outputFilePath)
defer func() {
if closer != nil {
err := closer()
Expand Down
48 changes: 45 additions & 3 deletions grype/presenter/json/presenter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"regexp"
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"

"github.com/anchore/go-testutils"
Expand All @@ -30,7 +31,7 @@ func TestJsonImgsPresenter(t *testing.T) {
MetadataProvider: metadataProvider,
}

pres := NewPresenter(pb, "")
pres := NewPresenter(afero.NewMemMapFs(), pb, "")

// run presenter
if err := pres.Present(&buffer); err != nil {
Expand Down Expand Up @@ -63,7 +64,7 @@ func TestJsonDirsPresenter(t *testing.T) {
MetadataProvider: metadataProvider,
}

pres := NewPresenter(pb, "")
pres := NewPresenter(afero.NewMemMapFs(), pb, "")

// run presenter
if err := pres.Present(&buffer); err != nil {
Expand Down Expand Up @@ -106,7 +107,7 @@ func TestEmptyJsonPresenter(t *testing.T) {
MetadataProvider: nil,
}

pres := NewPresenter(pb, "")
pres := NewPresenter(afero.NewMemMapFs(), pb, "")

// run presenter
if err := pres.Present(&buffer); err != nil {
Expand All @@ -128,3 +129,44 @@ func TestEmptyJsonPresenter(t *testing.T) {
func redact(content []byte) []byte {
return timestampRegexp.ReplaceAll(content, []byte(`"timestamp":""`))
}

func TestJsonPresentWithOutputFile(t *testing.T) {
var buffer bytes.Buffer

matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme)

pb := models.PresenterConfig{
Matches: matches,
Packages: packages,
Context: context,
MetadataProvider: metadataProvider,
}

outputFilePath := "/tmp/report.test.txt"
fs := afero.NewMemMapFs()
pres := NewPresenter(fs, pb, outputFilePath)

// run presenter
if err := pres.Present(&buffer); err != nil {
t.Fatal(err)
}

f, err := fs.Open(outputFilePath)
if err != nil {
t.Fatalf("no output file: %+v", err)
}

outputContent, err := afero.ReadAll(f)
if err != nil {
t.Fatalf("could not file: %+v", err)
}
outputContent = redact(outputContent)

if *update {
testutils.UpdateGoldenFileContents(t, outputContent)
}

var expected = testutils.GetGoldenFileContents(t)

assert.Equal(t, string(expected), string(outputContent))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
{
"matches": [
{
"vulnerability": {
"id": "CVE-1999-0001",
"dataSource": "",
"severity": "Low",
"urls": [],
"description": "1999-01 description",
"cvss": [
{
"version": "3.0",
"vector": "another vector",
"metrics": {
"baseScore": 4
},
"vendorMetadata": {}
}
],
"fix": {
"versions": [
"the-next-version"
],
"state": "fixed"
},
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": [
{
"type": "exact-direct-match",
"matcher": "dpkg-matcher",
"searchedBy": {
"distro": {
"type": "ubuntu",
"version": "20.04"
}
},
"found": {
"constraint": ">= 20"
}
}
],
"artifact": {
"id": "96699b00fe3004b4",
"name": "package-1",
"version": "1.1.1",
"type": "rpm",
"locations": [
{
"path": "/foo/bar/somefile-1.txt"
}
],
"language": "",
"licenses": [],
"cpes": [
"cpe:2.3:a:anchore:engine:0.9.2:*:*:python:*:*:*:*"
],
"purl": "",
"upstreams": [
{
"name": "nothing",
"version": "3.2"
}
],
"metadataType": "RpmMetadata",
"metadata": {
"epoch": 2,
"modularityLabel": ""
}
}
},
{
"vulnerability": {
"id": "CVE-1999-0002",
"dataSource": "",
"severity": "Critical",
"urls": [],
"description": "1999-02 description",
"cvss": [
{
"version": "2.0",
"vector": "vector",
"metrics": {
"baseScore": 1,
"exploitabilityScore": 2,
"impactScore": 3
},
"vendorMetadata": {
"BaseSeverity": "Low",
"Status": "verified"
}
}
],
"fix": {
"versions": [],
"state": ""
},
"advisories": []
},
"relatedVulnerabilities": [],
"matchDetails": [
{
"type": "exact-indirect-match",
"matcher": "dpkg-matcher",
"searchedBy": {
"cpe": "somecpe"
},
"found": {
"constraint": "somecpe"
}
}
],
"artifact": {
"id": "b4013a965511376c",
"name": "package-2",
"version": "2.2.2",
"type": "deb",
"locations": [
{
"path": "/foo/bar/somefile-2.txt"
}
],
"language": "",
"licenses": [
"MIT",
"Apache-2.0"
],
"cpes": [
"cpe:2.3:a:anchore:engine:2.2.2:*:*:python:*:*:*:*"
],
"purl": "",
"upstreams": []
}
}
],
"source": {
"type": "directory",
"target": "/some/path"
},
"distro": {
"name": "centos",
"version": "8.0",
"idLike": [
"centos"
]
},
"descriptor": {
"name": "grype",
"version": "[not provided]",
"timestamp":""
}
}
6 changes: 4 additions & 2 deletions grype/presenter/presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/anchore/grype/grype/presenter/table"
"github.com/anchore/grype/grype/presenter/template"
"github.com/anchore/grype/internal/log"
"github.com/spf13/afero"
)

// Presenter is the main interface other Presenters need to implement
Expand All @@ -20,10 +21,11 @@ type Presenter interface {
// GetPresenter retrieves a Presenter that matches a CLI option
// TODO dependency cycle with presenter package to sub formats
func GetPresenters(c Config, pb models.PresenterConfig) (presenters []Presenter) {
fs := afero.NewOsFs()
for _, f := range c.formats {
switch f.id {
case jsonFormat:
presenters = append(presenters, json.NewPresenter(pb, f.outputFilePath))
presenters = append(presenters, json.NewPresenter(fs, pb, f.outputFilePath))
case tableFormat:
presenters = append(presenters, table.NewPresenter(pb, c.showSuppressed))

Expand All @@ -39,7 +41,7 @@ func GetPresenters(c Config, pb models.PresenterConfig) (presenters []Presenter)
case sarifFormat:
presenters = append(presenters, sarif.NewPresenter(pb))
case templateFormat:
presenters = append(presenters, template.NewPresenter(pb, f.outputFilePath, c.templateFilePath))
presenters = append(presenters, template.NewPresenter(fs, pb, f.outputFilePath, c.templateFilePath))
// DEPRECATED TODO: remove in v1.0
case embeddedVEXJSON:
log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0")
Expand Down
Loading

0 comments on commit 21e25b9

Please sign in to comment.