@@ -2,16 +2,23 @@ package cyclonedx
22
33import (
44 "io"
5+ "sort"
56 "strconv"
7+ "strings"
68 "time"
79
810 cdx "github.com/CycloneDX/cyclonedx-go"
911 "github.com/google/uuid"
12+ "golang.org/x/exp/maps"
1013 "golang.org/x/xerrors"
1114 "k8s.io/utils/clock"
1215
1316 ftypes "github.com/aquasecurity/fanal/types"
17+ dtypes "github.com/aquasecurity/trivy-db/pkg/types"
18+ "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
19+ "github.com/aquasecurity/trivy/pkg/log"
1420 "github.com/aquasecurity/trivy/pkg/purl"
21+ "github.com/aquasecurity/trivy/pkg/scanner/utils"
1522 "github.com/aquasecurity/trivy/pkg/types"
1623)
1724
@@ -127,25 +134,30 @@ func (cw *Writer) convertToBom(r types.Report, version string) (*cdx.BOM, error)
127134 Component : metadataComponent ,
128135 }
129136
130- bom .Components , bom .Dependencies , err = cw .parseComponents (r , bom .Metadata .Component .BOMRef )
137+ bom .Components , bom .Dependencies , bom . Vulnerabilities , err = cw .parseComponents (r , bom .Metadata .Component .BOMRef )
131138 if err != nil {
132139 return nil , xerrors .Errorf ("failed to parse components: %w" , err )
133140 }
134141
135142 return bom , nil
136143}
137144
138- func (cw * Writer ) parseComponents (r types.Report , bomRef string ) (* []cdx.Component , * []cdx.Dependency , error ) {
145+ func (cw * Writer ) parseComponents (r types.Report , bomRef string ) (* []cdx.Component , * []cdx.Dependency , * []cdx. Vulnerability , error ) {
139146 var components []cdx.Component
140147 var dependencies []cdx.Dependency
141148 var metadataDependencies []cdx.Dependency
142149 libraryUniqMap := map [string ]struct {}{}
150+ vulnMap := map [string ]cdx.Vulnerability {}
143151 for _ , result := range r .Results {
144152 var componentDependencies []cdx.Dependency
153+ bomRefMap := map [string ]string {}
145154 for _ , pkg := range result .Packages {
146155 pkgComponent , err := cw .pkgToComponent (result .Type , r .Metadata , pkg )
147156 if err != nil {
148- return nil , nil , xerrors .Errorf ("failed to parse pkg: %w" , err )
157+ return nil , nil , nil , xerrors .Errorf ("failed to parse pkg: %w" , err )
158+ }
159+ if _ , ok := bomRefMap [pkg .Name + utils .FormatVersion (pkg )+ pkg .FilePath ]; ! ok {
160+ bomRefMap [pkg .Name + utils .FormatVersion (pkg )+ pkg .FilePath ] = pkgComponent .BOMRef
149161 }
150162
151163 // When multiple lock files have the same dependency with the same name and version,
@@ -171,6 +183,20 @@ func (cw *Writer) parseComponents(r types.Report, bomRef string) (*[]cdx.Compone
171183
172184 componentDependencies = append (componentDependencies , cdx.Dependency {Ref : pkgComponent .BOMRef })
173185 }
186+ for _ , vuln := range result .Vulnerabilities {
187+ // Take a bom-ref
188+ ref := bomRefMap [vuln .PkgName + vuln .InstalledVersion + vuln .PkgPath ]
189+ if v , ok := vulnMap [vuln .VulnerabilityID ]; ok {
190+ // If a vulnerability depends on multiple packages,
191+ // it will be commonised into a single vulnerability.
192+ // Vulnerability component (CVE-2020-26247)
193+ // -> Library component (nokogiri /srv/app1/vendor/bundle/ruby/3.0.0/specifications/nokogiri-1.10.0.gemspec)
194+ // -> Library component (nokogiri /srv/app2/vendor/bundle/ruby/3.0.0/specifications/nokogiri-1.10.0.gemspec)
195+ * v .Affects = append (* v .Affects , affects (ref , vuln .InstalledVersion ))
196+ } else {
197+ vulnMap [vuln .VulnerabilityID ] = cw .vulnerability (vuln , ref )
198+ }
199+ }
174200
175201 if result .Type == ftypes .NodePkg || result .Type == ftypes .PythonPkg || result .Type == ftypes .GoBinary ||
176202 result .Type == ftypes .GemSpec || result .Type == ftypes .Jar {
@@ -215,11 +241,36 @@ func (cw *Writer) parseComponents(r types.Report, bomRef string) (*[]cdx.Compone
215241 metadataDependencies = append (metadataDependencies , cdx.Dependency {Ref : resultComponent .BOMRef })
216242 }
217243 }
244+ vulns := maps .Values (vulnMap )
245+ sort .Slice (vulns , func (i , j int ) bool {
246+ return vulns [i ].ID > vulns [j ].ID
247+ })
218248
219249 dependencies = append (dependencies ,
220250 cdx.Dependency {Ref : bomRef , Dependencies : & metadataDependencies },
221251 )
222- return & components , & dependencies , nil
252+ return & components , & dependencies , & vulns , nil
253+ }
254+
255+ func (cw * Writer ) vulnerability (vuln types.DetectedVulnerability , bomRef string ) cdx.Vulnerability {
256+ v := cdx.Vulnerability {
257+ ID : vuln .VulnerabilityID ,
258+ Source : source (vuln .DataSource ),
259+ Ratings : ratings (vuln ),
260+ CWEs : cwes (vuln .CweIDs ),
261+ Description : vuln .Description ,
262+ Advisories : advisories (vuln .References ),
263+ }
264+ if vuln .PublishedDate != nil {
265+ v .Published = vuln .PublishedDate .String ()
266+ }
267+ if vuln .LastModifiedDate != nil {
268+ v .Updated = vuln .LastModifiedDate .String ()
269+ }
270+
271+ v .Affects = & []cdx.Affects {affects (bomRef , vuln .InstalledVersion )}
272+
273+ return v
223274}
224275
225276func (cw * Writer ) pkgToComponent (t string , meta types.Metadata , pkg ftypes.Package ) (cdx.Component , error ) {
@@ -363,3 +414,148 @@ func property(key, value string) cdx.Property {
363414 Value : value ,
364415 }
365416}
417+
418+ func advisories (refs []string ) * []cdx.Advisory {
419+ var advs []cdx.Advisory
420+ for _ , ref := range refs {
421+ advs = append (advs , cdx.Advisory {
422+ URL : ref ,
423+ })
424+ }
425+ return & advs
426+ }
427+
428+ func cwes (cweIDs []string ) * []int {
429+ var ret []int
430+ for _ , cweID := range cweIDs {
431+ number , err := strconv .Atoi (strings .TrimPrefix (strings .ToLower (cweID ), "cwe-" ))
432+ if err != nil {
433+ log .Logger .Debugf ("cwe id parse error: %s" , err )
434+ continue
435+ }
436+ ret = append (ret , number )
437+ }
438+ return & ret
439+ }
440+
441+ func ratings (vulnerability types.DetectedVulnerability ) * []cdx.VulnerabilityRating {
442+ var rates []cdx.VulnerabilityRating
443+ for sourceID , severity := range vulnerability .VendorSeverity {
444+ // When the vendor also provides CVSS score/vector
445+ if cvss , ok := vulnerability .CVSS [sourceID ]; ok {
446+ if cvss .V2Score != 0 || cvss .V2Vector != "" {
447+ rates = append (rates , ratingV2 (sourceID , severity , cvss ))
448+ }
449+ if cvss .V3Score != 0 || cvss .V3Vector != "" {
450+ rates = append (rates , ratingV3 (sourceID , severity , cvss ))
451+ }
452+ } else { // When the vendor provides only severity
453+ rate := cdx.VulnerabilityRating {
454+ Source : & cdx.Source {
455+ Name : string (sourceID ),
456+ },
457+ Severity : toCDXSeverity (severity ),
458+ }
459+ rates = append (rates , rate )
460+ }
461+ }
462+
463+ // For consistency
464+ sort .Slice (rates , func (i , j int ) bool {
465+ if rates [i ].Source .Name != rates [j ].Source .Name {
466+ return rates [i ].Source .Name < rates [j ].Source .Name
467+ }
468+ if rates [i ].Method != rates [j ].Method {
469+ return rates [i ].Method < rates [j ].Method
470+ }
471+ return rates [i ].Score < rates [j ].Score
472+ })
473+ return & rates
474+ }
475+
476+ func ratingV2 (sourceID dtypes.SourceID , severity dtypes.Severity , cvss dtypes.CVSS ) cdx.VulnerabilityRating {
477+ cdxSeverity := toCDXSeverity (severity )
478+
479+ // Trivy keeps only CVSSv3 severity for NVD.
480+ // The CVSSv2 severity must be calculated according to CVSSv2 score.
481+ if sourceID == vulnerability .NVD {
482+ cdxSeverity = nvdSeverityV2 (cvss .V2Score )
483+ }
484+ return cdx.VulnerabilityRating {
485+ Source : & cdx.Source {
486+ Name : string (sourceID ),
487+ },
488+ Score : cvss .V2Score ,
489+ Method : cdx .ScoringMethodCVSSv2 ,
490+ Severity : cdxSeverity ,
491+ Vector : cvss .V2Vector ,
492+ }
493+ }
494+
495+ func nvdSeverityV2 (score float64 ) cdx.Severity {
496+ // cf. https://nvd.nist.gov/vuln-metrics/cvss
497+ switch {
498+ case score < 4.0 :
499+ return cdx .SeverityInfo
500+ case 4.0 <= score && score < 7.0 :
501+ return cdx .SeverityMedium
502+ case 7.0 <= score :
503+ return cdx .SeverityHigh
504+ }
505+ return cdx .SeverityUnknown
506+ }
507+
508+ func ratingV3 (sourceID dtypes.SourceID , severity dtypes.Severity , cvss dtypes.CVSS ) cdx.VulnerabilityRating {
509+ rate := cdx.VulnerabilityRating {
510+ Source : & cdx.Source {
511+ Name : string (sourceID ),
512+ },
513+ Score : cvss .V3Score ,
514+ Method : cdx .ScoringMethodCVSSv3 ,
515+ Severity : toCDXSeverity (severity ),
516+ Vector : cvss .V3Vector ,
517+ }
518+ if strings .HasPrefix (cvss .V3Vector , "CVSS:3.1" ) {
519+ rate .Method = cdx .ScoringMethodCVSSv31
520+ }
521+ return rate
522+ }
523+
524+ func toCDXSeverity (s dtypes.Severity ) cdx.Severity {
525+ switch s {
526+ case dtypes .SeverityLow :
527+ return cdx .SeverityLow
528+ case dtypes .SeverityMedium :
529+ return cdx .SeverityMedium
530+ case dtypes .SeverityHigh :
531+ return cdx .SeverityHigh
532+ case dtypes .SeverityCritical :
533+ return cdx .SeverityCritical
534+ default :
535+ return cdx .SeverityUnknown
536+ }
537+ }
538+
539+ func source (source * dtypes.DataSource ) * cdx.Source {
540+ if source == nil {
541+ return nil
542+ }
543+
544+ return & cdx.Source {
545+ Name : string (source .ID ),
546+ URL : source .URL ,
547+ }
548+ }
549+
550+ func affects (ref , version string ) cdx.Affects {
551+ return cdx.Affects {
552+ Ref : ref ,
553+ Range : & []cdx.AffectedVersions {
554+ {
555+ Version : version ,
556+ Status : cdx .VulnerabilityStatusAffected ,
557+ // "AffectedVersions.Range" is not included, because it does not exist in DetectedVulnerability.
558+ },
559+ },
560+ }
561+ }
0 commit comments