Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vex): Add support for CSAF format #5535

Merged
merged 21 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/aquasecurity/trivy

go 1.20
go 1.21
juan131 marked this conversation as resolved.
Show resolved Hide resolved

toolchain go1.21.1

require (
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
Expand Down Expand Up @@ -41,6 +43,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cheggaaa/pb/v3 v3.1.4
github.com/containerd/containerd v1.7.7
github.com/csaf-poc/csaf_distribution/v3 v3.0.0-rc.1
github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/fatih/color v1.15.0
Expand Down Expand Up @@ -100,7 +103,7 @@ require (
github.com/twitchtv/twirp v8.1.2+incompatible
github.com/xeipuuv/gojsonschema v1.2.0
github.com/xlab/treeprint v1.2.0
go.etcd.io/bbolt v1.3.7
go.etcd.io/bbolt v1.3.8
go.uber.org/zap v1.26.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/mod v0.13.0
Expand Down Expand Up @@ -133,6 +136,8 @@ require (
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
github.com/Intevation/gval v1.3.0 // indirect
github.com/Intevation/jsonpath v0.2.1 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
Expand All @@ -141,7 +146,7 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.1 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
Expand Down Expand Up @@ -206,7 +211,7 @@ require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cloudflare/circl v1.3.6 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/containerd/continuity v0.4.2 // indirect
github.com/containerd/fifo v1.1.0 // indirect
Expand Down Expand Up @@ -339,6 +344,7 @@ require (
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rubenv/sql-migrate v1.5.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
Expand Down
87 changes: 80 additions & 7 deletions go.sum

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions pkg/vex/csaf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package vex

import (
csaf "github.com/csaf-poc/csaf_distribution/v3/csaf"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if use https://github.com/openvex/go-vex/blob/main/pkg/csaf/csaf.go

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather use the one maintained by OASIS CSAF TC, see https://oasis-open.github.io/csaf-documentation/tools.html

"github.com/samber/lo"
"go.uber.org/zap"
"golang.org/x/exp/slices"

"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
)

type CSAF struct {
advisory csaf.Advisory
logger *zap.SugaredLogger
}

func newCSAF(advisory csaf.Advisory) VEX {
return &CSAF{
advisory: advisory,
logger: log.Logger.With(zap.String("VEX format", "CSAF")),
}
}

func (v *CSAF) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulnerability {
return lo.Filter(vulns, func(vuln types.DetectedVulnerability, _ int) bool {
found, ok := lo.Find(v.advisory.Vulnerabilities, func(item *csaf.Vulnerability) bool {
return string(*item.CVE) == vuln.VulnerabilityID
})

if !ok {
return true
}

return v.affected(found, vuln.PkgRef)
})
}

func (v *CSAF) affected(vuln *csaf.Vulnerability, pkgRef string) bool {
if vuln.ProductStatus != nil {
if vuln.ProductStatus.KnownNotAffected != nil {
notAffectedPURLs := findProductsPURLs(v.advisory, *vuln.ProductStatus.KnownNotAffected)
if slices.Contains(notAffectedPURLs, pkgRef) {
v.logger.Infow(
"Filtered out the detected vulnerability",
zap.String("vulnerability-id", string(*vuln.CVE)),
zap.String("status", string(StatusNotAffected)),
)
return false
}
}
if vuln.ProductStatus.Fixed != nil {
fixedPURLS := findProductsPURLs(v.advisory, *vuln.ProductStatus.Fixed)
if slices.Contains(fixedPURLS, pkgRef) {
v.logger.Infow(
"Filtered out the detected vulnerability",
zap.String("vulnerability-id", string(*vuln.CVE)),
zap.String("status", string(StatusFixed)),
)
return false
}
}
}

return true
}
73 changes: 73 additions & 0 deletions pkg/vex/csaf_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package vex

import (
"slices"

csaf "github.com/csaf-poc/csaf_distribution/v3/csaf"
)

// TODO: CSAF library should provide a function to find the pURL (or similar identifier, e.g. CPE)
// for a given product. Once that is available, we can remove this code.
// see https://github.com/csaf-poc/csaf_distribution/issues/484
func findProductsPURLs(advisory csaf.Advisory, products csaf.Products) []string {
var productsPURLs []string
for _, product := range products {
if product == nil {
continue
}

pURLsMap := findEveryProductPURLs(advisory)
if pURLs, ok := pURLsMap[string(*product)]; ok {
productsPURLs = append(productsPURLs, pURLs...)
}
}

return productsPURLs
}

// findEveryProductPURLs returns a map of every product id to a list of purls
func findEveryProductPURLs(adv csaf.Advisory) map[string][]string {
tree := adv.ProductTree
if tree == nil {
return nil
}

pURLsMap := make(map[string][]string)
// If we have found it and we have a valid URL add unique.
add := func(pid *csaf.ProductID, h *csaf.ProductIdentificationHelper) {
if pid != nil && h != nil && h.PURL != nil {
if _, ok := pURLsMap[string(*pid)]; !ok {
pURLsMap[string(*pid)] = []string{string(*h.PURL)}
} else {
if !slices.Contains(pURLsMap[string(*pid)], string(*h.PURL)) {
pURLsMap[string(*pid)] = append(pURLsMap[string(*pid)], string(*h.PURL))
}
}
}
}

// First iterate over full product names.
if names := tree.FullProductNames; names != nil {
for _, name := range *names {
if name != nil && name.ProductID != nil {
add(name.ProductID, name.ProductIdentificationHelper)
}
}
}

// Second traverse the branches recursively.
var recBranch func(*csaf.Branch)
recBranch = func(b *csaf.Branch) {
if p := b.Product; p != nil && p.ProductID != nil {
add(p.ProductID, p.ProductIdentificationHelper)
}
for _, c := range b.Branches {
recBranch(c)
}
}
for _, b := range tree.Branches {
recBranch(b)
}

return pURLsMap
}
95 changes: 95 additions & 0 deletions pkg/vex/cyclonedx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package vex

import (
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/samber/lo"
"go.uber.org/zap"

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

type CycloneDX struct {
sbom *ftypes.CycloneDX
statements []Statement
logger *zap.SugaredLogger
}

type Statement struct {
VulnerabilityID string
Affects []string
Status Status
Justification string // TODO: define a type
}

func newCycloneDX(cdxSBOM *ftypes.CycloneDX, vex *cdx.BOM) *CycloneDX {
var stmts []Statement
for _, vuln := range lo.FromPtr(vex.Vulnerabilities) {
affects := lo.Map(lo.FromPtr(vuln.Affects), func(item cdx.Affects, index int) string {
return item.Ref
})

analysis := lo.FromPtr(vuln.Analysis)
stmts = append(stmts, Statement{
VulnerabilityID: vuln.ID,
Affects: affects,
Status: cdxStatus(analysis.State),
Justification: string(analysis.Justification),
})
}
return &CycloneDX{
sbom: cdxSBOM,
statements: stmts,
logger: log.Logger.With(zap.String("VEX format", "CycloneDX")),
}
}

func cdxStatus(s cdx.ImpactAnalysisState) Status {
switch s {
case cdx.IASResolved, cdx.IASResolvedWithPedigree:
return StatusFixed
case cdx.IASExploitable:
return StatusAffected
case cdx.IASInTriage:
return StatusUnderInvestigation
case cdx.IASFalsePositive, cdx.IASNotAffected:
return StatusNotAffected
}
return StatusUnknown
}

func (v *CycloneDX) 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 {
return true
}
return v.affected(vuln, stmt)
})
}

func (v *CycloneDX) affected(vuln types.DetectedVulnerability, stmt Statement) bool {
for _, affect := range stmt.Affects {
// Affect must be BOM-Link at the moment
link, err := cdx.ParseBOMLink(affect)
if err != nil {
v.logger.Warnw("Unable to parse BOM-Link", zap.String("affect", affect))
continue
}
if v.sbom.SerialNumber != link.SerialNumber() || v.sbom.Version != link.Version() {
v.logger.Warnw("URN doesn't match with SBOM", zap.String("serial number", link.SerialNumber()),
zap.Int("version", link.Version()))
continue
}
if vuln.PkgRef == link.Reference() &&
(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
}
42 changes: 42 additions & 0 deletions pkg/vex/openvex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package vex

import (
openvex "github.com/openvex/go-vex/pkg/vex"
"github.com/samber/lo"
"go.uber.org/zap"

"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/types"
)

type OpenVEX struct {
vex openvex.VEX
logger *zap.SugaredLogger
}

func newOpenVEX(vex openvex.VEX) VEX {
return &OpenVEX{
vex: vex,
logger: log.Logger.With(zap.String("VEX format", "OpenVEX")),
}
}

func (v *OpenVEX) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulnerability {
return lo.Filter(vulns, func(vuln types.DetectedVulnerability, _ int) bool {
stmts := v.vex.Matches(vuln.VulnerabilityID, vuln.PkgRef, nil)
if len(stmts) == 0 {
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
})
}
Loading
Loading