From d8173cdd422ec9f7dfc6a43f75e905dca151a6d9 Mon Sep 17 00:00:00 2001 From: MaineK00n Date: Sat, 29 Jun 2024 16:35:06 +0900 Subject: [PATCH] feat(cve/mitre): support go-cve-dictionary:mitre (#1978) * feat(cve/mitre): support go-cve-dictionary:mitre * chore: adopt reviewer comment * refactor(models): refactor CveContents method --- detector/detector.go | 21 +- detector/util.go | 2 +- go.mod | 4 +- go.sum | 8 +- models/cvecontents.go | 319 +++++++++++++------------- models/cvecontents_test.go | 452 +++++++++++++++++++++++++++++++++++-- models/utils.go | 120 ++++++++++ models/vulninfos.go | 74 ++++-- models/vulninfos_test.go | 150 ++++++++++++ reporter/sbom/cyclonedx.go | 29 +++ reporter/slack.go | 4 +- reporter/syslog.go | 5 + reporter/util.go | 12 +- server/server.go | 2 +- tui/tui.go | 15 +- 15 files changed, 1005 insertions(+), 212 deletions(-) diff --git a/detector/detector.go b/detector/detector.go index df693c0f7b..a2f8aa3c24 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -204,7 +204,7 @@ func Detect(rs []models.ScanResult, dir string) ([]models.ScanResult, error) { return nil, xerrors.Errorf("Failed to fill with gost: %w", err) } - if err := FillCvesWithNvdJvnFortinet(&r, config.Conf.CveDict, config.Conf.LogOpts); err != nil { + if err := FillCvesWithGoCVEDictionary(&r, config.Conf.CveDict, config.Conf.LogOpts); err != nil { return nil, xerrors.Errorf("Failed to fill with CVE: %w", err) } @@ -435,8 +435,8 @@ func DetectWordPressCves(r *models.ScanResult, wpCnf config.WpScanConf) error { return nil } -// FillCvesWithNvdJvnFortinet fills CVE detail with NVD, JVN, Fortinet -func FillCvesWithNvdJvnFortinet(r *models.ScanResult, cnf config.GoCveDictConf, logOpts logging.LogOpts) (err error) { +// FillCvesWithGoCVEDictionary fills CVE detail with NVD, JVN, Fortinet, MITRE +func FillCvesWithGoCVEDictionary(r *models.ScanResult, cnf config.GoCveDictConf, logOpts logging.LogOpts) (err error) { cveIDs := []string{} for _, v := range r.ScannedCves { cveIDs = append(cveIDs, v.CveID) @@ -461,6 +461,7 @@ func FillCvesWithNvdJvnFortinet(r *models.ScanResult, cnf config.GoCveDictConf, nvds, exploits, mitigations := models.ConvertNvdToModel(d.CveID, d.Nvds) jvns := models.ConvertJvnToModel(d.CveID, d.Jvns) fortinets := models.ConvertFortinetToModel(d.CveID, d.Fortinets) + mitres := models.ConvertMitreToModel(d.CveID, d.Mitres) alerts := fillCertAlerts(&d) for cveID, vinfo := range r.ScannedCves { @@ -475,18 +476,16 @@ func FillCvesWithNvdJvnFortinet(r *models.ScanResult, cnf config.GoCveDictConf, } for _, con := range append(jvns, fortinets...) { if !con.Empty() { - found := false - for _, cveCont := range vinfo.CveContents[con.Type] { - if con.SourceLink == cveCont.SourceLink { - found = true - break - } - } - if !found { + if !slices.ContainsFunc(vinfo.CveContents[con.Type], func(e models.CveContent) bool { + return con.SourceLink == e.SourceLink + }) { vinfo.CveContents[con.Type] = append(vinfo.CveContents[con.Type], con) } } } + for _, con := range mitres { + vinfo.CveContents[con.Type] = append(vinfo.CveContents[con.Type], con) + } vinfo.AlertDict = alerts vinfo.Exploits = append(vinfo.Exploits, exploits...) vinfo.Mitigations = append(vinfo.Mitigations, mitigations...) diff --git a/detector/util.go b/detector/util.go index c00c9b57c0..cee10e3bbd 100644 --- a/detector/util.go +++ b/detector/util.go @@ -181,7 +181,7 @@ func getMinusDiffCves(previous, current models.ScanResult) models.VulnInfos { } func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { - cTypes := append([]models.CveContentType{models.Nvd, models.Jvn}, models.GetCveContentTypes(current.Family)...) + cTypes := append([]models.CveContentType{models.Mitre, models.Nvd, models.Jvn}, models.GetCveContentTypes(current.Family)...) prevLastModified := map[models.CveContentType][]time.Time{} preVinfo, ok := previous.ScannedCves[cveID] diff --git a/go.mod b/go.mod index 286bf3922a..1c6b36b9e3 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/vulsio/go-cti v0.0.5-0.20240318121747-822b3ef289cb - github.com/vulsio/go-cve-dictionary v0.10.2-0.20240319004433-af03be313b77 + github.com/vulsio/go-cve-dictionary v0.10.2-0.20240628072614-73f15707be8e github.com/vulsio/go-exploitdb v0.4.7-0.20240318122115-ccb3abc151a1 github.com/vulsio/go-kev v0.1.4-0.20240318121733-b3386e67d3fb github.com/vulsio/go-msfdb v0.2.4-0.20240318121704-8bfc812656dc @@ -97,7 +97,7 @@ require ( github.com/Microsoft/hcsshim v0.12.0 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect - github.com/PuerkitoBio/goquery v1.9.1 // indirect + github.com/PuerkitoBio/goquery v1.9.2 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect diff --git a/go.sum b/go.sum index 33fd87942e..035bba0225 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8 github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= -github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Ullaakut/nmap/v2 v2.2.2 h1:178Ety3d8T21sF6WZxyj7QVZUhnC1tL1J+tHLLW507Q= @@ -1168,8 +1168,8 @@ github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinC github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/vulsio/go-cti v0.0.5-0.20240318121747-822b3ef289cb h1:aC6CqML20oYEI5Wjx04uwpARsXjdGCrOk4ken+l4dG8= github.com/vulsio/go-cti v0.0.5-0.20240318121747-822b3ef289cb/go.mod h1:MHlQMcrMMUGXVc9G1JBZg1J/frsugODntu7CfLInEFs= -github.com/vulsio/go-cve-dictionary v0.10.2-0.20240319004433-af03be313b77 h1:utQlIgdHOqx+TOHecQm3vk4Bu9QHZcwkKj2DMQ4F3mo= -github.com/vulsio/go-cve-dictionary v0.10.2-0.20240319004433-af03be313b77/go.mod h1:NYtVYgM43dITGd0wVGTGhBqGHYisdK7k6pLo+71rMzU= +github.com/vulsio/go-cve-dictionary v0.10.2-0.20240628072614-73f15707be8e h1:z/rVzYJy6LCeSzoLFZuiAFfe45giUYdsyPL+iprlC78= +github.com/vulsio/go-cve-dictionary v0.10.2-0.20240628072614-73f15707be8e/go.mod h1:Kxpy1CE1D/Wsu7HH+5K1RAQQ6PErMOPHZ2W0+bsxqNc= github.com/vulsio/go-exploitdb v0.4.7-0.20240318122115-ccb3abc151a1 h1:rQRTmiO2gYEhyjthvGseV34Qj+nwrVgZEnFvk6Z2AqM= github.com/vulsio/go-exploitdb v0.4.7-0.20240318122115-ccb3abc151a1/go.mod h1:ml2oTRyR37hUyyP4kWD9NSlBYIQuJUVNaAfbflSu4i4= github.com/vulsio/go-kev v0.1.4-0.20240318121733-b3386e67d3fb h1:j03zKKkR+WWaPoPzMBwNxpDsc1mYDtt9s1VrHaIxmfw= diff --git a/models/cvecontents.go b/models/cvecontents.go index 33cbeb0a82..a9108436bd 100644 --- a/models/cvecontents.go +++ b/models/cvecontents.go @@ -1,10 +1,14 @@ package models import ( - "sort" + "cmp" + "fmt" + "slices" "strings" "time" + "golang.org/x/exp/maps" + "github.com/future-architect/vuls/constant" ) @@ -15,18 +19,14 @@ type CveContents map[CveContentType][]CveContent func NewCveContents(conts ...CveContent) CveContents { m := CveContents{} for _, cont := range conts { - if cont.Type == Jvn { - found := false - for _, cveCont := range m[cont.Type] { - if cont.SourceLink == cveCont.SourceLink { - found = true - break - } - } - if !found { + switch cont.Type { + case Jvn: + if !slices.ContainsFunc(m[cont.Type], func(e CveContent) bool { + return cont.SourceLink == e.SourceLink + }) { m[cont.Type] = append(m[cont.Type], cont) } - } else { + default: m[cont.Type] = []CveContent{cont} } } @@ -43,14 +43,7 @@ type CveContentStr struct { func (v CveContents) Except(exceptCtypes ...CveContentType) (values CveContents) { values = CveContents{} for ctype, content := range v { - found := false - for _, exceptCtype := range exceptCtypes { - if ctype == exceptCtype { - found = true - break - } - } - if !found { + if !slices.Contains(exceptCtypes, ctype) { values[ctype] = content } } @@ -63,43 +56,51 @@ func (v CveContents) PrimarySrcURLs(lang, myFamily, cveID string, confidences Co return } - if conts, found := v[Nvd]; found { - for _, cont := range conts { - for _, r := range cont.References { - for _, t := range r.Tags { - if t == "Vendor Advisory" { - values = append(values, CveContentStr{Nvd, r.Link}) + for _, ctype := range append(append(CveContentTypes{Mitre, Nvd, Jvn}, GetCveContentTypes(myFamily)...), GitHub) { + for _, cont := range v[ctype] { + switch ctype { + case Nvd: + for _, r := range cont.References { + if slices.Contains(r.Tags, "Vendor Advisory") { + if !slices.ContainsFunc(values, func(e CveContentStr) bool { + return e.Type == ctype && e.Value == r.Link + }) { + values = append(values, CveContentStr{ + Type: ctype, + Value: r.Link, + }) + } } } - } - } - } - - order := append(append(CveContentTypes{Nvd}, GetCveContentTypes(myFamily)...), GitHub) - for _, ctype := range order { - if conts, found := v[ctype]; found { - for _, cont := range conts { - if cont.SourceLink == "" { - continue + if cont.SourceLink != "" && !slices.ContainsFunc(values, func(e CveContentStr) bool { + return e.Type == ctype && e.Value == cont.SourceLink + }) { + values = append(values, CveContentStr{ + Type: ctype, + Value: cont.SourceLink, + }) } - values = append(values, CveContentStr{ctype, cont.SourceLink}) - } - } - } - - jvnMatch := false - for _, confidence := range confidences { - if confidence.DetectionMethod == JvnVendorProductMatchStr { - jvnMatch = true - break - } - } - - if lang == "ja" || jvnMatch { - if conts, found := v[Jvn]; found { - for _, cont := range conts { - if 0 < len(cont.SourceLink) { - values = append(values, CveContentStr{Jvn, cont.SourceLink}) + case Jvn: + if lang == "ja" || slices.ContainsFunc(confidences, func(e Confidence) bool { + return e.DetectionMethod == JvnVendorProductMatchStr + }) { + if cont.SourceLink != "" && !slices.ContainsFunc(values, func(e CveContentStr) bool { + return e.Type == ctype && e.Value == cont.SourceLink + }) { + values = append(values, CveContentStr{ + Type: ctype, + Value: cont.SourceLink, + }) + } + } + default: + if cont.SourceLink != "" && !slices.ContainsFunc(values, func(e CveContentStr) bool { + return e.Type == ctype && e.Value == cont.SourceLink + }) { + values = append(values, CveContentStr{ + Type: ctype, + Value: cont.SourceLink, + }) } } } @@ -108,7 +109,7 @@ func (v CveContents) PrimarySrcURLs(lang, myFamily, cveID string, confidences Co if len(values) == 0 && strings.HasPrefix(cveID, "CVE") { return []CveContentStr{{ Type: Nvd, - Value: "https://nvd.nist.gov/vuln/detail/" + cveID, + Value: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", cveID), }} } return values @@ -116,17 +117,10 @@ func (v CveContents) PrimarySrcURLs(lang, myFamily, cveID string, confidences Co // PatchURLs returns link of patch func (v CveContents) PatchURLs() (urls []string) { - conts, found := v[Nvd] - if !found { - return - } - - for _, cont := range conts { + for _, cont := range v[Nvd] { for _, r := range cont.References { - for _, t := range r.Tags { - if t == "Patch" { - urls = append(urls, r.Link) - } + if slices.Contains(r.Tags, "Patch") && !slices.Contains(urls, r.Link) { + urls = append(urls, r.Link) } } } @@ -145,21 +139,24 @@ func (v CveContents) Cpes(myFamily string) (values []CveContentCpes) { order = append(order, AllCveContetTypes.Except(order...)...) for _, ctype := range order { - if conts, found := v[ctype]; found { - for _, cont := range conts { - if 0 < len(cont.Cpes) { - values = append(values, CveContentCpes{ - Type: ctype, - Value: cont.Cpes, - }) - } + for _, cont := range v[ctype] { + if len(cont.Cpes) == 0 { + continue + } + if !slices.ContainsFunc(values, func(e CveContentCpes) bool { + return e.Type == ctype && slices.Equal(e.Value, cont.Cpes) + }) { + values = append(values, CveContentCpes{ + Type: ctype, + Value: cont.Cpes, + }) } } } return } -// CveContentRefs has CveContentType and Cpes +// CveContentRefs has CveContentType and References type CveContentRefs struct { Type CveContentType Value []Reference @@ -171,14 +168,19 @@ func (v CveContents) References(myFamily string) (values []CveContentRefs) { order = append(order, AllCveContetTypes.Except(order...)...) for _, ctype := range order { - if conts, found := v[ctype]; found { - for _, cont := range conts { - if 0 < len(cont.References) { - values = append(values, CveContentRefs{ - Type: ctype, - Value: cont.References, - }) - } + for _, cont := range v[ctype] { + if len(cont.References) == 0 { + continue + } + if !slices.ContainsFunc(values, func(e CveContentRefs) bool { + return e.Type == ctype && slices.EqualFunc(e.Value, cont.References, func(e1, e2 Reference) bool { + return e1.Link == e2.Link && e1.RefID == e2.RefID && e1.Source == e2.Source && slices.Equal(e1.Tags, e2.Tags) + }) + }) { + values = append(values, CveContentRefs{ + Type: ctype, + Value: cont.References, + }) } } } @@ -191,20 +193,18 @@ func (v CveContents) CweIDs(myFamily string) (values []CveContentStr) { order := GetCveContentTypes(myFamily) order = append(order, AllCveContetTypes.Except(order...)...) for _, ctype := range order { - if conts, found := v[ctype]; found { - for _, cont := range conts { - if 0 < len(cont.CweIDs) { - for _, cweID := range cont.CweIDs { - for _, val := range values { - if val.Value == cweID { - continue - } - } - values = append(values, CveContentStr{ - Type: ctype, - Value: cweID, - }) - } + for _, cont := range v[ctype] { + if len(cont.CweIDs) == 0 { + continue + } + for _, cweID := range cont.CweIDs { + if !slices.ContainsFunc(values, func(e CveContentStr) bool { + return e.Type == ctype && e.Value == cweID + }) { + values = append(values, CveContentStr{ + Type: ctype, + Value: cweID, + }) } } } @@ -213,52 +213,55 @@ func (v CveContents) CweIDs(myFamily string) (values []CveContentStr) { } // UniqCweIDs returns Uniq CweIDs -func (v CveContents) UniqCweIDs(myFamily string) (values []CveContentStr) { +func (v CveContents) UniqCweIDs(myFamily string) []CveContentStr { uniq := map[string]CveContentStr{} for _, cwes := range v.CweIDs(myFamily) { uniq[cwes.Value] = cwes } - for _, cwe := range uniq { - values = append(values, cwe) + return maps.Values(uniq) +} + +// CveContentSSVC has CveContentType and SSVC +type CveContentSSVC struct { + Type CveContentType + Value SSVC +} + +func (v CveContents) SSVC() (value []CveContentSSVC) { + for _, cont := range v[Mitre] { + if cont.SSVC == nil { + continue + } + t := Mitre + if s, ok := cont.Optional["source"]; ok { + t = CveContentType(fmt.Sprintf("%s(%s)", Mitre, s)) + } + value = append(value, CveContentSSVC{ + Type: t, + Value: *cont.SSVC, + }) } - return values + return } // Sort elements for integration-testing func (v CveContents) Sort() { for contType, contents := range v { - // CVSS3 desc, CVSS2 desc, SourceLink asc - sort.Slice(contents, func(i, j int) bool { - if contents[i].Cvss3Score > contents[j].Cvss3Score { - return true - } else if contents[i].Cvss3Score == contents[i].Cvss3Score { - if contents[i].Cvss2Score > contents[j].Cvss2Score { - return true - } else if contents[i].Cvss2Score == contents[i].Cvss2Score { - if contents[i].SourceLink < contents[j].SourceLink { - return true - } - } - } - return false + // CVSS40 desc, CVSS3 desc, CVSS2 desc, SourceLink asc + slices.SortFunc(contents, func(a, b CveContent) int { + return cmp.Or( + cmp.Compare(b.Cvss40Score, a.Cvss40Score), + cmp.Compare(b.Cvss3Score, a.Cvss3Score), + cmp.Compare(b.Cvss2Score, a.Cvss2Score), + cmp.Compare(a.SourceLink, b.SourceLink), + ) }) - v[contType] = contents - } - for contType, contents := range v { for cveID, cont := range contents { - sort.Slice(cont.References, func(i, j int) bool { - return cont.References[i].Link < cont.References[j].Link - }) - sort.Slice(cont.CweIDs, func(i, j int) bool { - return cont.CweIDs[i] < cont.CweIDs[j] - }) - for i, ref := range cont.References { - // sort v.CveContents[].References[].Tags - sort.Slice(ref.Tags, func(j, k int) bool { - return ref.Tags[j] < ref.Tags[k] - }) - cont.References[i] = ref + slices.SortFunc(cont.References, func(a, b Reference) int { return cmp.Compare(a.Link, b.Link) }) + for i := range cont.References { + slices.Sort(cont.References[i].Tags) } + slices.Sort(cont.CweIDs) contents[cveID] = cont } v[contType] = contents @@ -267,23 +270,27 @@ func (v CveContents) Sort() { // CveContent has abstraction of various vulnerability information type CveContent struct { - Type CveContentType `json:"type"` - CveID string `json:"cveID"` - Title string `json:"title"` - Summary string `json:"summary"` - Cvss2Score float64 `json:"cvss2Score"` - Cvss2Vector string `json:"cvss2Vector"` - Cvss2Severity string `json:"cvss2Severity"` - Cvss3Score float64 `json:"cvss3Score"` - Cvss3Vector string `json:"cvss3Vector"` - Cvss3Severity string `json:"cvss3Severity"` - SourceLink string `json:"sourceLink"` - Cpes []Cpe `json:"cpes,omitempty"` - References References `json:"references,omitempty"` - CweIDs []string `json:"cweIDs,omitempty"` - Published time.Time `json:"published"` - LastModified time.Time `json:"lastModified"` - Optional map[string]string `json:"optional,omitempty"` + Type CveContentType `json:"type"` + CveID string `json:"cveID"` + Title string `json:"title"` + Summary string `json:"summary"` + Cvss2Score float64 `json:"cvss2Score"` + Cvss2Vector string `json:"cvss2Vector"` + Cvss2Severity string `json:"cvss2Severity"` + Cvss3Score float64 `json:"cvss3Score"` + Cvss3Vector string `json:"cvss3Vector"` + Cvss3Severity string `json:"cvss3Severity"` + Cvss40Score float64 `json:"cvss40Score"` + Cvss40Vector string `json:"cvss40Vector"` + Cvss40Severity string `json:"cvss40Severity"` + SSVC *SSVC `json:"ssvc,omitempty"` + SourceLink string `json:"sourceLink"` + Cpes []Cpe `json:"cpes,omitempty"` + References References `json:"references,omitempty"` + CweIDs []string `json:"cweIDs,omitempty"` + Published time.Time `json:"published"` + LastModified time.Time `json:"lastModified"` + Optional map[string]string `json:"optional,omitempty"` } // Empty checks the content is empty @@ -297,6 +304,8 @@ type CveContentType string // NewCveContentType create CveContentType func NewCveContentType(name string) CveContentType { switch name { + case "mitre": + return Mitre case "nvd": return Nvd case "jvn": @@ -415,6 +424,9 @@ func GetCveContentTypes(family string) []CveContentType { } const ( + // Mitre is Mitre + Mitre CveContentType = "mitre" + // Nvd is Nvd JSON Nvd CveContentType = "nvd" @@ -556,6 +568,7 @@ type CveContentTypes []CveContentType // AllCveContetTypes has all of CveContentTypes var AllCveContetTypes = CveContentTypes{ + Mitre, Nvd, Jvn, Fortinet, @@ -603,14 +616,7 @@ var AllCveContetTypes = CveContentTypes{ // Except returns CveContentTypes except for given args func (c CveContentTypes) Except(excepts ...CveContentType) (excepted CveContentTypes) { for _, ctype := range c { - found := false - for _, except := range excepts { - if ctype == except { - found = true - break - } - } - if !found { + if !slices.Contains(excepts, ctype) { excepted = append(excepted, ctype) } } @@ -633,3 +639,10 @@ type Reference struct { RefID string `json:"refID,omitempty"` Tags []string `json:"tags,omitempty"` } + +// SSVC has SSVC decision points +type SSVC struct { + Exploitation string `json:"exploitation,omitempty"` + Automatable string `json:"automatable,omitempty"` + TechnicalImpact string `json:"technical_impact,omitempty"` +} diff --git a/models/cvecontents_test.go b/models/cvecontents_test.go index 2472598ecc..7f005df3fc 100644 --- a/models/cvecontents_test.go +++ b/models/cvecontents_test.go @@ -7,26 +7,37 @@ import ( "github.com/future-architect/vuls/constant" ) -func TestExcept(t *testing.T) { - var tests = []struct { - in CveContents - out CveContents - }{{ - in: CveContents{ - RedHat: []CveContent{{Type: RedHat}}, - Ubuntu: []CveContent{{Type: Ubuntu}}, - Debian: []CveContent{{Type: Debian}}, - }, - out: CveContents{ - RedHat: []CveContent{{Type: RedHat}}, +func TestCveContents_Except(t *testing.T) { + type args struct { + exceptCtypes []CveContentType + } + tests := []struct { + name string + v CveContents + args args + wantValues CveContents + }{ + { + name: "happy", + v: CveContents{ + RedHat: []CveContent{{Type: RedHat}}, + Ubuntu: []CveContent{{Type: Ubuntu}}, + Debian: []CveContent{{Type: Debian}}, + }, + args: args{ + exceptCtypes: []CveContentType{Ubuntu, Debian}, + }, + wantValues: CveContents{ + RedHat: []CveContent{{Type: RedHat}}, + }, }, - }, } for _, tt := range tests { - actual := tt.in.Except(Ubuntu, Debian) - if !reflect.DeepEqual(tt.out, actual) { - t.Errorf("\nexpected: %v\n actual: %v\n", tt.out, actual) - } + t.Run(tt.name, func(t *testing.T) { + if gotValues := tt.v.Except(tt.args.exceptCtypes...); !reflect.DeepEqual(gotValues, tt.wantValues) { + t.Errorf("CveContents.Except() = %v, want %v", gotValues, tt.wantValues) + } + }) } } @@ -84,14 +95,14 @@ func TestSourceLinks(t *testing.T) { Type: Nvd, Value: "https://nvd.nist.gov/vuln/detail/CVE-2017-6074", }, - { - Type: RedHat, - Value: "https://access.redhat.com/security/cve/CVE-2017-6074", - }, { Type: Jvn, Value: "https://jvn.jp/vu/JVNVU93610402/", }, + { + Type: RedHat, + Value: "https://access.redhat.com/security/cve/CVE-2017-6074", + }, }, }, // lang: en @@ -162,6 +173,294 @@ func TestSourceLinks(t *testing.T) { } } +func TestCveContents_PatchURLs(t *testing.T) { + tests := []struct { + name string + v CveContents + wantUrls []string + }{ + { + name: "happy", + v: CveContents{ + Nvd: []CveContent{ + { + References: []Reference{ + { + Link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c52873e5a1ef72f845526d9f6a50704433f9c625", + Source: "cve@mitre.org", + Tags: []string{"Patch", "Vendor Advisory"}, + }, + { + Link: "https://lists.debian.org/debian-lts-announce/2020/01/msg00013.html", + Source: "cve@mitre.org", + Tags: []string{"Mailing List", "Third Party Advisory"}, + }, + }, + }, + { + References: []Reference{ + { + Link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c52873e5a1ef72f845526d9f6a50704433f9c625", + Tags: []string{"Patch", "Vendor Advisory"}, + }, + }, + }, + }, + }, + wantUrls: []string{"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c52873e5a1ef72f845526d9f6a50704433f9c625"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotUrls := tt.v.PatchURLs(); !reflect.DeepEqual(gotUrls, tt.wantUrls) { + t.Errorf("CveContents.PatchURLs() = %v, want %v", gotUrls, tt.wantUrls) + } + }) + } +} + +func TestCveContents_Cpes(t *testing.T) { + type args struct { + myFamily string + } + tests := []struct { + name string + v CveContents + args args + wantValues []CveContentCpes + }{ + { + name: "happy", + v: CveContents{ + Nvd: []CveContent{{ + Cpes: []Cpe{{ + URI: "cpe:/a:microsoft:internet_explorer:8.0.6001:beta", + FormattedString: "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*", + }}, + }}, + }, + args: args{myFamily: "redhat"}, + wantValues: []CveContentCpes{{ + Type: Nvd, + Value: []Cpe{{ + URI: "cpe:/a:microsoft:internet_explorer:8.0.6001:beta", + FormattedString: "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*", + }}, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotValues := tt.v.Cpes(tt.args.myFamily); !reflect.DeepEqual(gotValues, tt.wantValues) { + t.Errorf("CveContents.Cpes() = %v, want %v", gotValues, tt.wantValues) + } + }) + } +} +func TestCveContents_References(t *testing.T) { + type args struct { + myFamily string + } + tests := []struct { + name string + v CveContents + args args + wantValues []CveContentRefs + }{ + { + name: "happy", + v: CveContents{ + Mitre: []CveContent{{CveID: "CVE-2024-0001"}}, + Nvd: []CveContent{ + { + References: []Reference{ + { + Link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c52873e5a1ef72f845526d9f6a50704433f9c625", + Source: "cve@mitre.org", + Tags: []string{"Patch", "Vendor Advisory"}, + }, + { + Link: "https://lists.debian.org/debian-lts-announce/2020/01/msg00013.html", + Source: "cve@mitre.org", + Tags: []string{"Mailing List", "Third Party Advisory"}, + }, + }, + }, + { + References: []Reference{ + { + Link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c52873e5a1ef72f845526d9f6a50704433f9c625", + Tags: []string{"Patch", "Vendor Advisory"}, + }, + }, + }, + }, + }, + wantValues: []CveContentRefs{ + { + Type: Nvd, + Value: []Reference{ + { + Link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c52873e5a1ef72f845526d9f6a50704433f9c625", + Source: "cve@mitre.org", + Tags: []string{"Patch", "Vendor Advisory"}, + }, + { + Link: "https://lists.debian.org/debian-lts-announce/2020/01/msg00013.html", + Source: "cve@mitre.org", + Tags: []string{"Mailing List", "Third Party Advisory"}, + }, + }, + }, + { + Type: Nvd, + Value: []Reference{ + { + Link: "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c52873e5a1ef72f845526d9f6a50704433f9c625", + Tags: []string{"Patch", "Vendor Advisory"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotValues := tt.v.References(tt.args.myFamily); !reflect.DeepEqual(gotValues, tt.wantValues) { + t.Errorf("CveContents.References() = %v, want %v", gotValues, tt.wantValues) + } + }) + } +} + +func TestCveContents_CweIDs(t *testing.T) { + type args struct { + myFamily string + } + tests := []struct { + name string + v CveContents + args args + wantValues []CveContentStr + }{ + { + name: "happy", + v: CveContents{ + Mitre: []CveContent{{CweIDs: []string{"CWE-001"}}}, + Nvd: []CveContent{ + {CweIDs: []string{"CWE-001"}}, + {CweIDs: []string{"CWE-001"}}, + }, + }, + args: args{myFamily: "redhat"}, + wantValues: []CveContentStr{ + { + Type: Mitre, + Value: "CWE-001", + }, + { + Type: Nvd, + Value: "CWE-001", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotValues := tt.v.CweIDs(tt.args.myFamily); !reflect.DeepEqual(gotValues, tt.wantValues) { + t.Errorf("CveContents.CweIDs() = %v, want %v", gotValues, tt.wantValues) + } + }) + } +} + +func TestCveContents_UniqCweIDs(t *testing.T) { + type args struct { + myFamily string + } + tests := []struct { + name string + v CveContents + args args + want []CveContentStr + }{ + { + name: "happy", + v: CveContents{ + Mitre: []CveContent{{CweIDs: []string{"CWE-001"}}}, + Nvd: []CveContent{ + {CweIDs: []string{"CWE-001"}}, + {CweIDs: []string{"CWE-001"}}, + }, + }, + args: args{myFamily: "redhat"}, + want: []CveContentStr{ + { + Type: Nvd, + Value: "CWE-001", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.v.UniqCweIDs(tt.args.myFamily); !reflect.DeepEqual(got, tt.want) { + t.Errorf("CveContents.UniqCweIDs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCveContents_SSVC(t *testing.T) { + tests := []struct { + name string + v CveContents + want []CveContentSSVC + }{ + { + name: "happy", + v: CveContents{ + Mitre: []CveContent{ + { + Type: Mitre, + CveID: "CVE-2024-5732", + Title: "Clash Proxy Port improper authentication", + Optional: map[string]string{"source": "CNA"}, + }, + { + Type: Mitre, + CveID: "CVE-2024-5732", + Title: "CISA ADP Vulnrichment", + SSVC: &SSVC{ + Exploitation: "none", + Automatable: "no", + TechnicalImpact: "partial", + }, + Optional: map[string]string{"source": "ADP:CISA-ADP"}, + }, + }, + }, + want: []CveContentSSVC{ + { + Type: "mitre(ADP:CISA-ADP)", + Value: SSVC{ + Exploitation: "none", + Automatable: "no", + TechnicalImpact: "partial", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.v.SSVC(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("CveContents.SSVC() = %v, want %v", got, tt.want) + } + }) + } +} + func TestCveContents_Sort(t *testing.T) { tests := []struct { name string @@ -241,6 +540,48 @@ func TestCveContents_Sort(t *testing.T) { }, }, }, + { + name: "sort CVSS v4.0", + v: CveContents{ + Mitre: []CveContent{ + {Cvss40Score: 0}, + {Cvss40Score: 6.9}, + }, + }, + want: CveContents{ + Mitre: []CveContent{ + {Cvss40Score: 6.9}, + {Cvss40Score: 0}, + }, + }, + }, + { + name: "sort CVSS v4.0 and CVSS v3", + v: CveContents{ + Mitre: []CveContent{ + { + Cvss40Score: 0, + Cvss3Score: 7.3, + }, + { + Cvss40Score: 0, + Cvss3Score: 9.8, + }, + }, + }, + want: CveContents{ + Mitre: []CveContent{ + { + Cvss40Score: 0, + Cvss3Score: 9.8, + }, + { + Cvss40Score: 0, + Cvss3Score: 7.3, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -252,6 +593,47 @@ func TestCveContents_Sort(t *testing.T) { } } +func TestCveContent_Empty(t *testing.T) { + type fields struct { + Type CveContentType + CveID string + Title string + Summary string + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "empty", + fields: fields{ + Summary: "", + }, + want: true, + }, + { + name: "not empty", + fields: fields{ + Summary: "summary", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := (CveContent{ + Type: tt.fields.Type, + CveID: tt.fields.CveID, + Title: tt.fields.Title, + Summary: tt.fields.Summary, + }).Empty(); got != tt.want { + t.Errorf("CveContent.Empty() = %v, want %v", got, tt.want) + } + }) + } +} + func TestNewCveContentType(t *testing.T) { tests := []struct { name string @@ -309,3 +691,31 @@ func TestGetCveContentTypes(t *testing.T) { }) } } + +func TestCveContentTypes_Except(t *testing.T) { + type args struct { + excepts []CveContentType + } + tests := []struct { + name string + c CveContentTypes + args args + wantExcepted CveContentTypes + }{ + { + name: "happy", + c: CveContentTypes{Ubuntu, UbuntuAPI}, + args: args{ + excepts: []CveContentType{Ubuntu}, + }, + wantExcepted: CveContentTypes{UbuntuAPI}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotExcepted := tt.c.Except(tt.args.excepts...); !reflect.DeepEqual(gotExcepted, tt.wantExcepted) { + t.Errorf("CveContentTypes.Except() = %v, want %v", gotExcepted, tt.wantExcepted) + } + }) + } +} diff --git a/models/utils.go b/models/utils.go index 9a4eb105fa..da6dde487a 100644 --- a/models/utils.go +++ b/models/utils.go @@ -6,6 +6,7 @@ package models import ( "fmt" "strings" + "time" cvedict "github.com/vulsio/go-cve-dictionary/models" ) @@ -178,3 +179,122 @@ func ConvertFortinetToModel(cveID string, fortinets []cvedict.Fortinet) []CveCon } return cves } + +// ConvertMitreToModel convert Mitre to CveContent +func ConvertMitreToModel(cveID string, mitres []cvedict.Mitre) []CveContent { + var cves []CveContent + for _, mitre := range mitres { + for _, c := range mitre.Containers { + cve := CveContent{ + Type: Mitre, + CveID: cveID, + Title: func() string { + if c.Title != nil { + return *c.Title + } + return "" + }(), + Summary: func() string { + for _, d := range c.Descriptions { + if d.Lang == "en" { + return d.Value + } + } + return "" + }(), + SourceLink: fmt.Sprintf("https://www.cve.org/CVERecord?id=%s", cveID), + Published: func() time.Time { + if mitre.CVEMetadata.DatePublished != nil { + return *mitre.CVEMetadata.DatePublished + } + return time.Time{} + }(), + LastModified: func() time.Time { + if mitre.CVEMetadata.DateUpdated != nil { + return *mitre.CVEMetadata.DateUpdated + } + if mitre.CVEMetadata.DatePublished != nil { + return *mitre.CVEMetadata.DatePublished + } + return time.Time{} + }(), + Optional: map[string]string{"source": func() string { + if c.ProviderMetadata.ShortName != nil { + return fmt.Sprintf("%s:%s", c.ContainerType, *c.ProviderMetadata.ShortName) + } + return fmt.Sprintf("%s:%s", c.ContainerType, c.ProviderMetadata.OrgID) + }()}, + } + + for _, m := range c.Metrics { + if m.CVSSv2 != nil { + cve.Cvss2Score = m.CVSSv2.BaseScore + cve.Cvss2Vector = m.CVSSv2.VectorString + } + if m.CVSSv30 != nil { + if cve.Cvss3Vector == "" { + cve.Cvss3Score = m.CVSSv30.BaseScore + cve.Cvss3Vector = m.CVSSv30.VectorString + cve.Cvss3Severity = m.CVSSv30.BaseSeverity + } + } + if m.CVSSv31 != nil { + cve.Cvss3Score = m.CVSSv31.BaseScore + cve.Cvss3Vector = m.CVSSv31.VectorString + cve.Cvss3Severity = m.CVSSv31.BaseSeverity + } + if m.CVSSv40 != nil { + cve.Cvss40Score = m.CVSSv40.BaseScore + cve.Cvss40Vector = m.CVSSv40.VectorString + cve.Cvss40Severity = m.CVSSv40.BaseSeverity + } + if m.SSVC != nil { + cve.SSVC = &SSVC{ + Exploitation: func() string { + if m.SSVC.Exploitation != nil { + return *m.SSVC.Exploitation + } + return "" + }(), + Automatable: func() string { + if m.SSVC.Automatable != nil { + return *m.SSVC.Automatable + } + return "" + }(), + TechnicalImpact: func() string { + if m.SSVC.TechnicalImpact != nil { + return *m.SSVC.TechnicalImpact + } + return "" + }(), + } + } + } + + for _, r := range c.References { + cve.References = append(cve.References, Reference{ + Link: r.Link, + Source: r.Source, + Tags: func() []string { + if len(r.Tags) > 0 { + return strings.Split(r.Tags, ",") + } + return nil + }(), + }) + } + + for _, p := range c.ProblemTypes { + for _, d := range p.Descriptions { + if d.CweID != nil { + cve.CweIDs = append(cve.CweIDs, *d.CweID) + } + } + } + + cves = append(cves, cve) + } + } + return cves +} diff --git a/models/vulninfos.go b/models/vulninfos.go index 5cd4d15c17..4aa1f50be8 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -123,8 +123,7 @@ func (v VulnInfos) FilterIgnorePkgs(ignorePkgsRegexps []string) (_ VulnInfos, nF // FindScoredVulns return scored vulnerabilities func (v VulnInfos) FindScoredVulns() (_ VulnInfos, nFiltered int) { return v.Find(func(vv VulnInfo) bool { - if 0 < vv.MaxCvss2Score().Value.Score || - 0 < vv.MaxCvss3Score().Value.Score { + if 0 < vv.MaxCvss2Score().Value.Score || 0 < vv.MaxCvss3Score().Value.Score || 0 < vv.MaxCvss40Score().Value.Score { return true } nFiltered++ @@ -152,7 +151,10 @@ func (v VulnInfos) ToSortedSlice() (sorted []VulnInfo) { func (v VulnInfos) CountGroupBySeverity() map[string]int { m := map[string]int{} for _, vInfo := range v { - score := vInfo.MaxCvss3Score().Value.Score + score := vInfo.MaxCvss40Score().Value.Score + if score < 0.1 { + score = vInfo.MaxCvss3Score().Value.Score + } if score < 0.1 { score = vInfo.MaxCvss2Score().Value.Score } @@ -417,7 +419,7 @@ func (v VulnInfo) Titles(lang, myFamily string) (values []CveContentStr) { } } - order := append(GetCveContentTypes(string(Trivy)), append(CveContentTypes{Fortinet, Nvd}, GetCveContentTypes(myFamily)...)...) + order := append(GetCveContentTypes(string(Trivy)), append(CveContentTypes{Fortinet, Nvd, Mitre}, GetCveContentTypes(myFamily)...)...) order = append(order, AllCveContetTypes.Except(append(order, Jvn)...)...) for _, ctype := range order { if conts, found := v.CveContents[ctype]; found { @@ -464,7 +466,7 @@ func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) { } } - order := append(append(GetCveContentTypes(string(Trivy)), GetCveContentTypes(myFamily)...), Fortinet, Nvd, GitHub) + order := append(append(GetCveContentTypes(string(Trivy)), GetCveContentTypes(myFamily)...), Fortinet, Nvd, Mitre, GitHub) order = append(order, AllCveContetTypes.Except(append(order, Jvn)...)...) for _, ctype := range order { if conts, found := v.CveContents[ctype]; found { @@ -510,7 +512,7 @@ func (v VulnInfo) Summaries(lang, myFamily string) (values []CveContentStr) { // Cvss2Scores returns CVSS V2 Scores func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) { - order := append([]CveContentType{RedHatAPI, RedHat, Nvd, Jvn}, GetCveContentTypes(string(Trivy))...) + order := append([]CveContentType{RedHatAPI, RedHat, Nvd, Mitre, Jvn}, GetCveContentTypes(string(Trivy))...) for _, ctype := range order { if conts, found := v.CveContents[ctype]; found { for _, cont := range conts { @@ -535,7 +537,7 @@ func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) { // Cvss3Scores returns CVSS V3 Score func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) { - order := append([]CveContentType{RedHatAPI, RedHat, SUSE, Microsoft, Fortinet, Nvd, Jvn}, GetCveContentTypes(string(Trivy))...) + order := append([]CveContentType{RedHatAPI, RedHat, SUSE, Microsoft, Fortinet, Nvd, Mitre, Jvn}, GetCveContentTypes(string(Trivy))...) for _, ctype := range order { if conts, found := v.CveContents[ctype]; found { for _, cont := range conts { @@ -606,9 +608,37 @@ func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) { return } +// Cvss40Scores returns CVSS V4 Score +func (v VulnInfo) Cvss40Scores() (values []CveContentCvss) { + for _, ctype := range []CveContentType{Mitre} { + if conts, found := v.CveContents[ctype]; found { + for _, cont := range conts { + if cont.Cvss40Score == 0 && cont.Cvss40Severity == "" { + continue + } + // https://nvd.nist.gov/vuln-metrics/cvss + values = append(values, CveContentCvss{ + Type: ctype, + Value: Cvss{ + Type: CVSS40, + Score: cont.Cvss40Score, + Vector: cont.Cvss40Vector, + Severity: strings.ToUpper(cont.Cvss40Severity), + }, + }) + } + } + } + return +} + // MaxCvssScore returns max CVSS Score // If there is no CVSS Score, return Severity as a numerical value. func (v VulnInfo) MaxCvssScore() CveContentCvss { + v40Max := v.MaxCvss40Score() + if v40Max.Type != Unknown { + return v40Max + } v3Max := v.MaxCvss3Score() if v3Max.Type != Unknown { return v3Max @@ -616,6 +646,20 @@ func (v VulnInfo) MaxCvssScore() CveContentCvss { return v.MaxCvss2Score() } +// MaxCvss40Score returns Max CVSS V4.0 Score +func (v VulnInfo) MaxCvss40Score() CveContentCvss { + max := CveContentCvss{ + Type: Unknown, + Value: Cvss{Type: CVSS40}, + } + for _, cvss := range v.Cvss40Scores() { + if max.Value.Score < cvss.Value.Score { + max = cvss + } + } + return max +} + // MaxCvss3Score returns Max CVSS V3 Score func (v VulnInfo) MaxCvss3Score() CveContentCvss { max := CveContentCvss{ @@ -648,17 +692,14 @@ func (v VulnInfo) MaxCvss2Score() CveContentCvss { func (v VulnInfo) AttackVector() string { for _, conts := range v.CveContents { for _, cont := range conts { - if strings.HasPrefix(cont.Cvss2Vector, "AV:N") || - strings.Contains(cont.Cvss3Vector, "AV:N") { + switch { + case strings.HasPrefix(cont.Cvss2Vector, "AV:N") || strings.Contains(cont.Cvss3Vector, "AV:N") || strings.Contains(cont.Cvss40Vector, "AV:N"): return "AV:N" - } else if strings.HasPrefix(cont.Cvss2Vector, "AV:A") || - strings.Contains(cont.Cvss3Vector, "AV:A") { + case strings.HasPrefix(cont.Cvss2Vector, "AV:A") || strings.Contains(cont.Cvss3Vector, "AV:A") || strings.Contains(cont.Cvss40Vector, "AV:A"): return "AV:A" - } else if strings.HasPrefix(cont.Cvss2Vector, "AV:L") || - strings.Contains(cont.Cvss3Vector, "AV:L") { + case strings.HasPrefix(cont.Cvss2Vector, "AV:L") || strings.Contains(cont.Cvss3Vector, "AV:L") || strings.Contains(cont.Cvss40Vector, "AV:L"): return "AV:L" - } else if strings.Contains(cont.Cvss3Vector, "AV:P") { - // no AV:P in CVSS v2 + case strings.Contains(cont.Cvss3Vector, "AV:P") || strings.Contains(cont.Cvss40Vector, "AV:P"): // no AV:P in CVSS v2 return "AV:P" } } @@ -724,6 +765,9 @@ const ( // CVSS3 means CVSS version3 CVSS3 CvssType = "3" + + // CVSS40 means CVSS version4.0 + CVSS40 CvssType = "4.0" ) // Cvss has CVSS Score diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index 12e9b40b43..7ec2486c4f 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -917,6 +917,50 @@ func TestMaxCvssScores(t *testing.T) { }, }, }, + // 6 : CVSSv4.0 and CVSSv3.1 + { + in: VulnInfo{ + CveContents: CveContents{ + Mitre: []CveContent{ + { + Type: Mitre, + Cvss40Score: 6.9, + Cvss40Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N", + Cvss40Severity: "MEDIUM", + Cvss3Score: 7.3, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + Cvss3Severity: "HIGH", + Optional: map[string]string{"source": "CNA"}, + }, + }, + Nvd: []CveContent{ + { + Type: Nvd, + Cvss3Score: 9.8, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + Cvss3Severity: "CRITICAL", + Optional: map[string]string{"source": "nvd@nist.gov"}, + }, + { + Type: Nvd, + Cvss3Score: 7.3, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + Cvss3Severity: "HIGH", + Optional: map[string]string{"source": "cna@vuldb.com"}, + }, + }, + }, + }, + out: CveContentCvss{ + Type: Mitre, + Value: Cvss{ + Type: CVSS40, + Score: 6.9, + Severity: "MEDIUM", + Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N", + }, + }, + }, // Empty { in: VulnInfo{}, @@ -1859,3 +1903,109 @@ func TestVulnInfo_PatchStatus(t *testing.T) { }) } } + +func TestVulnInfo_Cvss40Scores(t *testing.T) { + type fields struct { + CveID string + CveContents CveContents + } + tests := []struct { + name string + fields fields + want []CveContentCvss + }{ + { + name: "happy", + fields: fields{ + CveID: "CVE-2024-5732", + CveContents: CveContents{ + Mitre: []CveContent{ + { + Type: Mitre, + Cvss40Score: 6.9, + Cvss40Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N", + Cvss40Severity: "MEDIUM", + Cvss3Score: 7.3, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + Cvss3Severity: "HIGH", + Optional: map[string]string{"source": "CNA"}, + }, + }, + }, + }, + want: []CveContentCvss{ + { + Type: Mitre, + Value: Cvss{ + Type: CVSS40, + Score: 6.9, + Severity: "MEDIUM", + Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := (VulnInfo{ + CveID: tt.fields.CveID, + CveContents: tt.fields.CveContents, + }).Cvss40Scores(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("VulnInfo.Cvss40Scores() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVulnInfo_MaxCvss40Score(t *testing.T) { + type fields struct { + CveID string + CveContents CveContents + } + tests := []struct { + name string + fields fields + want CveContentCvss + }{ + { + name: "happy", + fields: fields{ + CveID: "CVE-2024-5732", + CveContents: CveContents{ + Mitre: []CveContent{ + { + Type: Mitre, + Cvss40Score: 6.9, + Cvss40Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N", + Cvss40Severity: "MEDIUM", + Cvss3Score: 7.3, + Cvss3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L", + Cvss3Severity: "HIGH", + Optional: map[string]string{"source": "CNA"}, + }, + }, + }, + }, + want: CveContentCvss{ + Type: Mitre, + Value: Cvss{ + Type: CVSS40, + Score: 6.9, + Severity: "MEDIUM", + Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := (VulnInfo{ + CveID: tt.fields.CveID, + CveContents: tt.fields.CveContents, + }).MaxCvss40Score(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("VulnInfo.MaxsCvss40Score() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/reporter/sbom/cyclonedx.go b/reporter/sbom/cyclonedx.go index 45db64db71..3bd2db8c38 100644 --- a/reporter/sbom/cyclonedx.go +++ b/reporter/sbom/cyclonedx.go @@ -424,6 +424,9 @@ func cdxRatings(cveContents models.CveContents) *[]cdx.VulnerabilityRating { if content.Cvss3Score != 0 || content.Cvss3Vector != "" || content.Cvss3Severity != "" { ratings = append(ratings, cdxCVSS3Rating(string(content.Type), content.Cvss3Vector, content.Cvss3Score, content.Cvss3Severity)) } + if content.Cvss40Score != 0 || content.Cvss40Vector != "" || content.Cvss40Severity != "" { + ratings = append(ratings, cdxCVSS40Rating(string(content.Type), content.Cvss40Vector, content.Cvss40Score, content.Cvss40Severity)) + } } } return &ratings @@ -480,6 +483,32 @@ func cdxCVSS3Rating(source, vector string, score float64, severity string) cdx.V return r } +func cdxCVSS40Rating(source, vector string, score float64, severity string) cdx.VulnerabilityRating { + r := cdx.VulnerabilityRating{ + Source: &cdx.Source{Name: source}, + Method: cdx.ScoringMethodCVSSv4, + Vector: vector, + } + 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, ghpkgToPURL 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)) diff --git a/reporter/slack.go b/reporter/slack.go index 0c229f7a3e..9273044328 100644 --- a/reporter/slack.go +++ b/reporter/slack.go @@ -253,7 +253,7 @@ func (w SlackWriter) attachmentText(vinfo models.VulnInfo, cweDict map[string]mo maxCvss := vinfo.MaxCvssScore() vectors := []string{} - scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...) + scores := append(vinfo.Cvss40Scores(), append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...)...) for _, cvss := range scores { if cvss.Value.Severity == "" { continue @@ -268,6 +268,8 @@ func (w SlackWriter) attachmentText(vinfo models.VulnInfo, cweDict map[string]mo calcURL = fmt.Sprintf( "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=%s", vinfo.CveID) + case models.CVSS40: + calcURL = fmt.Sprintf("https://www.first.org/cvss/calculator/4.0#%s", cvss.Value.Vector) } if conts, ok := vinfo.CveContents[cvss.Type]; ok { diff --git a/reporter/syslog.go b/reporter/syslog.go index 9df8a4e367..33f04449b0 100644 --- a/reporter/syslog.go +++ b/reporter/syslog.go @@ -73,6 +73,11 @@ func (w SyslogWriter) encodeSyslog(result models.ScanResult) (messages []string) kvPairs = append(kvPairs, fmt.Sprintf(`cvss_vector_%s_v3="%s"`, cvss.Type, cvss.Value.Vector)) } + for _, cvss := range vinfo.Cvss40Scores() { + kvPairs = append(kvPairs, fmt.Sprintf(`cvss_score_%s_v40="%.2f"`, cvss.Type, cvss.Value.Score)) + kvPairs = append(kvPairs, fmt.Sprintf(`cvss_vector_%s_v40="%s"`, cvss.Type, cvss.Value.Vector)) + } + if conts, ok := vinfo.CveContents[models.Nvd]; ok { for _, cont := range conts { cwes := strings.Join(cont.CweIDs, ",") diff --git a/reporter/util.go b/reporter/util.go index f18f5fcbba..d9cfdaa93b 100644 --- a/reporter/util.go +++ b/reporter/util.go @@ -337,18 +337,26 @@ No CVE-IDs are found in updatable packages. for _, vuln := range r.ScannedCves.ToSortedSlice() { data := [][]string{} data = append(data, []string{"Max Score", vuln.FormatMaxCvssScore()}) + for _, cvss := range vuln.Cvss40Scores() { + if cvssstr := cvss.Value.Format(); cvssstr != "" { + data = append(data, []string{string(cvss.Type), cvssstr}) + } + } for _, cvss := range vuln.Cvss3Scores() { if cvssstr := cvss.Value.Format(); cvssstr != "" { data = append(data, []string{string(cvss.Type), cvssstr}) } } - for _, cvss := range vuln.Cvss2Scores() { if cvssstr := cvss.Value.Format(); cvssstr != "" { data = append(data, []string{string(cvss.Type), cvssstr}) } } + for _, ssvc := range vuln.CveContents.SSVC() { + data = append(data, []string{fmt.Sprintf("SSVC[%s]", ssvc.Type), fmt.Sprintf("Exploitation : %s\nAutomatable : %s\nTechnicalImpact : %s", ssvc.Value.Exploitation, ssvc.Value.Automatable, ssvc.Value.TechnicalImpact)}) + } + data = append(data, []string{"Summary", vuln.Summaries( r.Lang, r.Family)[0].Value}) @@ -770,7 +778,7 @@ func getMinusDiffCves(previous, current models.ScanResult) models.VulnInfos { } func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { - cTypes := append([]models.CveContentType{models.Nvd, models.Jvn}, models.GetCveContentTypes(current.Family)...) + cTypes := append([]models.CveContentType{models.Mitre, models.Nvd, models.Jvn}, models.GetCveContentTypes(current.Family)...) prevLastModifieds := map[models.CveContentType][]time.Time{} preVinfo, ok := previous.ScannedCves[cveID] diff --git a/server/server.go b/server/server.go index a736ed0536..b166bac49d 100644 --- a/server/server.go +++ b/server/server.go @@ -76,7 +76,7 @@ func (h VulsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { } logging.Log.Infof("Fill CVE detailed with CVE-DB") - if err := detector.FillCvesWithNvdJvnFortinet(&r, config.Conf.CveDict, config.Conf.LogOpts); err != nil { + if err := detector.FillCvesWithGoCVEDictionary(&r, config.Conf.CveDict, config.Conf.LogOpts); err != nil { logging.Log.Errorf("Failed to fill with CVE: %+v", err) http.Error(w, err.Error(), http.StatusServiceUnavailable) } diff --git a/tui/tui.go b/tui/tui.go index c335850252..4407f5602c 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -899,6 +899,7 @@ func setChangelogLayout(g *gocui.Gui) error { type dataForTmpl struct { CveID string Cvsses string + SSVC []models.CveContentSSVC Exploits []models.Exploit Metasploits []models.Metasploit Summary string @@ -979,7 +980,7 @@ func detailLines() (string, error) { table := uitable.New() table.MaxColWidth = 100 table.Wrap = true - scores := append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...) + scores := append(vinfo.Cvss40Scores(), append(vinfo.Cvss3Scores(), vinfo.Cvss2Scores()...)...) var cols []interface{} for _, score := range scores { cols = []interface{}{ @@ -1002,6 +1003,7 @@ func detailLines() (string, error) { data := dataForTmpl{ CveID: vinfo.CveID, Cvsses: fmt.Sprintf("%s\n", table), + SSVC: vinfo.CveContents.SSVC(), Summary: fmt.Sprintf("%s (%s)", summary.Value, summary.Type), Mitigation: strings.Join(mitigations, "\n"), PatchURLs: vinfo.CveContents.PatchURLs(), @@ -1027,6 +1029,17 @@ CVSS Scores ----------- {{.Cvsses }} +{{if .SSVC}} +SSVC +----------- +{{range $ssvc := .SSVC -}} +* {{$ssvc.Type}} + Exploitation : {{$ssvc.Value.Exploitation}} + Automatable : {{$ssvc.Value.Automatable}} + TechnicalImpact : {{$ssvc.Value.TechnicalImpact}} +{{end}} +{{end}} + Summary ----------- {{.Summary }}