mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
add initial support for embedded CycloneDX VEX documents (#678)
This commit is contained in:
parent
523f5ce9c0
commit
9f70cdbf24
24 changed files with 7807 additions and 2 deletions
7
Makefile
7
Makefile
|
@ -74,7 +74,7 @@ all: clean static-analysis test ## Run all checks (linting, license check, unit,
|
|||
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
|
||||
|
||||
.PHONY: test
|
||||
test: unit validate-cyclonedx-schema integration cli ## Run all tests (unit, integration, linux acceptance, and CLI tests)
|
||||
test: unit validate-cyclonedx-schema validate-cyclonedx-vex-schema integration cli ## Run all tests (unit, integration, linux acceptance, and CLI tests)
|
||||
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
|
||||
|
@ -96,6 +96,7 @@ bootstrap-tools: $(TEMPDIR)
|
|||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
|
||||
# the only difference between goimports and gosimports is that gosimports removes extra whitespace between import blocks (see https://github.com/golang/go/issues/20818)
|
||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@v0.1.5
|
||||
GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@v1.4.0
|
||||
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v1.4.1
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
|
@ -143,6 +144,10 @@ check-go-mod-tidy:
|
|||
validate-cyclonedx-schema:
|
||||
cd schema/cyclonedx && make
|
||||
|
||||
.PHONY: validate-cyclonedx-vex-schema
|
||||
validate-cyclonedx-vex-schema:
|
||||
cd schema/cyclonedxvex && make
|
||||
|
||||
.PHONY: validate-grype-db-schema
|
||||
validate-grype-db-schema:
|
||||
# ensure the codebase is only referencing a single grype-db schema version, multiple is not allowed
|
||||
|
|
2
go.mod
2
go.mod
|
@ -3,6 +3,7 @@ module github.com/anchore/grype
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.2
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/adrg/xdg v0.2.1
|
||||
|
@ -75,7 +76,6 @@ require (
|
|||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/CycloneDX/cyclonedx-go v0.5.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
|
|
39
grype/presenter/cyclonedxvex/bom_metadata.go
Normal file
39
grype/presenter/cyclonedxvex/bom_metadata.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package cyclonedxvex
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// NewBomMetadata returns a new BomDescriptor tailored for the current time and "syft" tool details.
|
||||
func NewBomMetadata(name, version string, srcMetadata *source.Metadata) *cyclonedx.Metadata {
|
||||
metadata := cyclonedx.Metadata{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Tools: &[]cyclonedx.Tool{
|
||||
{
|
||||
Vendor: "anchore",
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
},
|
||||
}
|
||||
if srcMetadata != nil {
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
metadata.Component = &cyclonedx.Component{
|
||||
Type: "container",
|
||||
Name: srcMetadata.ImageMetadata.UserInput,
|
||||
Version: srcMetadata.ImageMetadata.ManifestDigest,
|
||||
}
|
||||
case source.DirectoryScheme:
|
||||
metadata.Component = &cyclonedx.Component{
|
||||
Type: "file",
|
||||
Name: srcMetadata.Path,
|
||||
}
|
||||
}
|
||||
}
|
||||
return &metadata
|
||||
}
|
88
grype/presenter/cyclonedxvex/document.go
Normal file
88
grype/presenter/cyclonedxvex/document.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package cyclonedxvex
|
||||
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/version"
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// NewDocument returns a CycloneDX Document object populated with the SBOM and vulnerability findings.
|
||||
func NewDocument(packages []pkg.Package, matches match.Matches, srcMetadata *source.Metadata, provider vulnerability.MetadataProvider) (*cyclonedx.BOM, error) {
|
||||
versionInfo := version.FromBuild()
|
||||
doc := cyclonedx.NewBOM()
|
||||
doc.SerialNumber = uuid.New().URN()
|
||||
if srcMetadata != nil {
|
||||
doc.Metadata = NewBomMetadata(internal.ApplicationName, versionInfo.Version, srcMetadata)
|
||||
}
|
||||
|
||||
// attach matches
|
||||
components := []cyclonedx.Component{}
|
||||
vulnerabilities := []cyclonedx.Vulnerability{}
|
||||
|
||||
for _, p := range packages {
|
||||
component := getComponent(p)
|
||||
pkgMatches := matches.GetByPkgID(p.ID)
|
||||
|
||||
if len(pkgMatches) > 0 {
|
||||
for _, m := range pkgMatches {
|
||||
v, err := NewVulnerability(m, provider)
|
||||
if err != nil {
|
||||
return &cyclonedx.BOM{}, err
|
||||
}
|
||||
v.Affects = &[]cyclonedx.Affects{
|
||||
{
|
||||
Ref: component.BOMRef,
|
||||
},
|
||||
}
|
||||
vulnerabilities = append(vulnerabilities, v)
|
||||
}
|
||||
}
|
||||
// add a *copy* of the Component to the bom document
|
||||
components = append(components, component)
|
||||
}
|
||||
doc.Components = &components
|
||||
doc.Vulnerabilities = &vulnerabilities
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func getComponent(p pkg.Package) cyclonedx.Component {
|
||||
bomRef := string(p.ID)
|
||||
// try and parse the PURL if possible and append syft id to it, to make
|
||||
// the purl unique in the BOM.
|
||||
// TODO: In the future we may want to dedupe by PURL and combine components with
|
||||
// the same PURL while preserving their unique metadata.
|
||||
if parsedPURL, err := packageurl.FromString(p.PURL); err == nil {
|
||||
parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: string(p.ID)})
|
||||
bomRef = parsedPURL.ToString()
|
||||
}
|
||||
// make a new Component (by value)
|
||||
component := cyclonedx.Component{
|
||||
Type: "library", // TODO: this is not accurate, syft does the same thing
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
PackageURL: p.PURL,
|
||||
BOMRef: bomRef,
|
||||
}
|
||||
|
||||
var licenses cyclonedx.Licenses
|
||||
for _, licenseName := range p.Licenses {
|
||||
licenses = append(licenses, cyclonedx.LicenseChoice{
|
||||
License: &cyclonedx.License{
|
||||
Name: licenseName,
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(licenses) > 0 {
|
||||
// adding licenses to the Component
|
||||
component.Licenses = &licenses
|
||||
}
|
||||
return component
|
||||
}
|
52
grype/presenter/cyclonedxvex/presenter.go
Normal file
52
grype/presenter/cyclonedxvex/presenter.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package cyclonedxvex
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Presenter writes a CycloneDX report from the given Matches and Scope contents
|
||||
type Presenter struct {
|
||||
results match.Matches
|
||||
packages []pkg.Package
|
||||
srcMetadata *source.Metadata
|
||||
metadataProvider vulnerability.MetadataProvider
|
||||
embedded bool
|
||||
format cyclonedx.BOMFileFormat
|
||||
}
|
||||
|
||||
// NewPresenter is a *Presenter constructor
|
||||
func NewPresenter(results match.Matches, packages []pkg.Package, srcMetadata *source.Metadata, metadataProvider vulnerability.MetadataProvider, embedded bool, format cyclonedx.BOMFileFormat) *Presenter {
|
||||
return &Presenter{
|
||||
results: results,
|
||||
packages: packages,
|
||||
metadataProvider: metadataProvider,
|
||||
srcMetadata: srcMetadata,
|
||||
embedded: embedded,
|
||||
format: format,
|
||||
}
|
||||
}
|
||||
|
||||
// Present creates a CycloneDX-based reporting
|
||||
func (pres *Presenter) Present(output io.Writer) error {
|
||||
bom, err := NewDocument(pres.packages, pres.results, pres.srcMetadata, pres.metadataProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoder := cyclonedx.NewBOMEncoder(output, pres.format)
|
||||
encoder.SetPretty(true)
|
||||
|
||||
err = encoder.Encode(bom)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
186
grype/presenter/cyclonedxvex/presenter_test.go
Normal file
186
grype/presenter/cyclonedxvex/presenter_test.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
package cyclonedxvex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
|
||||
|
||||
func createResults() (match.Matches, []pkg.Package) {
|
||||
|
||||
pkg1 := pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: syftPkg.DebPkg,
|
||||
}
|
||||
pkg2 := pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: syftPkg.DebPkg,
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"Apache-v2",
|
||||
},
|
||||
}
|
||||
|
||||
var match1 = match.Match{
|
||||
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-1999-0001",
|
||||
Namespace: "source-1",
|
||||
},
|
||||
Package: pkg1,
|
||||
Details: []match.Detail{
|
||||
{
|
||||
Type: match.ExactDirectMatch,
|
||||
Matcher: match.DpkgMatcher,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var match2 = match.Match{
|
||||
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-1999-0002",
|
||||
Namespace: "source-2",
|
||||
},
|
||||
Package: pkg2,
|
||||
Details: []match.Detail{
|
||||
{
|
||||
Type: match.ExactIndirectMatch,
|
||||
Matcher: match.DpkgMatcher,
|
||||
SearchedBy: map[string]interface{}{
|
||||
"some": "key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
matches := match.NewMatches()
|
||||
|
||||
matches.Add(match1, match2)
|
||||
|
||||
return matches, []pkg.Package{pkg1, pkg2}
|
||||
}
|
||||
|
||||
func TestCycloneDxPresenterImage(t *testing.T) {
|
||||
for _, tcase := range []struct {
|
||||
name string
|
||||
format cyclonedx.BOMFileFormat
|
||||
}{
|
||||
{name: "json", format: cyclonedx.BOMFileFormatJSON},
|
||||
{name: "xml", format: cyclonedx.BOMFileFormatXML},
|
||||
} {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
matches, packages := createResults()
|
||||
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||
s, _ := source.NewFromImage(img, "user-input")
|
||||
|
||||
// This accounts for the non-deterministic digest value that we end up with when
|
||||
// we build a container image dynamically during testing. Ultimately, we should
|
||||
// use a golden image as a test fixture in place of building this image during
|
||||
// testing. At that time, this line will no longer be necessary.
|
||||
//
|
||||
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
|
||||
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
pres := NewPresenter(matches, packages, &s.Metadata, models.NewMetadataMock(), true, tcase.format)
|
||||
// run presenter
|
||||
err := pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual := buffer.Bytes()
|
||||
if *update {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which are tested independently
|
||||
actual = redact(actual)
|
||||
expected = redact(expected)
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCycloneDxPresenterDir(t *testing.T) {
|
||||
for _, tcase := range []struct {
|
||||
name string
|
||||
format cyclonedx.BOMFileFormat
|
||||
}{
|
||||
{name: "json", format: cyclonedx.BOMFileFormatJSON},
|
||||
{name: "xml", format: cyclonedx.BOMFileFormatXML},
|
||||
} {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
matches, packages := createResults()
|
||||
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pres := NewPresenter(matches, packages, &s.Metadata, models.NewMetadataMock(), true, tcase.format)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual := buffer.Bytes()
|
||||
if *update {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which are tested independently
|
||||
actual = redact(actual)
|
||||
expected = redact(expected)
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func redact(s []byte) []byte {
|
||||
serialPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
|
||||
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
|
||||
|
||||
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern} {
|
||||
s = pattern.ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
|
||||
FROM scratch
|
||||
ADD file-1.txt /somefile-1.txt
|
||||
ADD file-2.txt /somefile-2.txt
|
||||
# note: adding a directory will behave differently on docker engine v18 vs v19
|
||||
ADD target /
|
|
@ -0,0 +1 @@
|
|||
this file has contents
|
|
@ -0,0 +1 @@
|
|||
file-2 contents!
|
|
@ -0,0 +1,2 @@
|
|||
another file!
|
||||
with lines...
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.4",
|
||||
"serialNumber": "urn:uuid:ef5bc559-c07e-46bd-be44-dc400d6ff87b",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2022-03-31T22:50:49+01:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "grype",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"type": "file",
|
||||
"name": "/some/path"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "package-1-id",
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1"
|
||||
},
|
||||
{
|
||||
"bom-ref": "package-2-id",
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"licenses": [
|
||||
{
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"license": {
|
||||
"name": "Apache-v2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-1999-0001",
|
||||
"source": {
|
||||
"name": "source-1",
|
||||
"url": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0001"
|
||||
},
|
||||
"ratings": [
|
||||
{
|
||||
"score": 0,
|
||||
"severity": "low"
|
||||
},
|
||||
{
|
||||
"score": 4,
|
||||
"method": "CVSSv3",
|
||||
"vector": "another vector"
|
||||
}
|
||||
],
|
||||
"description": "1999-01 description",
|
||||
"advisories": [],
|
||||
"analysis": {
|
||||
"state": "in_triage"
|
||||
},
|
||||
"affects": [
|
||||
{
|
||||
"ref": "package-1-id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CVE-1999-0002",
|
||||
"source": {
|
||||
"name": "source-2",
|
||||
"url": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0002"
|
||||
},
|
||||
"ratings": [
|
||||
{
|
||||
"score": 0,
|
||||
"severity": "critical"
|
||||
},
|
||||
{
|
||||
"score": 1,
|
||||
"method": "CVSSv2",
|
||||
"vector": "vector"
|
||||
}
|
||||
],
|
||||
"description": "1999-02 description",
|
||||
"advisories": [],
|
||||
"analysis": {
|
||||
"state": "in_triage"
|
||||
},
|
||||
"affects": [
|
||||
{
|
||||
"ref": "package-2-id"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:88a468fe-e518-4932-8af7-984a41026f6a" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-03-31T22:50:49+01:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>grype</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="file">
|
||||
<name>/some/path</name>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="package-1-id" type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
</component>
|
||||
<component bom-ref="package-2-id" type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
</component>
|
||||
</components>
|
||||
<vulnerabilities>
|
||||
<vulnerability>
|
||||
<id>CVE-1999-0001</id>
|
||||
<source>
|
||||
<name>source-1</name>
|
||||
<url>http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0001</url>
|
||||
</source>
|
||||
<ratings>
|
||||
<rating>
|
||||
<score>0</score>
|
||||
<severity>low</severity>
|
||||
</rating>
|
||||
<rating>
|
||||
<score>4</score>
|
||||
<method>CVSSv3</method>
|
||||
<vector>another vector</vector>
|
||||
</rating>
|
||||
</ratings>
|
||||
<description>1999-01 description</description>
|
||||
<advisories></advisories>
|
||||
<analysis>
|
||||
<state>in_triage</state>
|
||||
</analysis>
|
||||
<affects>
|
||||
<target>
|
||||
<ref>package-1-id</ref>
|
||||
</target>
|
||||
</affects>
|
||||
</vulnerability>
|
||||
<vulnerability>
|
||||
<id>CVE-1999-0002</id>
|
||||
<source>
|
||||
<name>source-2</name>
|
||||
<url>http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0002</url>
|
||||
</source>
|
||||
<ratings>
|
||||
<rating>
|
||||
<score>0</score>
|
||||
<severity>critical</severity>
|
||||
</rating>
|
||||
<rating>
|
||||
<score>1</score>
|
||||
<method>CVSSv2</method>
|
||||
<vector>vector</vector>
|
||||
</rating>
|
||||
</ratings>
|
||||
<description>1999-02 description</description>
|
||||
<advisories></advisories>
|
||||
<analysis>
|
||||
<state>in_triage</state>
|
||||
</analysis>
|
||||
<affects>
|
||||
<target>
|
||||
<ref>package-2-id</ref>
|
||||
</target>
|
||||
</affects>
|
||||
</vulnerability>
|
||||
</vulnerabilities>
|
||||
</bom>
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.4",
|
||||
"serialNumber": "urn:uuid:7266e5f9-8e64-464a-b998-fed127984f96",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2022-03-31T22:50:49+01:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "grype",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"type": "container",
|
||||
"name": "user-input",
|
||||
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "package-1-id",
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1"
|
||||
},
|
||||
{
|
||||
"bom-ref": "package-2-id",
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
"licenses": [
|
||||
{
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"license": {
|
||||
"name": "Apache-v2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-1999-0001",
|
||||
"source": {
|
||||
"name": "source-1",
|
||||
"url": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0001"
|
||||
},
|
||||
"ratings": [
|
||||
{
|
||||
"score": 0,
|
||||
"severity": "low"
|
||||
},
|
||||
{
|
||||
"score": 4,
|
||||
"method": "CVSSv3",
|
||||
"vector": "another vector"
|
||||
}
|
||||
],
|
||||
"description": "1999-01 description",
|
||||
"advisories": [],
|
||||
"analysis": {
|
||||
"state": "in_triage"
|
||||
},
|
||||
"affects": [
|
||||
{
|
||||
"ref": "package-1-id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "CVE-1999-0002",
|
||||
"source": {
|
||||
"name": "source-2",
|
||||
"url": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0002"
|
||||
},
|
||||
"ratings": [
|
||||
{
|
||||
"score": 0,
|
||||
"severity": "critical"
|
||||
},
|
||||
{
|
||||
"score": 1,
|
||||
"method": "CVSSv2",
|
||||
"vector": "vector"
|
||||
}
|
||||
],
|
||||
"description": "1999-02 description",
|
||||
"advisories": [],
|
||||
"analysis": {
|
||||
"state": "in_triage"
|
||||
},
|
||||
"affects": [
|
||||
{
|
||||
"ref": "package-2-id"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:0f37ab1e-55b1-46ec-bb8b-5e1c5f6b7904" version="1">
|
||||
<metadata>
|
||||
<timestamp>2022-03-31T22:50:49+01:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>grype</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component type="container">
|
||||
<name>user-input</name>
|
||||
<version>sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="package-1-id" type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
</component>
|
||||
<component bom-ref="package-2-id" type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
</component>
|
||||
</components>
|
||||
<vulnerabilities>
|
||||
<vulnerability>
|
||||
<id>CVE-1999-0001</id>
|
||||
<source>
|
||||
<name>source-1</name>
|
||||
<url>http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0001</url>
|
||||
</source>
|
||||
<ratings>
|
||||
<rating>
|
||||
<score>0</score>
|
||||
<severity>low</severity>
|
||||
</rating>
|
||||
<rating>
|
||||
<score>4</score>
|
||||
<method>CVSSv3</method>
|
||||
<vector>another vector</vector>
|
||||
</rating>
|
||||
</ratings>
|
||||
<description>1999-01 description</description>
|
||||
<advisories></advisories>
|
||||
<analysis>
|
||||
<state>in_triage</state>
|
||||
</analysis>
|
||||
<affects>
|
||||
<target>
|
||||
<ref>package-1-id</ref>
|
||||
</target>
|
||||
</affects>
|
||||
</vulnerability>
|
||||
<vulnerability>
|
||||
<id>CVE-1999-0002</id>
|
||||
<source>
|
||||
<name>source-2</name>
|
||||
<url>http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-1999-0002</url>
|
||||
</source>
|
||||
<ratings>
|
||||
<rating>
|
||||
<score>0</score>
|
||||
<severity>critical</severity>
|
||||
</rating>
|
||||
<rating>
|
||||
<score>1</score>
|
||||
<method>CVSSv2</method>
|
||||
<vector>vector</vector>
|
||||
</rating>
|
||||
</ratings>
|
||||
<description>1999-02 description</description>
|
||||
<advisories></advisories>
|
||||
<analysis>
|
||||
<state>in_triage</state>
|
||||
</analysis>
|
||||
<affects>
|
||||
<target>
|
||||
<ref>package-2-id</ref>
|
||||
</target>
|
||||
</affects>
|
||||
</vulnerability>
|
||||
</vulnerabilities>
|
||||
</bom>
|
108
grype/presenter/cyclonedxvex/vulnerability.go
Normal file
108
grype/presenter/cyclonedxvex/vulnerability.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package cyclonedxvex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
)
|
||||
|
||||
// cvssVersionToMethod accepts a CVSS version as string (e.g. "3.1") and converts it to a
|
||||
// CycloneDx rating Method, for example "CVSSv3"
|
||||
func cvssVersionToMethod(version string) (cyclonedx.ScoringMethod, error) {
|
||||
value, err := strconv.ParseFloat(version, 64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch value {
|
||||
case 2:
|
||||
return cyclonedx.ScoringMethodCVSSv2, nil
|
||||
case 3:
|
||||
return cyclonedx.ScoringMethodCVSSv3, nil
|
||||
case 3.1:
|
||||
return cyclonedx.ScoringMethodCVSSv31, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unable to parse %s into a CVSS version", version)
|
||||
}
|
||||
}
|
||||
|
||||
// NewVulnerability creates a Vulnerability document from a match and the metadata provider
|
||||
func NewVulnerability(m match.Match, p vulnerability.MetadataProvider) (cyclonedx.Vulnerability, error) {
|
||||
metadata, err := p.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
|
||||
if err != nil {
|
||||
return cyclonedx.Vulnerability{}, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
|
||||
}
|
||||
|
||||
// The schema does not allow "Negligible", only allowing the following:
|
||||
// 'None', 'Low', 'Medium', 'High', 'Critical', 'Unknown'
|
||||
severityMap := map[string]cyclonedx.Severity{
|
||||
"unknown": cyclonedx.SeverityUnknown,
|
||||
"none": cyclonedx.SeverityNone,
|
||||
"info": cyclonedx.SeverityInfo,
|
||||
"negligible": cyclonedx.SeverityLow,
|
||||
"low": cyclonedx.SeverityLow,
|
||||
"medium": cyclonedx.SeverityMedium,
|
||||
"high": cyclonedx.SeverityHigh,
|
||||
"critical": cyclonedx.SeverityCritical,
|
||||
}
|
||||
severity, ok := severityMap[strings.ToLower(metadata.Severity)]
|
||||
if !ok {
|
||||
severity = cyclonedx.SeverityUnknown
|
||||
}
|
||||
var ratings = []cyclonedx.VulnerabilityRating{
|
||||
{
|
||||
Severity: severity,
|
||||
},
|
||||
}
|
||||
for _, cvss := range metadata.Cvss {
|
||||
method, err := cvssVersionToMethod(cvss.Version)
|
||||
if err != nil {
|
||||
log.Errorf("unable to parse CVSS version: %v", err)
|
||||
// do not halt execution if one CVSS fails to provide an accurate Version
|
||||
continue
|
||||
}
|
||||
rating := cyclonedx.VulnerabilityRating{
|
||||
Method: method,
|
||||
Vector: cvss.Vector,
|
||||
Score: cvss.Metrics.BaseScore,
|
||||
}
|
||||
|
||||
ratings = append(ratings, rating)
|
||||
}
|
||||
advisories := []cyclonedx.Advisory{}
|
||||
for _, url := range metadata.URLs {
|
||||
advisories = append(advisories, cyclonedx.Advisory{
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
v := cyclonedx.Vulnerability{
|
||||
ID: m.Vulnerability.ID,
|
||||
Source: &cyclonedx.Source{
|
||||
Name: m.Vulnerability.Namespace,
|
||||
URL: makeVulnerabilityURL(m.Vulnerability.ID),
|
||||
},
|
||||
Ratings: &ratings,
|
||||
Description: metadata.Description,
|
||||
Advisories: &advisories,
|
||||
Analysis: &cyclonedx.VulnerabilityAnalysis{
|
||||
State: cyclonedx.IASInTriage,
|
||||
},
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func makeVulnerabilityURL(id string) string {
|
||||
if strings.HasPrefix(id, "CVE-") {
|
||||
return fmt.Sprintf("http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", id)
|
||||
}
|
||||
if strings.HasPrefix(id, "GHSA") {
|
||||
return fmt.Sprintf("https://github.com/advisories/%s", id)
|
||||
}
|
||||
return id
|
||||
}
|
146
grype/presenter/cyclonedxvex/vulnerability_test.go
Normal file
146
grype/presenter/cyclonedxvex/vulnerability_test.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package cyclonedxvex
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
)
|
||||
|
||||
func TestCvssVersionToMethod(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input string
|
||||
expected cyclonedx.ScoringMethod
|
||||
errors bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid (not float)",
|
||||
input: "",
|
||||
expected: "",
|
||||
errors: true,
|
||||
},
|
||||
{
|
||||
desc: "CVSS v2",
|
||||
input: "2.0",
|
||||
expected: cyclonedx.ScoringMethodCVSSv2,
|
||||
errors: false,
|
||||
},
|
||||
{
|
||||
desc: "CVSS v3",
|
||||
input: "3.1",
|
||||
expected: cyclonedx.ScoringMethodCVSSv31,
|
||||
errors: false,
|
||||
},
|
||||
{
|
||||
desc: "CVSS v3",
|
||||
input: "3.0",
|
||||
expected: cyclonedx.ScoringMethodCVSSv3,
|
||||
errors: false,
|
||||
},
|
||||
{
|
||||
desc: "invalid (no match)",
|
||||
input: "15.4",
|
||||
expected: "",
|
||||
errors: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
actual, err := cvssVersionToMethod(tc.input)
|
||||
if !tc.errors {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type metadataProvider struct {
|
||||
severity string
|
||||
cvss []vulnerability.Cvss
|
||||
}
|
||||
|
||||
func (m metadataProvider) GetMetadata(id, namespace string) (*vulnerability.Metadata, error) {
|
||||
return &vulnerability.Metadata{
|
||||
ID: id,
|
||||
DataSource: "",
|
||||
Namespace: namespace,
|
||||
Severity: m.severity,
|
||||
URLs: nil,
|
||||
Description: "",
|
||||
Cvss: m.cvss,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestNewVulnerability_AlwaysIncludesSeverity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
match match.Match
|
||||
metadataProvider *metadataProvider
|
||||
}{
|
||||
{
|
||||
name: "populates severity with missing CVSS records",
|
||||
match: match.Match{
|
||||
Vulnerability: vulnerability.Vulnerability{},
|
||||
Package: pkg.Package{},
|
||||
Details: nil,
|
||||
},
|
||||
metadataProvider: &metadataProvider{
|
||||
severity: "High",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "populates severity with all CVSS records",
|
||||
match: match.Match{
|
||||
Vulnerability: vulnerability.Vulnerability{},
|
||||
Package: pkg.Package{},
|
||||
Details: nil,
|
||||
},
|
||||
metadataProvider: &metadataProvider{
|
||||
severity: "High",
|
||||
cvss: []vulnerability.Cvss{
|
||||
{
|
||||
Version: "2.0",
|
||||
Metrics: vulnerability.CvssMetrics{
|
||||
BaseScore: 1.1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "3.0",
|
||||
Metrics: vulnerability.CvssMetrics{
|
||||
BaseScore: 2.1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: "3.1",
|
||||
Metrics: vulnerability.CvssMetrics{
|
||||
BaseScore: 3.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual, err := NewVulnerability(test.match, test.metadataProvider)
|
||||
assert.NoError(t, err)
|
||||
if len(*actual.Ratings) == 0 {
|
||||
t.Fatalf("expected a rating but found none")
|
||||
}
|
||||
assert.Equal(t, string((*actual.Ratings)[0].Severity), strings.ToLower(test.metadataProvider.severity))
|
||||
for _, r := range (*actual.Ratings)[1:] {
|
||||
assert.Equal(t, string(r.Severity), "")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ const (
|
|||
cycloneDXFormat format = "cyclonedx"
|
||||
sarifFormat format = "sarif"
|
||||
templateFormat format = "template"
|
||||
embeddedVEXJSON format = "embedded-cyclonedx-vex-json"
|
||||
embeddedVEXXML format = "embedded-cyclonedx-vex-xml"
|
||||
)
|
||||
|
||||
// format is a dedicated type to represent a specific kind of presenter output format.
|
||||
|
@ -35,6 +37,10 @@ func parse(userInput string) format {
|
|||
return sarifFormat
|
||||
case strings.ToLower(templateFormat.String()):
|
||||
return templateFormat
|
||||
case strings.ToLower(embeddedVEXJSON.String()):
|
||||
return embeddedVEXJSON
|
||||
case strings.ToLower(embeddedVEXXML.String()):
|
||||
return embeddedVEXXML
|
||||
default:
|
||||
return unknownFormat
|
||||
}
|
||||
|
|
|
@ -3,9 +3,12 @@ package presenter
|
|||
import (
|
||||
"io"
|
||||
|
||||
cdx "github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/presenter/cyclonedx"
|
||||
"github.com/anchore/grype/grype/presenter/cyclonedxvex"
|
||||
"github.com/anchore/grype/grype/presenter/json"
|
||||
"github.com/anchore/grype/grype/presenter/sarif"
|
||||
"github.com/anchore/grype/grype/presenter/table"
|
||||
|
@ -27,6 +30,10 @@ func GetPresenter(presenterConfig Config, matches match.Matches, ignoredMatches
|
|||
return table.NewPresenter(matches, packages, metadataProvider)
|
||||
case cycloneDXFormat:
|
||||
return cyclonedx.NewPresenter(matches, packages, context.Source, metadataProvider)
|
||||
case embeddedVEXJSON:
|
||||
return cyclonedxvex.NewPresenter(matches, packages, context.Source, metadataProvider, true, cdx.BOMFileFormatJSON)
|
||||
case embeddedVEXXML:
|
||||
return cyclonedxvex.NewPresenter(matches, packages, context.Source, metadataProvider, true, cdx.BOMFileFormatXML)
|
||||
case sarifFormat:
|
||||
return sarif.NewPresenter(matches, packages, context.Source, metadataProvider)
|
||||
case templateFormat:
|
||||
|
|
2
schema/cyclonedxvex/.gitignore
vendored
Normal file
2
schema/cyclonedxvex/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
bom.xml
|
||||
bom.json
|
14
schema/cyclonedxvex/Makefile
Normal file
14
schema/cyclonedxvex/Makefile
Normal file
|
@ -0,0 +1,14 @@
|
|||
.DEFAULT_GOAL := validate-schema
|
||||
|
||||
.PHONY: validate-schema
|
||||
validate-schema: validate-schema-xml validate-schema-json
|
||||
|
||||
.PHONY: validate-schema-xml
|
||||
validate-schema-xml:
|
||||
go run ../../main.go -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o embedded-cyclondex-vex-xml > bom.xml
|
||||
xmllint --noout --schema ./cyclonedx.xsd bom.xml
|
||||
|
||||
.PHONY: validate-schema-json
|
||||
validate-schema-json:
|
||||
go run ../../main.go -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o embedded-cyclondex-vex-json > bom.json
|
||||
../../.tmp/yajsv -s cyclonedx.json bom.json
|
5
schema/cyclonedxvex/README.md
Normal file
5
schema/cyclonedxvex/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# CycloneDX Schemas
|
||||
|
||||
`grype` generates a CycloneDX VEX output. This validation is similar to what is done in `syft`, validating output against CycloneDX schemas.
|
||||
|
||||
Validation is done with `xmllint`, which requires a copy of all schemas because it can't work with HTTP references. The schemas are modified to reference local copies of dependent schemas.
|
1697
schema/cyclonedxvex/cyclonedx.json
Normal file
1697
schema/cyclonedxvex/cyclonedx.json
Normal file
File diff suppressed because it is too large
Load diff
2407
schema/cyclonedxvex/cyclonedx.xsd
Normal file
2407
schema/cyclonedxvex/cyclonedx.xsd
Normal file
File diff suppressed because it is too large
Load diff
2639
schema/cyclonedxvex/spdx.xsd
Normal file
2639
schema/cyclonedxvex/spdx.xsd
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue