From dea80f860c359a80d199709d456b3de0ba9f7676 Mon Sep 17 00:00:00 2001 From: MaineK00n Date: Tue, 1 Nov 2022 13:58:31 +0900 Subject: [PATCH] feat(report): add cyclonedx format (#1543) --- go.mod | 4 +- go.sum | 5 +- reporter/localfile.go | 48 +++- reporter/sbom/cyclonedx.go | 510 +++++++++++++++++++++++++++++++++++++ subcmds/report.go | 49 ++-- 5 files changed, 585 insertions(+), 31 deletions(-) create mode 100644 reporter/sbom/cyclonedx.go diff --git a/go.mod b/go.mod index 119f0bd6..ae447999 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/Azure/azure-sdk-for-go v66.0.0+incompatible github.com/BurntSushi/toml v1.2.0 + github.com/CycloneDX/cyclonedx-go v0.7.0 github.com/Ullaakut/nmap/v2 v2.1.2-0.20210406060955-59a52fe80a4f github.com/aquasecurity/go-dep-parser v0.0.0-20221024082335-60502daef4ba github.com/aquasecurity/trivy v0.33.0 @@ -17,6 +18,7 @@ require ( github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.14.0 github.com/google/subcommands v1.2.0 + github.com/google/uuid v1.3.0 github.com/gosuri/uitable v0.0.4 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.6.0 @@ -31,6 +33,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/nlopes/slack v0.6.0 github.com/olekukonko/tablewriter v0.0.5 + github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a github.com/parnurzeal/gorequest v0.2.16 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/sirupsen/logrus v1.9.0 @@ -102,7 +105,6 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-containerregistry v0.12.0 // indirect github.com/google/licenseclassifier/v2 v2.0.0-pre6 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect github.com/googleapis/gax-go/v2 v2.5.1 // indirect github.com/googleapis/go-type-adapters v1.0.0 // indirect diff --git a/go.sum b/go.sum index b01cbb7d..cc5c71a9 100644 --- a/go.sum +++ b/go.sum @@ -98,7 +98,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CycloneDX/cyclonedx-go v0.6.0 h1:SizWGbZzFTC/O/1yh072XQBMxfvsoWqd//oKCIyzFyE= +github.com/CycloneDX/cyclonedx-go v0.7.0 h1:jNxp8hL7UpcvPDFXjY+Y1ibFtsW+e5zyF9QoSmhK/zg= +github.com/CycloneDX/cyclonedx-go v0.7.0/go.mod h1:W5Z9w8pTTL+t+yG3PCiFRGlr8PUlE0pGWzKSJbsyXkg= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -192,6 +193,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/c-robinson/iplib v1.0.3 h1:NG0UF0GoEsrC1/vyfX1Lx2Ss7CySWl3KqqXh3q4DdPU= @@ -851,6 +853,7 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/owenrumney/squealer v1.0.1-0.20220510063705-c0be93f0edea h1:RwQ26NYF4vvP7GckFRB4ABL18Byo7vnYBzMpmZKkGwQ= github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a h1:tkTSd1nhioPqi5Whu3CQ79UjPtaGOytqyNnSCVOqzHM= +github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ= github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= diff --git a/reporter/localfile.go b/reporter/localfile.go index 51c9054b..20187081 100644 --- a/reporter/localfile.go +++ b/reporter/localfile.go @@ -2,24 +2,30 @@ package reporter import ( "encoding/json" + "fmt" "os" "path/filepath" - "github.com/future-architect/vuls/models" + "github.com/CycloneDX/cyclonedx-go" "golang.org/x/xerrors" + + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/reporter/sbom" ) // LocalFileWriter writes results to a local file. type LocalFileWriter struct { - CurrentDir string - DiffPlus bool - DiffMinus bool - FormatJSON bool - FormatCsv bool - FormatFullText bool - FormatOneLineText bool - FormatList bool - Gzip bool + CurrentDir string + DiffPlus bool + DiffMinus bool + FormatJSON bool + FormatCsv bool + FormatFullText bool + FormatOneLineText bool + FormatList bool + FormatCycloneDXJSON bool + FormatCycloneDXXML bool + Gzip bool } func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) { @@ -86,6 +92,28 @@ func (w LocalFileWriter) Write(rs ...models.ScanResult) (err error) { } } + if w.FormatCycloneDXJSON { + bs, err := sbom.GenerateCycloneDX(cyclonedx.BOMFileFormatJSON, r) + if err != nil { + return xerrors.Errorf("Failed to generate CycloneDX JSON. err: %w", err) + } + p := fmt.Sprintf("%s_cyclonedx.json", path) + if err := w.writeFile(p, bs, 0600); err != nil { + return xerrors.Errorf("Failed to write CycloneDX JSON. path: %s, err: %w", p, err) + } + } + + if w.FormatCycloneDXXML { + bs, err := sbom.GenerateCycloneDX(cyclonedx.BOMFileFormatXML, r) + if err != nil { + return xerrors.Errorf("Failed to generate CycloneDX XML. err: %w", err) + } + p := fmt.Sprintf("%s_cyclonedx.xml", path) + if err := w.writeFile(p, bs, 0600); err != nil { + return xerrors.Errorf("Failed to write CycloneDX XML. path: %s, err: %w", p, err) + } + } + } return nil } diff --git a/reporter/sbom/cyclonedx.go b/reporter/sbom/cyclonedx.go new file mode 100644 index 00000000..94ff94fe --- /dev/null +++ b/reporter/sbom/cyclonedx.go @@ -0,0 +1,510 @@ +package sbom + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "time" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/google/uuid" + "github.com/package-url/packageurl-go" + "golang.org/x/exp/maps" + "golang.org/x/xerrors" + + "github.com/future-architect/vuls/constant" + "github.com/future-architect/vuls/models" +) + +func GenerateCycloneDX(format cdx.BOMFileFormat, r models.ScanResult) ([]byte, error) { + bom := cdx.NewBOM() + bom.SerialNumber = uuid.New().URN() + bom.Metadata = cdxMetadata(r) + bom.Components, bom.Dependencies, bom.Vulnerabilities = cdxComponents(r, bom.Metadata.Component.BOMRef) + + buf := new(bytes.Buffer) + enc := cdx.NewBOMEncoder(buf, format) + enc.SetPretty(true) + if err := enc.Encode(bom); err != nil { + return nil, xerrors.Errorf("Failed to encode CycloneDX. err: %w", err) + } + return buf.Bytes(), nil +} + +func cdxMetadata(result models.ScanResult) *cdx.Metadata { + metadata := cdx.Metadata{ + Timestamp: result.ReportedAt.Format(time.RFC3339), + Tools: &[]cdx.Tool{ + { + Vendor: "future-architect", + Name: "vuls", + Version: fmt.Sprintf("%s-%s", result.ReportedVersion, result.ReportedRevision), + }, + }, + Component: &cdx.Component{ + BOMRef: uuid.NewString(), + Type: cdx.ComponentTypeOS, + Name: result.ServerName, + }, + } + return &metadata +} + +func cdxComponents(result models.ScanResult, metaBomRef string) (*[]cdx.Component, *[]cdx.Dependency, *[]cdx.Vulnerability) { + var components []cdx.Component + bomRefs := map[string][]string{} + + ospkgToPURL := map[string]string{} + if ospkgComps := ospkgToCdxComponents(result.Family, result.Release, result.RunningKernel, result.Packages, result.SrcPackages, ospkgToPURL); ospkgComps != nil { + bomRefs[metaBomRef] = append(bomRefs[metaBomRef], ospkgComps[0].BOMRef) + for _, comp := range ospkgComps[1:] { + bomRefs[ospkgComps[0].BOMRef] = append(bomRefs[ospkgComps[0].BOMRef], comp.BOMRef) + } + components = append(components, ospkgComps...) + } + + if cpeComps := cpeToCdxComponents(result.ScannedCves); cpeComps != nil { + bomRefs[metaBomRef] = append(bomRefs[metaBomRef], cpeComps[0].BOMRef) + for _, comp := range cpeComps[1:] { + bomRefs[cpeComps[0].BOMRef] = append(bomRefs[cpeComps[0].BOMRef], comp.BOMRef) + } + components = append(components, cpeComps...) + } + + libpkgToPURL := map[string]map[string]string{} + for _, libscanner := range result.LibraryScanners { + libpkgToPURL[libscanner.LockfilePath] = map[string]string{} + + libpkgComps := libpkgToCdxComponents(libscanner, libpkgToPURL) + bomRefs[metaBomRef] = append(bomRefs[metaBomRef], libpkgComps[0].BOMRef) + for _, comp := range libpkgComps[1:] { + bomRefs[libpkgComps[0].BOMRef] = append(bomRefs[libpkgComps[0].BOMRef], comp.BOMRef) + } + components = append(components, libpkgComps...) + } + + wppkgToPURL := map[string]string{} + if wppkgComps := wppkgToCdxComponents(result.WordPressPackages, wppkgToPURL); wppkgComps != nil { + bomRefs[metaBomRef] = append(bomRefs[metaBomRef], wppkgComps[0].BOMRef) + for _, comp := range wppkgComps[1:] { + bomRefs[wppkgComps[0].BOMRef] = append(bomRefs[wppkgComps[0].BOMRef], comp.BOMRef) + } + components = append(components, wppkgComps...) + } + + return &components, cdxDependencies(bomRefs), cdxVulnerabilities(result, ospkgToPURL, libpkgToPURL, wppkgToPURL) +} + +func osToCdxComponent(family, release, runningKernelRelease, runningKernelVersion string) cdx.Component { + props := []cdx.Property{ + { + Name: "future-architect:vuls:Type", + Value: "Package", + }, + } + if runningKernelRelease != "" { + props = append(props, cdx.Property{ + Name: "RunningKernelRelease", + Value: runningKernelRelease, + }) + } + if runningKernelVersion != "" { + props = append(props, cdx.Property{ + Name: "RunningKernelVersion", + Value: runningKernelVersion, + }) + } + return cdx.Component{ + BOMRef: uuid.NewString(), + Type: cdx.ComponentTypeOS, + Name: family, + Version: release, + Properties: &props, + } +} + +func ospkgToCdxComponents(family, release string, runningKernel models.Kernel, binpkgs models.Packages, srcpkgs models.SrcPackages, ospkgToPURL map[string]string) []cdx.Component { + if family == "" { + return nil + } + + components := []cdx.Component{ + osToCdxComponent(family, release, runningKernel.Release, runningKernel.Version), + } + + if len(binpkgs) == 0 { + return components + } + + type srcpkg struct { + name string + version string + arch string + } + binToSrc := map[string]srcpkg{} + for _, pack := range srcpkgs { + for _, binpkg := range pack.BinaryNames { + binToSrc[binpkg] = srcpkg{ + name: pack.Name, + version: pack.Version, + arch: pack.Arch, + } + } + } + + for _, pack := range binpkgs { + var props []cdx.Property + if p, ok := binToSrc[pack.Name]; ok { + if p.name != "" { + props = append(props, cdx.Property{ + Name: "future-architect:vuls:SrcName", + Value: p.name, + }) + } + if p.version != "" { + props = append(props, cdx.Property{ + Name: "future-architect:vuls:SrcVersion", + Value: p.version, + }) + } + if p.arch != "" { + props = append(props, cdx.Property{ + Name: "future-architect:vuls:SrcArch", + Value: p.arch, + }) + } + } + + purl := toPkgPURL(family, release, pack.Name, pack.Version, pack.Release, pack.Arch, pack.Repository) + components = append(components, cdx.Component{ + BOMRef: purl, + Type: cdx.ComponentTypeLibrary, + Name: pack.Name, + Version: pack.Version, + PackageURL: purl, + Properties: &props, + }) + + ospkgToPURL[pack.Name] = purl + } + return components +} + +func cpeToCdxComponents(scannedCves models.VulnInfos) []cdx.Component { + cpes := map[string]struct{}{} + for _, cve := range scannedCves { + for _, cpe := range cve.CpeURIs { + cpes[cpe] = struct{}{} + } + } + if len(cpes) == 0 { + return nil + } + + components := []cdx.Component{ + { + BOMRef: uuid.NewString(), + Type: cdx.ComponentTypeApplication, + Name: "CPEs", + Properties: &[]cdx.Property{ + { + Name: "future-architect:vuls:Type", + Value: "CPE", + }, + }, + }, + } + for cpe := range cpes { + components = append(components, cdx.Component{ + BOMRef: cpe, + Type: cdx.ComponentTypeLibrary, + Name: cpe, + CPE: cpe, + }) + } + + return components +} + +func libpkgToCdxComponents(libscanner models.LibraryScanner, libpkgToPURL map[string]map[string]string) []cdx.Component { + components := []cdx.Component{ + { + BOMRef: uuid.NewString(), + Type: cdx.ComponentTypeApplication, + Name: libscanner.LockfilePath, + Properties: &[]cdx.Property{ + { + Name: "future-architect:vuls:Type", + Value: libscanner.Type, + }, + }, + }, + } + + for _, lib := range libscanner.Libs { + purl := packageurl.NewPackageURL(libscanner.Type, "", lib.Name, lib.Version, packageurl.Qualifiers{{Key: "file_path", Value: libscanner.LockfilePath}}, "").ToString() + components = append(components, cdx.Component{ + BOMRef: purl, + Type: cdx.ComponentTypeLibrary, + Name: lib.Name, + Version: lib.Version, + PackageURL: purl, + }) + + libpkgToPURL[libscanner.LockfilePath][lib.Name] = purl + } + + return components +} + +func wppkgToCdxComponents(wppkgs models.WordPressPackages, wppkgToPURL map[string]string) []cdx.Component { + if len(wppkgs) == 0 { + return nil + } + + components := []cdx.Component{ + { + BOMRef: uuid.NewString(), + Type: cdx.ComponentTypeApplication, + Name: "wordpress", + Properties: &[]cdx.Property{ + { + Name: "future-architect:vuls:Type", + Value: "WordPress", + }, + }, + }, + } + + for _, wppkg := range wppkgs { + purl := packageurl.NewPackageURL("wordpress", wppkg.Type, wppkg.Name, wppkg.Version, packageurl.Qualifiers{{Key: "status", Value: wppkg.Status}}, "").ToString() + components = append(components, cdx.Component{ + BOMRef: purl, + Type: cdx.ComponentTypeLibrary, + Name: wppkg.Name, + Version: wppkg.Version, + PackageURL: purl, + }) + + wppkgToPURL[wppkg.Name] = purl + } + + return components +} + +func cdxDependencies(bomRefs map[string][]string) *[]cdx.Dependency { + dependencies := make([]cdx.Dependency, 0, len(bomRefs)) + for ref, depRefs := range bomRefs { + ds := depRefs + dependencies = append(dependencies, cdx.Dependency{ + Ref: ref, + Dependencies: &ds, + }) + } + return &dependencies +} + +func toPkgPURL(osFamily, osVersion, packName, packVersion, packRelease, packArch, packRepository string) string { + var purlType string + switch osFamily { + case constant.Alma, constant.Amazon, constant.CentOS, constant.Fedora, constant.OpenSUSE, constant.OpenSUSELeap, constant.Oracle, constant.RedHat, constant.Rocky, constant.SUSEEnterpriseDesktop, constant.SUSEEnterpriseServer: + purlType = "rpm" + case constant.Alpine: + purlType = "apk" + case constant.Debian, constant.Raspbian, constant.Ubuntu: + purlType = "deb" + case constant.FreeBSD: + purlType = "pkg" + case constant.Windows: + purlType = "win" + case constant.ServerTypePseudo: + purlType = "pseudo" + default: + purlType = "unknown" + } + + version := packVersion + if packRelease != "" { + version = fmt.Sprintf("%s-%s", packVersion, packRelease) + } + + var qualifiers packageurl.Qualifiers + if osVersion != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "distro", + Value: osVersion, + }) + } + if packArch != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "arch", + Value: packArch, + }) + } + if packRepository != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "repo", + Value: packRepository, + }) + } + + return packageurl.NewPackageURL(purlType, osFamily, packName, version, qualifiers, "").ToString() +} + +func cdxVulnerabilities(result models.ScanResult, ospkgToPURL map[string]string, libpkgToPURL map[string]map[string]string, wppkgToPURL map[string]string) *[]cdx.Vulnerability { + vulnerabilities := make([]cdx.Vulnerability, 0, len(result.ScannedCves)) + for _, cve := range result.ScannedCves { + vulnerabilities = append(vulnerabilities, cdx.Vulnerability{ + ID: cve.CveID, + Ratings: cdxRatings(cve.CveContents), + CWEs: cdxCWEs(cve.CveContents), + Description: cdxDescription(cve.CveContents), + Advisories: cdxAdvisories(cve.CveContents), + Affects: cdxAffects(cve, ospkgToPURL, libpkgToPURL, wppkgToPURL), + }) + } + return &vulnerabilities +} + +func cdxRatings(cveContents models.CveContents) *[]cdx.VulnerabilityRating { + var ratings []cdx.VulnerabilityRating + for _, contents := range cveContents { + for _, content := range contents { + if content.Cvss2Score != 0 || content.Cvss2Vector != "" || content.Cvss2Severity != "" { + ratings = append(ratings, cdxCVSS2Rating(string(content.Type), content.Cvss2Vector, content.Cvss2Score, content.Cvss2Severity)) + } + if content.Cvss3Score != 0 || content.Cvss3Vector != "" || content.Cvss3Severity != "" { + ratings = append(ratings, cdxCVSS3Rating(string(content.Type), content.Cvss3Vector, content.Cvss3Score, content.Cvss3Severity)) + } + } + } + return &ratings +} + +func cdxCVSS2Rating(source, vector string, score float64, severity string) cdx.VulnerabilityRating { + r := cdx.VulnerabilityRating{ + Source: &cdx.Source{Name: source}, + Method: cdx.ScoringMethodCVSSv2, + Vector: vector, + } + if score != 0 { + r.Score = &score + } + switch strings.ToLower(severity) { + case "high": + r.Severity = cdx.SeverityHigh + case "medium": + r.Severity = cdx.SeverityMedium + case "low": + r.Severity = cdx.SeverityLow + default: + r.Severity = cdx.SeverityUnknown + } + return r +} + +func cdxCVSS3Rating(source, vector string, score float64, severity string) cdx.VulnerabilityRating { + r := cdx.VulnerabilityRating{ + Source: &cdx.Source{Name: source}, + Method: cdx.ScoringMethodCVSSv3, + Vector: vector, + } + if strings.HasPrefix(vector, "CVSS:3.1") { + r.Method = cdx.ScoringMethodCVSSv31 + } + if score != 0 { + r.Score = &score + } + switch strings.ToLower(severity) { + case "critical": + r.Severity = cdx.SeverityCritical + case "high": + r.Severity = cdx.SeverityHigh + case "medium": + r.Severity = cdx.SeverityMedium + case "low": + r.Severity = cdx.SeverityLow + case "none": + r.Severity = cdx.SeverityNone + default: + r.Severity = cdx.SeverityUnknown + } + return r +} + +func cdxAffects(cve models.VulnInfo, ospkgToPURL map[string]string, libpkgToPURL map[string]map[string]string, wppkgToPURL map[string]string) *[]cdx.Affects { + affects := make([]cdx.Affects, 0, len(cve.AffectedPackages)+len(cve.CpeURIs)+len(cve.LibraryFixedIns)+len(cve.WpPackageFixStats)) + + for _, p := range cve.AffectedPackages { + affects = append(affects, cdx.Affects{ + Ref: ospkgToPURL[p.Name], + }) + } + for _, cpe := range cve.CpeURIs { + affects = append(affects, cdx.Affects{ + Ref: cpe, + }) + } + for _, lib := range cve.LibraryFixedIns { + affects = append(affects, cdx.Affects{ + Ref: libpkgToPURL[lib.Path][lib.Name], + }) + + } + for _, wppack := range cve.WpPackageFixStats { + affects = append(affects, cdx.Affects{ + Ref: wppkgToPURL[wppack.Name], + }) + } + + return &affects +} + +func cdxCWEs(cveContents models.CveContents) *[]int { + m := map[int]struct{}{} + for _, contents := range cveContents { + for _, content := range contents { + for _, cweID := range content.CweIDs { + if !strings.HasPrefix(cweID, "CWE-") { + continue + } + i, err := strconv.Atoi(strings.TrimPrefix(cweID, "CWE-")) + if err != nil { + continue + } + m[i] = struct{}{} + } + } + } + cweIDs := maps.Keys(m) + return &cweIDs +} + +func cdxDescription(cveContents models.CveContents) string { + if contents, ok := cveContents[models.Nvd]; ok { + return contents[0].Summary + } + return "" +} + +func cdxAdvisories(cveContents models.CveContents) *[]cdx.Advisory { + urls := map[string]struct{}{} + for _, contents := range cveContents { + for _, content := range contents { + if content.SourceLink != "" { + urls[content.SourceLink] = struct{}{} + } + for _, r := range content.References { + urls[r.Link] = struct{}{} + } + } + } + advisories := make([]cdx.Advisory, 0, len(urls)) + for u := range urls { + advisories = append(advisories, cdx.Advisory{ + URL: u, + }) + } + return &advisories +} diff --git a/subcmds/report.go b/subcmds/report.go index 828bd105..cc6d7976 100644 --- a/subcmds/report.go +++ b/subcmds/report.go @@ -10,26 +10,29 @@ import ( "path/filepath" "github.com/aquasecurity/trivy/pkg/utils" + "github.com/google/subcommands" + "github.com/k0kubun/pp" + "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/detector" "github.com/future-architect/vuls/logging" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/reporter" - "github.com/google/subcommands" - "github.com/k0kubun/pp" ) // ReportCmd is subcommand for reporting type ReportCmd struct { configPath string - formatJSON bool - formatOneEMail bool - formatCsv bool - formatFullText bool - formatOneLineText bool - formatList bool - gzip bool + formatJSON bool + formatOneEMail bool + formatCsv bool + formatFullText bool + formatOneLineText bool + formatList bool + formatCycloneDXJSON bool + formatCycloneDXXML bool + gzip bool toSlack bool toChatWork bool @@ -80,6 +83,9 @@ func (*ReportCmd) Usage() string { [-format-one-line-text] [-format-list] [-format-full-text] + [-format-csv] + [-format-cyclonedx-json] + [-format-cyclonedx-xml] [-gzip] [-http-proxy=http://192.168.0.1:8080] [-debug] @@ -150,6 +156,8 @@ func (p *ReportCmd) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.formatList, "format-list", false, "Display as list format") f.BoolVar(&p.formatFullText, "format-full-text", false, "Detail report in plain text") + f.BoolVar(&p.formatCycloneDXJSON, "format-cyclonedx-json", false, "CycloneDX JSON format") + f.BoolVar(&p.formatCycloneDXXML, "format-cyclonedx-xml", false, "CycloneDX XML format") f.BoolVar(&p.toSlack, "to-slack", false, "Send report via Slack") f.BoolVar(&p.toChatWork, "to-chatwork", false, "Send report via chatwork") @@ -225,7 +233,8 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} } if !(p.formatJSON || p.formatOneLineText || - p.formatList || p.formatFullText || p.formatCsv) { + p.formatList || p.formatFullText || p.formatCsv || + p.formatCycloneDXJSON || p.formatCycloneDXXML) { p.formatList = true } @@ -310,15 +319,17 @@ func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{} if p.toLocalFile { reports = append(reports, reporter.LocalFileWriter{ - CurrentDir: dir, - DiffPlus: config.Conf.DiffPlus, - DiffMinus: config.Conf.DiffMinus, - FormatJSON: p.formatJSON, - FormatCsv: p.formatCsv, - FormatFullText: p.formatFullText, - FormatOneLineText: p.formatOneLineText, - FormatList: p.formatList, - Gzip: p.gzip, + CurrentDir: dir, + DiffPlus: config.Conf.DiffPlus, + DiffMinus: config.Conf.DiffMinus, + FormatJSON: p.formatJSON, + FormatCsv: p.formatCsv, + FormatFullText: p.formatFullText, + FormatOneLineText: p.formatOneLineText, + FormatList: p.formatList, + FormatCycloneDXJSON: p.formatCycloneDXJSON, + FormatCycloneDXXML: p.formatCycloneDXXML, + Gzip: p.gzip, }) }