Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c3ba95a

Browse files
committedFeb 26, 2025··
Add initial support for loading VEX files from External References defined in CycloneDX SBOMs
* by specifying option `--vex sbom-ref` the externalReferences of a CycloneDx SBOM are used to fetch external VEX documents referenced as type `exploitability-statement` * trivy will error if one of the referenced VEX statements can not be fetched or parsed * added documentation of feature
1 parent b3521e8 commit c3ba95a

File tree

8 files changed

+345
-14
lines changed

8 files changed

+345
-14
lines changed
 
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# VEX SBOM Reference
2+
3+
!!! warning "EXPERIMENTAL"
4+
This feature might change without preserving backwards compatibility.
5+
6+
## Using externally referenced VEX documents
7+
8+
Trivy can discover and download VEX documents referenced in the `externalReferences` of a scanned CycloneDX SBOM. This
9+
requires the references to be of type `exploitability-statement`.
10+
11+
To be picked up by Trivy, following top level content needs to be part of a CycloneDx SBOM to dynamically resolve a
12+
remotely hosted file VEX file at the location `https://vex.example.com`:
13+
14+
```
15+
"externalReferences": [
16+
{
17+
"type": "exploitability-statement",
18+
"url": "https://vex.example.com/vex"
19+
}
20+
]
21+
```
22+
23+
This can also be used to dynamically retrieve VEX files stored on GitHub with an `externalReference` such as:
24+
25+
```
26+
"externalReferences": [
27+
{
28+
"type": "exploitability-statement",
29+
"url": "https://raw.githubusercontent.com/aquasecurity/trivy/refs/heads/main/.vex/trivy.openvex.json"
30+
}
31+
]
32+
```
33+
34+
This is not enabled by default at the moment, but can be used when scanning a CycloneDx SBOM and explicitly specifying
35+
`--vex sbom-ref`.
36+
37+
```shell
38+
$ trivy sbom trivy.cdx.json --vex sbom-ref
39+
2025-01-19T13:29:31+01:00 INFO [vex] Retrieving external VEX document from host vex.example.com type="externalReference"
40+
2025-01-19T13:29:31+01:00 INFO Some vulnerabilities have been ignored/suppressed. Use the "--show-suppressed" flag to display them.
41+
```
42+
43+
All the referenced VEX files are retrieved via HTTP/HTTPS and used in the same way as if they were explicitly specified
44+
via a [file reference](./file.md).

‎mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ nav:
133133
- Overview: docs/supply-chain/vex/index.md
134134
- VEX Repository: docs/supply-chain/vex/repo.md
135135
- Local VEX Files: docs/supply-chain/vex/file.md
136+
- VEX SBOM Reference: docs/supply-chain/vex/sbom-ref.md
136137
- VEX Attestation: docs/supply-chain/vex/oci.md
137138
- Compliance:
138139
- Built-in Compliance: docs/compliance/compliance.md

‎pkg/sbom/core/bom.go

+27-6
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,13 @@ const (
4848
RelationshipDescribes RelationshipType = "describes"
4949
RelationshipContains RelationshipType = "contains"
5050
RelationshipDependsOn RelationshipType = "depends_on"
51+
52+
ExternalReferenceVEX ExternalReferenceType = "external_reference_vex"
5153
)
5254

5355
type ComponentType string
5456
type RelationshipType string
57+
type ExternalReferenceType string
5558

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

68+
// externalReferences is a list of documents that are referenced from this BOM but hosted elsewhere.
69+
// They are currently used to look for linked VEX documents
70+
externalReferences []ExternalReference
71+
6572
// Vulnerabilities is a list of vulnerabilities that affect the component.
6673
// CycloneDX: vulnerabilities
6774
// SPDX: N/A
@@ -192,6 +199,11 @@ type Relationship struct {
192199
Type RelationshipType
193200
}
194201

202+
type ExternalReference struct {
203+
URL string
204+
Type ExternalReferenceType
205+
}
206+
195207
type Vulnerability struct {
196208
dtypes.Vulnerability
197209
ID string
@@ -209,12 +221,13 @@ type Options struct {
209221

210222
func NewBOM(opts Options) *BOM {
211223
return &BOM{
212-
components: make(map[uuid.UUID]*Component),
213-
relationships: make(map[uuid.UUID][]Relationship),
214-
vulnerabilities: make(map[uuid.UUID][]Vulnerability),
215-
purls: make(map[string][]uuid.UUID),
216-
parents: make(map[uuid.UUID][]uuid.UUID),
217-
opts: opts,
224+
components: make(map[uuid.UUID]*Component),
225+
relationships: make(map[uuid.UUID][]Relationship),
226+
vulnerabilities: make(map[uuid.UUID][]Vulnerability),
227+
purls: make(map[string][]uuid.UUID),
228+
parents: make(map[uuid.UUID][]uuid.UUID),
229+
externalReferences: make([]ExternalReference, 0),
230+
opts: opts,
218231
}
219232
}
220233

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

295+
func (b *BOM) AddExternalReferences(refs []ExternalReference) {
296+
b.externalReferences = append(b.externalReferences, refs...)
297+
}
298+
282299
func (b *BOM) Root() *Component {
283300
root, ok := b.components[b.rootID]
284301
if !ok {
@@ -308,6 +325,10 @@ func (b *BOM) Vulnerabilities() map[uuid.UUID][]Vulnerability {
308325
return b.vulnerabilities
309326
}
310327

328+
func (b *BOM) ExternalReferences() []ExternalReference {
329+
return b.externalReferences
330+
}
331+
311332
func (b *BOM) Parents() map[uuid.UUID][]uuid.UUID {
312333
return b.parents
313334
}

‎pkg/sbom/cyclonedx/unmarshal.go

+39
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cyclonedx
33
import (
44
"bytes"
55
"errors"
6+
"fmt"
67
"io"
78
"strings"
89

@@ -87,6 +88,11 @@ func (b *BOM) parseBOM(bom *cdx.BOM) error {
8788
b.BOM.AddRelationship(ref, dependency, core.RelationshipDependsOn)
8889
}
8990
}
91+
92+
if refs := b.parseExternalReferences(bom); refs != nil {
93+
b.BOM.AddExternalReferences(refs)
94+
}
95+
9096
return nil
9197
}
9298

@@ -103,6 +109,39 @@ func (b *BOM) parseMetadataComponent(bom *cdx.BOM) (*core.Component, error) {
103109
return root, nil
104110
}
105111

112+
func (b *BOM) parseExternalReferences(bom *cdx.BOM) []core.ExternalReference {
113+
if bom.ExternalReferences == nil {
114+
return nil
115+
}
116+
var refs = make([]core.ExternalReference, 0)
117+
118+
for _, ref := range *bom.ExternalReferences {
119+
t, err := b.unmarshalReferenceType(ref.Type)
120+
if err != nil {
121+
continue
122+
}
123+
124+
externalReference := core.ExternalReference{
125+
Type: t,
126+
URL: ref.URL,
127+
}
128+
129+
refs = append(refs, externalReference)
130+
}
131+
return refs
132+
}
133+
134+
func (b *BOM) unmarshalReferenceType(t cdx.ExternalReferenceType) (core.ExternalReferenceType, error) {
135+
var referenceType core.ExternalReferenceType
136+
switch t {
137+
case cdx.ERTypeExploitabilityStatement:
138+
referenceType = core.ExternalReferenceVEX
139+
default:
140+
return "", fmt.Errorf("unsupported external reference type: %s", t)
141+
}
142+
return referenceType, nil
143+
}
144+
106145
func (b *BOM) parseComponents(cdxComponents *[]cdx.Component) map[string]*core.Component {
107146
components := make(map[string]*core.Component)
108147
for _, component := range lo.FromPtr(cdxComponents) {

‎pkg/vex/document.go

+15-5
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,39 @@ func NewDocument(filePath string, report *types.Report) (VEX, error) {
2727
}
2828
defer f.Close()
2929

30+
v, errs := decodeVEX(f, filePath, report)
31+
if errs != nil {
32+
return nil, xerrors.Errorf("unable to load VEX from file: %w", errs)
33+
} else {
34+
return v, nil
35+
}
36+
}
37+
38+
func decodeVEX(r io.ReadSeeker, source string, report *types.Report) (VEX, error) {
39+
3040
var errs error
3141
// Try CycloneDX JSON
32-
if ok, err := sbom.IsCycloneDXJSON(f); err != nil {
42+
if ok, err := sbom.IsCycloneDXJSON(r); err != nil {
3343
errs = multierror.Append(errs, err)
3444
} else if ok {
35-
return decodeCycloneDXJSON(f, report)
45+
return decodeCycloneDXJSON(r, report)
3646
}
3747

3848
// Try OpenVEX
39-
if v, err := decodeOpenVEX(f, filePath); err != nil {
49+
if v, err := decodeOpenVEX(r, source); err != nil {
4050
errs = multierror.Append(errs, err)
4151
} else if v != nil {
4252
return v, nil
4353
}
4454

4555
// Try CSAF
46-
if v, err := decodeCSAF(f, filePath); err != nil {
56+
if v, err := decodeCSAF(r, source); err != nil {
4757
errs = multierror.Append(errs, err)
4858
} else if v != nil {
4959
return v, nil
5060
}
5161

52-
return nil, xerrors.Errorf("unable to load VEX: %w", errs)
62+
return nil, xerrors.Errorf("unable to decode VEX: %w", errs)
5363
}
5464

5565
func decodeCycloneDXJSON(r io.ReadSeeker, report *types.Report) (*CycloneDX, error) {

‎pkg/vex/sbomref.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package vex
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
10+
"golang.org/x/xerrors"
11+
12+
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
13+
"github.com/aquasecurity/trivy/pkg/log"
14+
"github.com/aquasecurity/trivy/pkg/sbom/core"
15+
"github.com/aquasecurity/trivy/pkg/types"
16+
)
17+
18+
type SBOMReferenceSet struct {
19+
VEXes []VEX
20+
}
21+
22+
func NewSBOMReferenceSet(report *types.Report) (*SBOMReferenceSet, error) {
23+
24+
if report.ArtifactType != artifact.TypeCycloneDX {
25+
return nil, xerrors.Errorf("externalReferences can only be used when scanning CycloneDX SBOMs: %w", report.ArtifactType)
26+
}
27+
28+
var externalRefs = report.BOM.ExternalReferences()
29+
urls := parseToURLs(externalRefs)
30+
31+
v, err := retrieveExternalVEXDocuments(urls, report)
32+
if err != nil {
33+
return nil, xerrors.Errorf("failed to fetch external VEX documents: %w", err)
34+
} else if v == nil {
35+
return nil, nil
36+
}
37+
38+
return &SBOMReferenceSet{VEXes: v}, nil
39+
}
40+
41+
func parseToURLs(refs []core.ExternalReference) []url.URL {
42+
var urls []url.URL
43+
for _, ref := range refs {
44+
if ref.Type == core.ExternalReferenceVEX {
45+
val, err := url.Parse(ref.URL)
46+
// do not concern ourselves with relative URLs at this point
47+
if err != nil || (val.Scheme != "https" && val.Scheme != "http") {
48+
continue
49+
}
50+
urls = append(urls, *val)
51+
}
52+
}
53+
return urls
54+
}
55+
56+
func retrieveExternalVEXDocuments(refs []url.URL, report *types.Report) ([]VEX, error) {
57+
58+
logger := log.WithPrefix("vex").With(log.String("type", "external_reference"))
59+
60+
var docs []VEX
61+
for _, ref := range refs {
62+
doc, err := retrieveExternalVEXDocument(ref, report)
63+
if err != nil {
64+
return nil, xerrors.Errorf("failed to retrieve external VEX document: %w", err)
65+
}
66+
docs = append(docs, doc)
67+
}
68+
logger.Debug("Retrieved external VEX documents", "count", len(docs))
69+
70+
if len(docs) == 0 {
71+
logger.Info("No external VEX documents found")
72+
return nil, nil
73+
}
74+
return docs, nil
75+
76+
}
77+
78+
func retrieveExternalVEXDocument(vexUrl url.URL, report *types.Report) (VEX, error) {
79+
80+
logger := log.WithPrefix("vex").With(log.String("type", "external_reference"))
81+
82+
logger.Info(fmt.Sprintf("Retrieving external VEX document from host %s", vexUrl.Host))
83+
84+
res, err := http.Get(vexUrl.String())
85+
if err != nil {
86+
return nil, xerrors.Errorf("unable to fetch file via HTTP: %w", err)
87+
}
88+
defer res.Body.Close()
89+
90+
if res.StatusCode != http.StatusOK {
91+
return nil, xerrors.Errorf("did not receive 2xx status code: %w", res.StatusCode)
92+
}
93+
94+
val, err := io.ReadAll(res.Body)
95+
if err != nil {
96+
return nil, xerrors.Errorf("unable to read response into memory: %w", err)
97+
}
98+
99+
if v, err := decodeVEX(bytes.NewReader(val), vexUrl.String(), report); err != nil {
100+
return nil, xerrors.Errorf("unable to load VEX from external reference: %w", err)
101+
} else {
102+
return v, nil
103+
}
104+
}
105+
106+
func (set *SBOMReferenceSet) NotAffected(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) {
107+
108+
for _, vex := range set.VEXes {
109+
if m, notAffected := vex.NotAffected(vuln, product, subComponent); notAffected {
110+
return m, notAffected
111+
}
112+
}
113+
return types.ModifiedFinding{}, false
114+
}

‎pkg/vex/sbomref_test.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package vex_test
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
13+
"github.com/aquasecurity/trivy/pkg/sbom/core"
14+
"github.com/aquasecurity/trivy/pkg/types"
15+
"github.com/aquasecurity/trivy/pkg/vex"
16+
)
17+
18+
const (
19+
vexExternalRef = "/openvex"
20+
vexUnknown = "/unknown"
21+
vexNotFound = "/not-found"
22+
)
23+
24+
func setUpServer(t *testing.T) *httptest.Server {
25+
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26+
println(r.URL.Path)
27+
if r.URL.Path == vexExternalRef {
28+
f, err := os.Open("testdata/" + vexExternalRef + ".json")
29+
if err != nil {
30+
t.Error(err)
31+
}
32+
33+
defer f.Close()
34+
35+
_, err = io.Copy(w, f)
36+
if err != nil {
37+
t.Error(err)
38+
}
39+
} else if r.URL.Path == vexUnknown {
40+
f, err := os.Open("testdata/" + vexUnknown + ".json")
41+
if err != nil {
42+
t.Error(err)
43+
}
44+
defer f.Close()
45+
46+
_, err = io.Copy(w, f)
47+
if err != nil {
48+
t.Error(err)
49+
}
50+
}
51+
52+
http.NotFound(w, r)
53+
return
54+
}))
55+
return s
56+
}
57+
58+
func setupTestReport(s *httptest.Server, path string) *types.Report {
59+
r := types.Report{
60+
ArtifactType: artifact.TypeCycloneDX,
61+
BOM: &core.BOM{},
62+
}
63+
r.BOM.AddExternalReferences([]core.ExternalReference{{
64+
URL: s.URL + path,
65+
Type: core.ExternalReferenceVEX,
66+
}})
67+
68+
return &r
69+
}
70+
71+
func TestRetrieveExternalVEXDocuments(t *testing.T) {
72+
s := setUpServer(t)
73+
t.Cleanup(s.Close)
74+
75+
t.Run("external vex retrieval", func(t *testing.T) {
76+
set, err := vex.NewSBOMReferenceSet(setupTestReport(s, vexExternalRef))
77+
require.NoError(t, err)
78+
require.Len(t, set.VEXes, 1)
79+
})
80+
81+
t.Run("incompatible external vex", func(t *testing.T) {
82+
set, err := vex.NewSBOMReferenceSet(setupTestReport(s, vexUnknown))
83+
require.NoError(t, err)
84+
require.Nil(t, set)
85+
})
86+
87+
t.Run("vex not found", func(t *testing.T) {
88+
set, err := vex.NewSBOMReferenceSet(setupTestReport(s, vexNotFound))
89+
require.Error(t, err)
90+
require.Nil(t, set)
91+
})
92+
}

‎pkg/vex/vex.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import (
1515
)
1616

1717
const (
18-
TypeFile SourceType = "file"
19-
TypeRepository SourceType = "repo"
20-
TypeOCI SourceType = "oci"
18+
TypeFile SourceType = "file"
19+
TypeRepository SourceType = "repo"
20+
TypeOCI SourceType = "oci"
21+
TypeSBOMReference SourceType = "sbom-ref"
2122
)
2223

2324
// VEX represents Vulnerability Exploitability eXchange. It abstracts multiple VEX formats.
@@ -49,6 +50,8 @@ func NewSource(src string) Source {
4950
return Source{Type: TypeRepository}
5051
case "oci":
5152
return Source{Type: TypeOCI}
53+
case "sbom-ref":
54+
return Source{Type: TypeSBOMReference}
5255
default:
5356
return Source{
5457
Type: TypeFile,
@@ -111,6 +114,13 @@ func New(ctx context.Context, report *types.Report, opts Options) (*Client, erro
111114
} else if v == nil {
112115
continue
113116
}
117+
case TypeSBOMReference:
118+
v, err = NewSBOMReferenceSet(report)
119+
if err != nil {
120+
return nil, xerrors.Errorf("failed to create set of external VEX documents: %w", err)
121+
} else if v == nil {
122+
continue
123+
}
114124
default:
115125
log.Warn("Unsupported VEX source", log.String("type", string(src.Type)))
116126
continue

0 commit comments

Comments
 (0)
Please sign in to comment.