Skip to content

Commit

Permalink
Change root of JSON presenter to a mapping (instead of a sequence) (#163
Browse files Browse the repository at this point in the history
)

* update root of json presenter document

Signed-off-by: Alex Goodman <[email protected]>

* change vulnerabilities to matches in json output

Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman authored Sep 25, 2020
1 parent b2715ff commit 9f6301b
Show file tree
Hide file tree
Showing 10 changed files with 488 additions and 166 deletions.
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-containerregistry v0.0.0-20200430153450-5cbd060f5c92 h1:SR1vV7r+Vn/Ps59OMxxm8T2uQC4LBRsSMtZ4NODG9BA=
github.com/google/go-containerregistry v0.0.0-20200430153450-5cbd060f5c92/go.mod h1:pD1UFYs7MCAx+ZLShBdttcaOSbyc8F9Na/9IZLNwJeA=
github.com/google/go-containerregistry v0.1.0 h1:hL5mVw7cTX3SBr64Arpv+cJH93L+Z9Q6WjckImYLB3g=
github.com/google/go-containerregistry v0.1.0/go.mod h1:npTSyywOeILcgWqd+rvtzGWflIPPcBQhYoOONaY4ltM=
github.com/google/go-containerregistry v0.1.1 h1:AG8FSAfXglim2l5qSrqp5VK2Xl03PiBf25NiTGGamws=
github.com/google/go-containerregistry v0.1.1/go.mod h1:npTSyywOeILcgWqd+rvtzGWflIPPcBQhYoOONaY4ltM=
Expand Down Expand Up @@ -1020,6 +1021,7 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38=
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -1166,6 +1168,7 @@ google.golang.org/genproto v0.0.0-20200519141106-08726f379972 h1:6ydLqG65DIMNJf6
google.golang.org/genproto v0.0.0-20200519141106-08726f379972/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200604104852-0b0486081ffb h1:ek2py5bOqzR7MR/6obzk0rXUgYCLmjyLnaO9ssT+l6w=
google.golang.org/genproto v0.0.0-20200604104852-0b0486081ffb/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7 h1:1N7l1PuXZwEK7OhHdmKQROOM75PnUjABGwvVRbLBgFk=
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
Expand Down
78 changes: 78 additions & 0 deletions grype/presenter/json/document.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package json

import (
"fmt"

"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/pkg"
syftJson "github.com/anchore/syft/syft/presenter/json"
"github.com/anchore/syft/syft/scope"
)

// Document represents the JSON document to be presented
type Document struct {
Matches []Match `json:"matches"`
Image *syftJson.Image `json:"image,omitempty"`
Directory *string `json:"directory,omitempty"`
}

// Match is a single item for the JSON array reported
type Match struct {
Vulnerability Vulnerability `json:"vulnerability"`
MatchDetails MatchDetails `json:"matchDetails"`
Artifact syftJson.Artifact `json:"artifact"`
}

// MatchDetails contains all data that indicates how the result match was found
type MatchDetails struct {
Matcher string `json:"matcher"`
SearchKey map[string]interface{} `json:"searchKey"`
MatchInfo map[string]interface{} `json:"matchedOn"`
}

// NewDocument creates and populates a new Document struct, representing the populated JSON document.
func NewDocument(catalog *pkg.Catalog, s scope.Scope, matches match.Matches, metadataProvider vulnerability.MetadataProvider) (Document, error) {
doc := Document{}

srcObj := s.Source()
switch src := srcObj.(type) {
case scope.ImageSource:
doc.Image = syftJson.NewImage(src)
case scope.DirSource:
doc.Directory = &s.DirSrc.Path
default:
return Document{}, fmt.Errorf("unsupported source: %T", src)
}

// we must preallocate the findings to ensure the JSON document does not show "null" when no matches are found
var findings = make([]Match, 0)
for m := range matches.Enumerate() {
p := catalog.Package(m.Package.ID())
art, err := syftJson.NewArtifact(p, s)
if err != nil {
return Document{}, err
}

metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.RecordSource)
if err != nil {
return Document{}, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
}

findings = append(
findings,
Match{
Vulnerability: NewVulnerability(m, metadata),
Artifact: art,
MatchDetails: MatchDetails{
Matcher: m.Matcher.String(),
SearchKey: m.SearchKey,
MatchInfo: m.SearchMatches,
},
},
)
}
doc.Matches = findings

return doc, nil
}
52 changes: 6 additions & 46 deletions grype/presenter/json/presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,38 @@ package json

import (
"encoding/json"
"fmt"
"io"

"github.com/anchore/grype/grype/match"

"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/pkg"
syftJson "github.com/anchore/syft/syft/presenter/json"
"github.com/anchore/syft/syft/scope"
)

// Presenter is a generic struct for holding fields needed for reporting
type Presenter struct {
results match.Matches
matches match.Matches
catalog *pkg.Catalog
scope scope.Scope
metadataProvider vulnerability.MetadataProvider
}

// NewPresenter is a *Presenter constructor
func NewPresenter(results match.Matches, catalog *pkg.Catalog, theScope scope.Scope, metadataProvider vulnerability.MetadataProvider) *Presenter {
func NewPresenter(matches match.Matches, catalog *pkg.Catalog, theScope scope.Scope, metadataProvider vulnerability.MetadataProvider) *Presenter {
return &Presenter{
results: results,
matches: matches,
catalog: catalog,
metadataProvider: metadataProvider,
scope: theScope,
}
}

// Finding is a single item for the JSON array reported
type Finding struct {
Vulnerability Vulnerability `json:"vulnerability"`
MatchDetails MatchDetails `json:"matchDetails"`
Artifact syftJson.Artifact `json:"artifact"`
}

// MatchDetails contains all data that indicates how the result match was found
type MatchDetails struct {
Matcher string `json:"matcher"`
SearchKey map[string]interface{} `json:"searchKey"`
MatchInfo map[string]interface{} `json:"matchedOn"`
}

// Present creates a JSON-based reporting
func (pres *Presenter) Present(output io.Writer) error {
doc := make([]Finding, 0)

for m := range pres.results.Enumerate() {
p := pres.catalog.Package(m.Package.ID())

art, err := syftJson.NewArtifact(p, pres.scope)
if err != nil {
return err
}

metadata, err := pres.metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.RecordSource)
if err != nil {
return fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
}

doc = append(
doc,
Finding{
Vulnerability: NewVulnerability(m, metadata),
Artifact: art,
MatchDetails: MatchDetails{
Matcher: m.Matcher.String(),
SearchKey: m.SearchKey,
MatchInfo: m.SearchMatches,
},
},
)
doc, err := NewDocument(pres.catalog, pres.scope, pres.matches, pres.metadataProvider)
if err != nil {
return err
}

enc := json.NewEncoder(output)
Expand Down
139 changes: 133 additions & 6 deletions grype/presenter/json/presenter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (m *metadataMock) GetMetadata(id, recordSource string) (*vulnerability.Meta
return &value, nil
}

func TestJsonPresenter(t *testing.T) {
func TestJsonImgsPresenter(t *testing.T) {
var buffer bytes.Buffer
var testImage = "image-simple"

Expand All @@ -70,7 +70,7 @@ func TestJsonPresenter(t *testing.T) {

var pkg1 = pkg.Package{
Name: "package-1",
Version: "1.0.1",
Version: "1.1.1",
Type: pkg.DebPkg,
Source: []file.Reference{
*img.SquashedTree().File("/somefile-1.txt"),
Expand All @@ -80,7 +80,7 @@ func TestJsonPresenter(t *testing.T) {

var pkg2 = pkg.Package{
Name: "package-2",
Version: "2.0.1",
Version: "2.2.2",
Type: pkg.DebPkg,
Source: []file.Reference{
*img.SquashedTree().File("/somefile-2.txt"),
Expand Down Expand Up @@ -149,6 +149,9 @@ func TestJsonPresenter(t *testing.T) {
catalog.Add(pkg2)

theScope, err := scope.NewScopeFromImage(img, scope.AllLayersScope)
if err != nil {
t.Fatalf("failed to create scope: %+v", err)
}

pres := NewPresenter(matches, catalog, theScope, newMetadataMock())

Expand All @@ -175,19 +178,143 @@ func TestJsonPresenter(t *testing.T) {
// validateAgainstV1Schema(t, string(actual))
}

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

catalog := pkg.NewCatalog()

// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-1",
Source: []file.Reference{
{Path: "/some/path/pkg1"},
},
})

var pkg1 *pkg.Package

// we need a package with an ID from the catalog (we should fix this)
// TODO: fix this
for p := range catalog.Enumerate() {
pkg1 = p
break
}

var match1 = match.Match{
Type: match.ExactDirectMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-1999-0001",
RecordSource: "source-1",
FixedInVersion: "the-next-version",
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchKey: map[string]interface{}{
"distro": map[string]string{
"type": "ubuntu",
"version": "20.04",
},
},
SearchMatches: map[string]interface{}{
"constraint": ">= 20",
},
}

var match2 = match.Match{
Type: match.ExactIndirectMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-1999-0002",
RecordSource: "source-2",
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchKey: map[string]interface{}{
"cpe": "somecpe",
},
SearchMatches: map[string]interface{}{
"constraint": "somecpe",
},
}

var match3 = match.Match{
Type: match.ExactIndirectMatch,
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-1999-0003",
RecordSource: "source-1",
FixedInVersion: "the-other-next-version",
},
Package: pkg1,
Matcher: match.DpkgMatcher,
SearchKey: map[string]interface{}{
"language": "java",
},
SearchMatches: map[string]interface{}{
"constraint": "< 2.0.0",
},
}

matches := match.NewMatches()
matches.Add(pkg1, match1, match2, match3)

s, err := scope.NewScopeFromDir("/some/path")
if err != nil {
t.Fatal(err)
}

pres := NewPresenter(matches, catalog, s, newMetadataMock())

// TODO: add a constructor for a match.Match when the data is better shaped

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

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

var expected = testutils.GetGoldenFileContents(t)

if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}

// TODO: add me back in when there is a JSON schema
// validateAgainstV1Schema(t, string(actual))
}

func TestEmptyJsonPresenter(t *testing.T) {
// Expected to have an empty JSON array back
var buffer bytes.Buffer

var testImage = "image-simple"

if *update {
testutils.UpdateGoldenFixtureImage(t, testImage)
}

img := testutils.GetGoldenFixtureImage(t, testImage)

matches := match.NewMatches()

catalog := pkg.NewCatalog()

pres := NewPresenter(matches, catalog, scope.Scope{}, nil)
theScope, err := scope.NewScopeFromImage(img, scope.AllLayersScope)
if err != nil {
t.Fatalf("failed to create scope: %+v", err)
}

pres := NewPresenter(matches, catalog, theScope, nil)

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

0 comments on commit 9f6301b

Please sign in to comment.