Skip to content

Commit

Permalink
Add initial support for embedded CycloneDX VEX documents
Browse files Browse the repository at this point in the history
Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>
  • Loading branch information
sambhav committed Mar 31, 2022
1 parent e00a252 commit 95a9d49
Show file tree
Hide file tree
Showing 24 changed files with 7,807 additions and 2 deletions.
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -56,7 +57,6 @@ require (
cloud.google.com/go/compute v1.3.0 // indirect
cloud.google.com/go/iam v0.1.1 // indirect
cloud.google.com/go/storage v1.21.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
Expand Down
39 changes: 39 additions & 0 deletions grype/presenter/cyclonedxvex/bom_metadata.go
Original file line number Diff line number Diff line change
@@ -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 changes: 88 additions & 0 deletions grype/presenter/cyclonedxvex/document.go
Original file line number Diff line number Diff line change
@@ -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: "syft-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 changes: 52 additions & 0 deletions grype/presenter/cyclonedxvex/presenter.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 95a9d49

Please sign in to comment.