Skip to content

Commit

Permalink
feat: PURL matching with qualifiers in OpenVEX (#5061)
Browse files Browse the repository at this point in the history
* feat: PURL match in OpenVEX

* test: fix fixture

* Update docs/docs/supply-chain/vex.md

Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>

* docs: add a comment about overriding statements

---------

Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
  • Loading branch information
knqyf263 and DmitriyLewen authored Aug 30, 2023
1 parent 4401998 commit 49fdd58
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 110 deletions.
1 change: 1 addition & 0 deletions .github/workflows/semantic-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jobs:
cyclonedx
spdx
purl
vex
helm
report
Expand Down
32 changes: 18 additions & 14 deletions docs/docs/supply-chain/vex.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Total: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
CVE-2020-8911 is no longer shown as it is filtered out according to the given CycloneDX VEX document.

## OpenVEX
Trivy also supports [OpenVEX](https://github.com/openvex/spec) that is designed to be minimal, compliant, interoperable, and embeddable.
Trivy also supports [OpenVEX][openvex] that is designed to be minimal, compliant, interoperable, and embeddable.
Since OpenVEX aims to be SBOM format agnostic, both CycloneDX and SPDX formats are available for use as input SBOMs in Trivy.

The following steps are required:
Expand All @@ -134,24 +134,21 @@ $ trivy image --format spdx-json --output debian11.spdx.json debian:11

### Create the VEX
Please see also [the example](https://github.com/openvex/examples).
The product identifiers differ depending on the SBOM format the VEX references.

- SPDX: [Package URL (PURL)](https://github.com/package-url/purl-spec)
- CycloneDX: [BOM-Link](https://cyclonedx.org/capabilities/bomlink/)
In Trivy, [the Package URL (PURL)][purl] is used as the product identifier.

```
$ cat <<EOF > trivy.openvex
$ cat <<EOF > debian11.openvex
{
"@context": "https://openvex.dev/ns",
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-2e67563e128250cbcb3e98930df948dd053e43271d70dc50cfa22d57e03fe96f",
"author": "Aqua Security",
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
"version": "1",
"timestamp": "2023-08-29T19:07:16.853479631-06:00",
"version": 1,
"statements": [
{
"vulnerability": "CVE-2019-8457",
"vulnerability": {"name": "CVE-2019-8457"},
"products": [
"pkg:deb/debian/libdb5.3@5.3.28+dfsg1-0.8?arch=arm64\u0026distro=debian-11.6"
{"@id": "pkg:deb/debian/libdb5.3@5.3.28+dfsg1-0.8"}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
Expand All @@ -161,15 +158,19 @@ $ cat <<EOF > trivy.openvex
EOF
```

In the above example, PURLs, located in `packages.externalRefs.referenceLocator` are used since the input SBOM format is SPDX.
In the above example, PURLs, located in `packages.externalRefs.referenceLocator` in SPDX are used for the product identifier.

As for CycloneDX BOM-Link, please reference [the CycloneDX section](#cyclonedx).
!!! note
If a qualifier is specified in the PURL used as the product id in the VEX, the qualifier is compared.
Other qualifiers are ignored in the comparison.
`pkg:deb/debian/curl@7.50.3-1` in OpenVEX matches `pkg:deb/debian/curl@7.50.3-1?arch=i386`,
while `pkg:deb/debian/curl@7.50.3-1?arch=amd64` does not match `pkg:deb/debian/curl@7.50.3-1?arch=i386`.

### Scan SBOM with VEX
Provide the VEX when scanning the SBOM.

```
$ trivy sbom debian11.spdx.json --vex trivy.openvex
$ trivy sbom debian11.spdx.json --vex debian11.openvex
...
2023-04-26T17:56:05.358+0300 INFO Filtered out the detected vulnerability {"VEX format": "OpenVEX", "vulnerability-id": "CVE-2019-8457", "status": "not_affected", "justification": "vulnerable_code_not_in_execute_path"}
Expand All @@ -179,3 +180,6 @@ Total: 80 (UNKNOWN: 0, LOW: 58, MEDIUM: 6, HIGH: 16, CRITICAL: 0)
```

CVE-2019-8457 is no longer shown as it is filtered out according to the given OpenVEX document.

[openvex]: https://github.com/openvex/spec
[purl]: https://github.com/package-url/purl-spec
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ require (
github.com/open-policy-agent/opa v0.45.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc4
github.com/openvex/go-vex v0.2.1
github.com/openvex/go-vex v0.2.5
github.com/owenrumney/go-sarif/v2 v2.2.0
github.com/package-url/packageurl-go v0.1.1
github.com/samber/lo v1.38.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1457,8 +1457,8 @@ github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaL
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openvex/go-vex v0.2.1 h1:OiZlTwYnT7jQjv68JRk+CmEcz+YAQhNQkJxg6kSkvuc=
github.com/openvex/go-vex v0.2.1/go.mod h1:BkkoLLIZxS5D8yDKM9pe6eRDHF00H2PuqNBnOxaExz0=
github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ=
github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo=
github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
github.com/owenrumney/go-sarif/v2 v2.2.0 h1:1DmZaijK0HBZCR1fgcDSGa7VzYkU9NDmbZ7qC2QfUjE=
github.com/owenrumney/go-sarif/v2 v2.2.0/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w=
Expand Down
8 changes: 4 additions & 4 deletions pkg/result/testdata/openvex.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"@context": "https://openvex.dev/ns",
"@context": "https://openvex.dev/ns/v0.2.0",
"author": "Aqua Security",
"role": "Project Release Bot",
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
"version": "1",
"version": 1,
"statements": [
{
"vulnerability": "CVE-2019-0001",
"vulnerability": {"name": "CVE-2019-0001"},
"products": [
"pkg:golang/github.com/aquasecurity/foo@1.2.3"
{"@id": "pkg:golang/github.com/aquasecurity/foo@1.2.3"}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
Expand Down
29 changes: 29 additions & 0 deletions pkg/vex/testdata/openvex-multiple.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"@context": "https://openvex.dev/ns",
"author": "Aqua Security",
"role": "Project Release Bot",
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
"version": 1,
"statements": [
{
"timestamp": "2023-01-15T01:02:03.853479631-06:00",
"vulnerability": {
"name": "CVE-2021-44228"
},
"products": [
{"@id": "pkg:maven/org.springframework.boot/spring-boot@2.6.0"}
],
"status": "affected"
},
{
"vulnerability": {
"name": "CVE-2021-44228"
},
"products": [
{"@id": "pkg:maven/org.springframework.boot/spring-boot@2.6.0"}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
}
]
}
17 changes: 0 additions & 17 deletions pkg/vex/testdata/openvex.cdx.json

This file was deleted.

10 changes: 6 additions & 4 deletions pkg/vex/testdata/openvex.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{
"@context": "https://openvex.dev/ns",
"@context": "https://openvex.dev/ns/v0.2.0",
"author": "Aqua Security",
"role": "Project Release Bot",
"timestamp": "2023-01-16T19:07:16.853479631-06:00",
"version": "1",
"version": 1,
"statements": [
{
"vulnerability": "CVE-2021-44228",
"vulnerability": {
"name": "CVE-2021-44228"
},
"products": [
"pkg:maven/org.springframework.boot/spring-boot@2.6.0"
{"@id": "pkg:maven/org.springframework.boot/spring-boot@2.6.0"}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
Expand Down
68 changes: 21 additions & 47 deletions pkg/vex/vex.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
Expand All @@ -36,62 +35,37 @@ type Statement struct {
}

type OpenVEX struct {
statements []Statement
logger *zap.SugaredLogger
vex openvex.VEX
logger *zap.SugaredLogger
}

func newOpenVEX(cycloneDX *ftypes.CycloneDX, vex openvex.VEX) VEX {
func newOpenVEX(vex openvex.VEX) VEX {
logger := log.Logger.With(zap.String("VEX format", "OpenVEX"))

openvex.SortStatements(vex.Statements, lo.FromPtr(vex.Timestamp))

// Convert openvex.Statement to Statement
stmts := lo.Map(vex.Statements, func(stmt openvex.Statement, index int) Statement {
return Statement{
// TODO: add subcomponents, etc.
VulnerabilityID: stmt.Vulnerability,
Affects: stmt.Products,
Status: Status(stmt.Status),
Justification: string(stmt.Justification),
}
})
// Reverse sorted statements so that the latest statement can come first.
stmts = lo.Reverse(stmts)

// If the SBOM format referenced by OpenVEX is CycloneDX
if cycloneDX != nil {
return &CycloneDX{
sbom: cycloneDX,
statements: stmts,
logger: logger,
}
}
return &OpenVEX{
statements: stmts,
logger: logger,
vex: vex,
logger: logger,
}
}

func (v *OpenVEX) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulnerability {
return lo.Filter(vulns, func(vuln types.DetectedVulnerability, _ int) bool {
stmt, ok := lo.Find(v.statements, func(item Statement) bool {
return item.VulnerabilityID == vuln.VulnerabilityID
})
if !ok {
stmts := v.vex.Matches(vuln.VulnerabilityID, vuln.PkgRef, nil)
if len(stmts) == 0 {
return true
}
return v.affected(vuln, stmt)
})
}

func (v *OpenVEX) affected(vuln types.DetectedVulnerability, stmt Statement) bool {
if slices.Contains(stmt.Affects, vuln.PkgRef) &&
(stmt.Status == StatusNotAffected || stmt.Status == StatusFixed) {
v.logger.Infow("Filtered out the detected vulnerability", zap.String("vulnerability-id", vuln.VulnerabilityID),
zap.String("status", string(stmt.Status)), zap.String("justification", stmt.Justification))
return false
}
return true
// Take the latest statement for a given vulnerability and product
// as a sequence of statements can be overridden by the newer one.
// cf. https://github.com/openvex/spec/blob/fa5ba0c0afedb008dc5ebad418548cacf16a3ca7/OPENVEX-SPEC.md#the-vex-statement
stmt := stmts[len(stmts)-1]
if stmt.Status == openvex.StatusNotAffected || stmt.Status == openvex.StatusFixed {
v.logger.Infow("Filtered out the detected vulnerability", zap.String("vulnerability-id", vuln.VulnerabilityID),
zap.String("status", string(stmt.Status)), zap.String("justification", string(stmt.Justification)))
return false
}
return true
})
}

type CycloneDX struct {
Expand Down Expand Up @@ -192,7 +166,7 @@ func New(filePath string, report types.Report) (VEX, error) {
}

// Try OpenVEX
if v, err := decodeOpenVEX(f, report); err != nil {
if v, err := decodeOpenVEX(f); err != nil {
errs = multierror.Append(errs, err)
} else {
return v, nil
Expand All @@ -215,7 +189,7 @@ func decodeCycloneDXJSON(r io.ReadSeeker, report types.Report) (VEX, error) {
return newCycloneDX(report.CycloneDX, vex), nil
}

func decodeOpenVEX(r io.ReadSeeker, report types.Report) (VEX, error) {
func decodeOpenVEX(r io.ReadSeeker) (VEX, error) {
// openvex/go-vex outputs log messages by default
logrus.SetOutput(io.Discard)

Expand All @@ -229,5 +203,5 @@ func decodeOpenVEX(r io.ReadSeeker, report types.Report) (VEX, error) {
if openVEX.Context == "" {
return nil, nil
}
return newOpenVEX(report.CycloneDX, openVEX), nil
return newOpenVEX(openVEX), nil
}
43 changes: 22 additions & 21 deletions pkg/vex/vex_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package vex_test

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/vex"
)

func TestMain(m *testing.M) {
log.InitLogger(false, true)
os.Exit(m.Run())
}

func TestVEX_Filter(t *testing.T) {
type fields struct {
filePath string
Expand All @@ -36,45 +43,39 @@ func TestVEX_Filter(t *testing.T) {
VulnerabilityID: "CVE-2021-44228",
PkgName: "spring-boot",
InstalledVersion: "2.6.0",
PkgRef: "pkg:maven/org.springframework.boot/spring-boot@2.6.0",
PkgRef: "pkg:maven/org.springframework.boot/spring-boot@2.6.0?type=pom",
},
},
},
want: []types.DetectedVulnerability{},
},
{
name: "CycloneDX SBOM with OpenVEX",
name: "OpenVEX, multiple statements",
fields: fields{
filePath: "testdata/openvex.cdx.json",
report: types.Report{
CycloneDX: &ftypes.CycloneDX{
SerialNumber: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
Version: 1,
},
},
filePath: "testdata/openvex-multiple.json",
},
args: args{
vulns: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2018-7489",
PkgName: "jackson-databind",
InstalledVersion: "2.8.0",
PkgRef: "jackson-databind-2.8.0",
VulnerabilityID: "CVE-2021-44228",
PkgName: "spring-boot",
InstalledVersion: "2.6.0",
PkgRef: "pkg:maven/org.springframework.boot/spring-boot@2.6.0?type=pom",
},
{
VulnerabilityID: "CVE-2018-7490",
PkgName: "jackson-databind",
InstalledVersion: "2.8.0",
PkgRef: "jackson-databind-2.8.0",
VulnerabilityID: "CVE-2021-0001",
PkgName: "spring-boot",
InstalledVersion: "2.6.0",
PkgRef: "pkg:maven/org.springframework.boot/spring-boot@2.6.0?type=pom",
},
},
},
want: []types.DetectedVulnerability{
{
VulnerabilityID: "CVE-2018-7490",
PkgName: "jackson-databind",
InstalledVersion: "2.8.0",
PkgRef: "jackson-databind-2.8.0",
VulnerabilityID: "CVE-2021-0001",
PkgName: "spring-boot",
InstalledVersion: "2.6.0",
PkgRef: "pkg:maven/org.springframework.boot/spring-boot@2.6.0?type=pom",
},
},
},
Expand Down

0 comments on commit 49fdd58

Please sign in to comment.