github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/vex/vex.go (about)

     1  package vex
     2  
     3  import (
     4  	"encoding/json"
     5  	"io"
     6  	"os"
     7  
     8  	cdx "github.com/CycloneDX/cyclonedx-go"
     9  	"github.com/hashicorp/go-multierror"
    10  	openvex "github.com/openvex/go-vex/pkg/vex"
    11  	"github.com/samber/lo"
    12  	"github.com/sirupsen/logrus"
    13  	"go.uber.org/zap"
    14  	"golang.org/x/xerrors"
    15  
    16  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    17  	"github.com/devseccon/trivy/pkg/log"
    18  	"github.com/devseccon/trivy/pkg/sbom"
    19  	"github.com/devseccon/trivy/pkg/sbom/cyclonedx"
    20  	"github.com/devseccon/trivy/pkg/types"
    21  )
    22  
    23  // VEX represents Vulnerability Exploitability eXchange. It abstracts multiple VEX formats.
    24  // Note: This is in the experimental stage and does not yet support many specifications.
    25  // The implementation may change significantly.
    26  type VEX interface {
    27  	Filter([]types.DetectedVulnerability) []types.DetectedVulnerability
    28  }
    29  
    30  type Statement struct {
    31  	VulnerabilityID string
    32  	Affects         []string
    33  	Status          Status
    34  	Justification   string // TODO: define a type
    35  }
    36  
    37  type OpenVEX struct {
    38  	vex    openvex.VEX
    39  	logger *zap.SugaredLogger
    40  }
    41  
    42  func newOpenVEX(vex openvex.VEX) VEX {
    43  	logger := log.Logger.With(zap.String("VEX format", "OpenVEX"))
    44  
    45  	return &OpenVEX{
    46  		vex:    vex,
    47  		logger: logger,
    48  	}
    49  }
    50  
    51  func (v *OpenVEX) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulnerability {
    52  	return lo.Filter(vulns, func(vuln types.DetectedVulnerability, _ int) bool {
    53  		stmts := v.vex.Matches(vuln.VulnerabilityID, vuln.PkgRef, nil)
    54  		if len(stmts) == 0 {
    55  			return true
    56  		}
    57  
    58  		// Take the latest statement for a given vulnerability and product
    59  		// as a sequence of statements can be overridden by the newer one.
    60  		// cf. https://github.com/openvex/spec/blob/fa5ba0c0afedb008dc5ebad418548cacf16a3ca7/OPENVEX-SPEC.md#the-vex-statement
    61  		stmt := stmts[len(stmts)-1]
    62  		if stmt.Status == openvex.StatusNotAffected || stmt.Status == openvex.StatusFixed {
    63  			v.logger.Infow("Filtered out the detected vulnerability", zap.String("vulnerability-id", vuln.VulnerabilityID),
    64  				zap.String("status", string(stmt.Status)), zap.String("justification", string(stmt.Justification)))
    65  			return false
    66  		}
    67  		return true
    68  	})
    69  }
    70  
    71  type CycloneDX struct {
    72  	sbom       *ftypes.CycloneDX
    73  	statements []Statement
    74  	logger     *zap.SugaredLogger
    75  }
    76  
    77  func newCycloneDX(cdxSBOM *ftypes.CycloneDX, vex *cdx.BOM) *CycloneDX {
    78  	var stmts []Statement
    79  	for _, vuln := range lo.FromPtr(vex.Vulnerabilities) {
    80  		affects := lo.Map(lo.FromPtr(vuln.Affects), func(item cdx.Affects, index int) string {
    81  			return item.Ref
    82  		})
    83  
    84  		analysis := lo.FromPtr(vuln.Analysis)
    85  
    86  		stmts = append(stmts, Statement{
    87  			VulnerabilityID: vuln.ID,
    88  			Affects:         affects,
    89  			Status:          cdxStatus(analysis.State),
    90  			Justification:   string(analysis.Justification),
    91  		})
    92  	}
    93  	return &CycloneDX{
    94  		sbom:       cdxSBOM,
    95  		statements: stmts,
    96  		logger:     log.Logger.With(zap.String("VEX format", "CycloneDX")),
    97  	}
    98  }
    99  
   100  func (v *CycloneDX) Filter(vulns []types.DetectedVulnerability) []types.DetectedVulnerability {
   101  	return lo.Filter(vulns, func(vuln types.DetectedVulnerability, _ int) bool {
   102  		stmt, ok := lo.Find(v.statements, func(item Statement) bool {
   103  			return item.VulnerabilityID == vuln.VulnerabilityID
   104  		})
   105  		if !ok {
   106  			return true
   107  		}
   108  		return v.affected(vuln, stmt)
   109  	})
   110  }
   111  
   112  func (v *CycloneDX) affected(vuln types.DetectedVulnerability, stmt Statement) bool {
   113  	for _, affect := range stmt.Affects {
   114  		// Affect must be BOM-Link at the moment
   115  		link, err := cdx.ParseBOMLink(affect)
   116  		if err != nil {
   117  			v.logger.Warnw("Unable to parse BOM-Link", zap.String("affect", affect))
   118  			continue
   119  		}
   120  		if v.sbom.SerialNumber != link.SerialNumber() || v.sbom.Version != link.Version() {
   121  			v.logger.Warnw("URN doesn't match with SBOM", zap.String("serial number", link.SerialNumber()),
   122  				zap.Int("version", link.Version()))
   123  			continue
   124  		}
   125  		if vuln.PkgRef == link.Reference() &&
   126  			(stmt.Status == StatusNotAffected || stmt.Status == StatusFixed) {
   127  			v.logger.Infow("Filtered out the detected vulnerability", zap.String("vulnerability-id", vuln.VulnerabilityID),
   128  				zap.String("status", string(stmt.Status)), zap.String("justification", stmt.Justification))
   129  			return false
   130  		}
   131  	}
   132  	return true
   133  }
   134  
   135  func cdxStatus(s cdx.ImpactAnalysisState) Status {
   136  	switch s {
   137  	case cdx.IASResolved, cdx.IASResolvedWithPedigree:
   138  		return StatusFixed
   139  	case cdx.IASExploitable:
   140  		return StatusAffected
   141  	case cdx.IASInTriage:
   142  		return StatusUnderInvestigation
   143  	case cdx.IASFalsePositive, cdx.IASNotAffected:
   144  		return StatusNotAffected
   145  	}
   146  	return StatusUnknown
   147  }
   148  
   149  func New(filePath string, report types.Report) (VEX, error) {
   150  	if filePath == "" {
   151  		return nil, nil
   152  	}
   153  	f, err := os.Open(filePath)
   154  	if err != nil {
   155  		return nil, xerrors.Errorf("file open error: %w", err)
   156  	}
   157  	defer f.Close()
   158  
   159  	var errs error
   160  
   161  	// Try CycloneDX JSON
   162  	if ok, err := sbom.IsCycloneDXJSON(f); err != nil {
   163  		errs = multierror.Append(errs, err)
   164  	} else if ok {
   165  		return decodeCycloneDXJSON(f, report)
   166  	}
   167  
   168  	// Try OpenVEX
   169  	if v, err := decodeOpenVEX(f); err != nil {
   170  		errs = multierror.Append(errs, err)
   171  	} else {
   172  		return v, nil
   173  	}
   174  
   175  	return nil, xerrors.Errorf("unable to load VEX: %w", errs)
   176  }
   177  
   178  func decodeCycloneDXJSON(r io.ReadSeeker, report types.Report) (VEX, error) {
   179  	if _, err := r.Seek(0, io.SeekStart); err != nil {
   180  		return nil, xerrors.Errorf("seek error: %w", err)
   181  	}
   182  	vex, err := cyclonedx.DecodeJSON(r)
   183  	if err != nil {
   184  		return nil, xerrors.Errorf("json decode error: %w", err)
   185  	}
   186  	if report.CycloneDX == nil {
   187  		return nil, xerrors.New("CycloneDX VEX can be used with CycloneDX SBOM")
   188  	}
   189  	return newCycloneDX(report.CycloneDX, vex), nil
   190  }
   191  
   192  func decodeOpenVEX(r io.ReadSeeker) (VEX, error) {
   193  	// openvex/go-vex outputs log messages by default
   194  	logrus.SetOutput(io.Discard)
   195  
   196  	if _, err := r.Seek(0, io.SeekStart); err != nil {
   197  		return nil, xerrors.Errorf("seek error: %w", err)
   198  	}
   199  	var openVEX openvex.VEX
   200  	if err := json.NewDecoder(r).Decode(&openVEX); err != nil {
   201  		return nil, err
   202  	}
   203  	if openVEX.Context == "" {
   204  		return nil, nil
   205  	}
   206  	return newOpenVEX(openVEX), nil
   207  }