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 }