Skip to content

Commit

Permalink
Add initial support for loading VEX files from External References de…
Browse files Browse the repository at this point in the history
…fined in CycloneDX SBOMs
  • Loading branch information
RingoDev committed Jan 17, 2025
1 parent f9a6a71 commit 0542fea
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 14 deletions.
33 changes: 27 additions & 6 deletions pkg/sbom/core/bom.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ const (
RelationshipDescribes RelationshipType = "describes"
RelationshipContains RelationshipType = "contains"
RelationshipDependsOn RelationshipType = "depends_on"

ExternalReferenceVex ExternalReferenceType = "external_reference_vex"
)

type ComponentType string
type RelationshipType string
type ExternalReferenceType string

// BOM represents an intermediate representation of a component for SBOM.
type BOM struct {
Expand All @@ -62,6 +65,10 @@ type BOM struct {
components map[uuid.UUID]*Component
relationships map[uuid.UUID][]Relationship

// externalReferences is a list of documents that are referenced from this BOM but hosted elsewhere.
// They are currently used to look for linked VEX documents
externalReferences []ExternalReference

// Vulnerabilities is a list of vulnerabilities that affect the component.
// CycloneDX: vulnerabilities
// SPDX: N/A
Expand Down Expand Up @@ -192,6 +199,11 @@ type Relationship struct {
Type RelationshipType
}

type ExternalReference struct {
URL string
Type ExternalReferenceType
}

type Vulnerability struct {
dtypes.Vulnerability
ID string
Expand All @@ -209,12 +221,13 @@ type Options struct {

func NewBOM(opts Options) *BOM {
return &BOM{
components: make(map[uuid.UUID]*Component),
relationships: make(map[uuid.UUID][]Relationship),
vulnerabilities: make(map[uuid.UUID][]Vulnerability),
purls: make(map[string][]uuid.UUID),
parents: make(map[uuid.UUID][]uuid.UUID),
opts: opts,
components: make(map[uuid.UUID]*Component),
relationships: make(map[uuid.UUID][]Relationship),
vulnerabilities: make(map[uuid.UUID][]Vulnerability),
purls: make(map[string][]uuid.UUID),
parents: make(map[uuid.UUID][]uuid.UUID),
externalReferences: make([]ExternalReference, 0),
opts: opts,
}
}

Expand Down Expand Up @@ -279,6 +292,10 @@ func (b *BOM) AddVulnerabilities(c *Component, vulns []Vulnerability) {
b.vulnerabilities[c.id] = vulns
}

func (b *BOM) AddExternalReferences(refs []ExternalReference) {
b.externalReferences = append(b.externalReferences, refs...)
}

func (b *BOM) Root() *Component {
root, ok := b.components[b.rootID]
if !ok {
Expand Down Expand Up @@ -308,6 +325,10 @@ func (b *BOM) Vulnerabilities() map[uuid.UUID][]Vulnerability {
return b.vulnerabilities
}

func (b *BOM) ExternalReferences() []ExternalReference {
return b.externalReferences
}

func (b *BOM) Parents() map[uuid.UUID][]uuid.UUID {
return b.parents
}
Expand Down
39 changes: 39 additions & 0 deletions pkg/sbom/cyclonedx/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ func (b *BOM) parseBOM(bom *cdx.BOM) error {
b.BOM.AddRelationship(ref, dependency, core.RelationshipDependsOn)
}
}

if refs := b.parseExternalReferences(bom); refs != nil {
b.BOM.AddExternalReferences(refs)
}

return nil
}

Expand All @@ -103,6 +108,40 @@ func (b *BOM) parseMetadataComponent(bom *cdx.BOM) (*core.Component, error) {
return root, nil
}

func (b *BOM) parseExternalReferences(bom *cdx.BOM) []core.ExternalReference {
if bom.ExternalReferences == nil {
return nil
}
refs := make([]core.ExternalReference, 0)

Check failure on line 115 in pkg/sbom/cyclonedx/unmarshal.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

ruleguard: replace 'refs := make([]core.ExternalReference, 0)' with 'var refs []core.ExternalReference' (gocritic)

for _, ref := range *bom.ExternalReferences {
t, err := b.unmarshalReferenceType(ref.Type)
if err != nil {
continue
}

externalReference := core.ExternalReference{
Type: t,
URL: ref.URL,
}

refs = append(refs, externalReference)
}
return refs
}

func (b *BOM) unmarshalReferenceType(t cdx.ExternalReferenceType) (core.ExternalReferenceType, error) {
var referenceType core.ExternalReferenceType
switch t {
case cdx.ERTypeExploitabilityStatement:
referenceType = core.ExternalReferenceVex
default:
// no need to treat as an error - we are only supporting 1 of 25 different ref types
return referenceType, nil
}
return referenceType, nil
}

func (b *BOM) parseComponents(cdxComponents *[]cdx.Component) map[string]*core.Component {
components := make(map[string]*core.Component)
for _, component := range lo.FromPtr(cdxComponents) {
Expand Down
19 changes: 14 additions & 5 deletions pkg/vex/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,38 @@ func NewDocument(filePath string, report *types.Report) (VEX, error) {
}
defer f.Close()

if v, err := decodeVEX(f, filePath, report); err != nil {
return nil, xerrors.Errorf("unable to load VEX: %w", err)
} else {
return v, nil
}
}

func decodeVEX(r io.ReadSeeker, source string, report *types.Report) (VEX, error) {

var errs error
// Try CycloneDX JSON
if ok, err := sbom.IsCycloneDXJSON(f); err != nil {
if ok, err := sbom.IsCycloneDXJSON(r); err != nil {
errs = multierror.Append(errs, err)
} else if ok {
return decodeCycloneDXJSON(f, report)
return decodeCycloneDXJSON(r, report)
}

// Try OpenVEX
if v, err := decodeOpenVEX(f, filePath); err != nil {
if v, err := decodeOpenVEX(r, source); err != nil {
errs = multierror.Append(errs, err)
} else if v != nil {
return v, nil
}

// Try CSAF
if v, err := decodeCSAF(f, filePath); err != nil {
if v, err := decodeCSAF(r, source); err != nil {
errs = multierror.Append(errs, err)
} else if v != nil {
return v, nil
}

return nil, xerrors.Errorf("unable to load VEX: %w", errs)
return nil, errs
}

func decodeCycloneDXJSON(r io.ReadSeeker, report *types.Report) (*CycloneDX, error) {
Expand Down
106 changes: 106 additions & 0 deletions pkg/vex/sbomref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package vex

import (
"bytes"
"fmt"
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/sbom/core"
"github.com/aquasecurity/trivy/pkg/types"
"golang.org/x/xerrors"
"io"
"net/http"
"net/url"
)

type SBOMReferenceSet struct {
Vexes []VEX
}

func NewSBOMReferenceSet(report *types.Report) (*SBOMReferenceSet, error) {
fmt.Println("hello there")

if report.ArtifactType != artifact.TypeCycloneDX {
return nil, xerrors.Errorf("externalReferences can only be used when scanning CycloneDX SBOMs: %w", report.ArtifactType)
}

var externalRefs = report.BOM.ExternalReferences()
urls := ParseToURLs(externalRefs)

v, err := RetrieveExternalVEXDocuments(urls, report)
if err != nil {
return nil, xerrors.Errorf("failed to fetch external VEX document: %w", err)
}

return &SBOMReferenceSet{Vexes: v}, nil
}

func ParseToURLs(refs []core.ExternalReference) []url.URL {
var urls []url.URL
for _, ref := range refs {
if ref.Type == core.ExternalReferenceVex {
val, err := url.Parse(ref.URL)
// do not concern ourselves with relative URLs at this point
if err != nil || (val.Scheme != "https" && val.Scheme != "http") {
continue
}
urls = append(urls, *val)
}
}
return urls
}

func RetrieveExternalVEXDocuments(refs []url.URL, report *types.Report) ([]VEX, error) {

logger := log.WithPrefix("vex").With(log.String("type", "externalReference"))

var docs []VEX
for _, ref := range refs {
doc, err := RetrieveExternalVEXDocument(ref, report)
if err != nil && doc != nil {
docs = append(docs, doc)
}
}
logger.Debug("Retrieved external VEX documents", "count", len(docs))

if len(docs) == 0 {
logger.Info("No external VEX documents found")
return nil, nil
}
return docs, nil

}

func RetrieveExternalVEXDocument(VEXUrl url.URL, report *types.Report) (VEX, error) {

Check failure on line 74 in pkg/vex/sbomref.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

captLocal: `VEXUrl' should not be capitalized (gocritic)

logger := log.WithPrefix("vex").With(log.String("type", "externalReference"))

logger.Info(fmt.Sprintf("Retrieving external VEX document from host %s", VEXUrl.Host))

res, err := http.Get(VEXUrl.String())
if err != nil {
return nil, xerrors.Errorf("unable to fetch file via HTTP: %w", err)
}
defer res.Body.Close()

val, err := io.ReadAll(res.Body)
if err != nil {
return nil, xerrors.Errorf("unable to read response into memory: %w", err)
}

if v, err := decodeVEX(bytes.NewReader(val), VEXUrl.String(), report); err != nil {
return nil, xerrors.Errorf("unable to load VEX: %w", err)
} else {
return v, nil
}
}

func (set *SBOMReferenceSet) NotAffected(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) {

for _, vex := range set.Vexes {
if m, notAffected := vex.NotAffected(vuln, product, subComponent); notAffected {
return m, notAffected
}
}
return types.ModifiedFinding{}, false
}
73 changes: 73 additions & 0 deletions pkg/vex/sbomref_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package vex_test

import (
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
"github.com/aquasecurity/trivy/pkg/sbom/core"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/vex"
"github.com/stretchr/testify/require"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
)

const (
vexExternalRef = "/openvex"
vexUnknown = "/unknown"
)

func setUpServer(t *testing.T) *httptest.Server {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == vexExternalRef {
f, err := os.Open("testdata/" + vexExternalRef + ".json")
require.NoError(t, err)

Check failure on line 25 in pkg/vex/sbomref_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

go-require: do not use require in http handlers (testifylint)
defer f.Close()

_, err = io.Copy(w, f)
require.NoError(t, err)

Check failure on line 29 in pkg/vex/sbomref_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

go-require: do not use require in http handlers (testifylint)
} else if r.URL.Path == vexUnknown {
f, err := os.Open("testdata/" + vexUnknown + ".json")
require.NoError(t, err)

Check failure on line 32 in pkg/vex/sbomref_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

go-require: do not use require in http handlers (testifylint)
defer f.Close()

_, err = io.Copy(w, f)
require.NoError(t, err)

Check failure on line 36 in pkg/vex/sbomref_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

go-require: do not use require in http handlers (testifylint)
}

http.NotFound(w, r)
return
}))
return s
}

func setupTestReport(s *httptest.Server, path string) *types.Report {
r := types.Report{
ArtifactType: artifact.TypeCycloneDX,
BOM: &core.BOM{},
}
r.BOM.AddExternalReferences([]core.ExternalReference{{
URL: s.URL + path,
Type: core.ExternalReferenceVex,
}})

return &r
}

func TestRetrieveExternalVEXDocuments(t *testing.T) {
s := setUpServer(t)
t.Cleanup(s.Close)

t.Run("external vex retrieval", func(t *testing.T) {
set, err := vex.NewSBOMReferenceSet(setupTestReport(s, vexExternalRef))
require.NoError(t, err)
require.Equal(t, 1, len(set.Vexes))

Check failure on line 65 in pkg/vex/sbomref_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

len: use require.Len (testifylint)
})

t.Run("incompatible external vex", func(t *testing.T) {
set, err := vex.NewSBOMReferenceSet(setupTestReport(s, vexUnknown))
require.NoError(t, err)
require.Equal(t, 0, len(set.Vexes))

Check failure on line 71 in pkg/vex/sbomref_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

empty: use require.Empty (testifylint)
})
}
16 changes: 13 additions & 3 deletions pkg/vex/vex.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (
)

const (
TypeFile SourceType = "file"
TypeRepository SourceType = "repo"
TypeOCI SourceType = "oci"
TypeFile SourceType = "file"
TypeRepository SourceType = "repo"
TypeOCI SourceType = "oci"
TypeSBOMReference SourceType = "sbom-ref"
)

// VEX represents Vulnerability Exploitability eXchange. It abstracts multiple VEX formats.
Expand Down Expand Up @@ -49,6 +50,8 @@ func NewSource(src string) Source {
return Source{Type: TypeRepository}
case "oci":
return Source{Type: TypeOCI}
case "sbom-ref":
return Source{Type: TypeSBOMReference}
default:
return Source{
Type: TypeFile,
Expand Down Expand Up @@ -111,6 +114,13 @@ func New(ctx context.Context, report *types.Report, opts Options) (*Client, erro
} else if v == nil {
continue
}
case TypeSBOMReference:
v, err = NewSBOMReferenceSet(report)
if err != nil {
return nil, xerrors.Errorf("Failed to create set of external VEX documents: %w", err)
} else if v == nil {
continue
}
default:
log.Warn("Unsupported VEX source", log.String("type", string(src.Type)))
continue
Expand Down

0 comments on commit 0542fea

Please sign in to comment.