diff --git a/cli/presenter/apimodel.go b/cli/presenter/apimodel.go index 085043c2d9..c19552df62 100644 --- a/cli/presenter/apimodel.go +++ b/cli/presenter/apimodel.go @@ -60,28 +60,22 @@ func ConvertSBOMResultToPackages(result *sbom.Result) []apitypes.Package { func ConvertVulnResultToVulnerabilities(result *vulnerabilities.Result) []apitypes.Vulnerability { vuls := []apitypes.Vulnerability{} - if result == nil || result.MergedVulnerabilitiesByKey == nil { + if result == nil || result.VulnerabilitiesByKey == nil { return vuls } - for _, vulCandidates := range result.MergedVulnerabilitiesByKey { - if len(vulCandidates) < 1 { - continue - } - - vulCandidate := vulCandidates[0] - + for _, vulCandidate := range result.VulnerabilitiesByKey { vul := apitypes.Vulnerability{ - Cvss: ConvertVulnCvssToAPIModel(vulCandidate.Vulnerability.CVSS), - Description: to.Ptr(vulCandidate.Vulnerability.Description), - Distro: ConvertVulnDistroToAPIModel(vulCandidate.Vulnerability.Distro), - Fix: ConvertVulnFixToAPIModel(vulCandidate.Vulnerability.Fix), - LayerId: to.Ptr(vulCandidate.Vulnerability.LayerID), - Links: to.Ptr(vulCandidate.Vulnerability.Links), - Package: ConvertVulnPackageToAPIModel(vulCandidate.Vulnerability.Package), - Path: to.Ptr(vulCandidate.Vulnerability.Path), - Severity: ConvertVulnSeverityToAPIModel(vulCandidate.Vulnerability.Severity), - VulnerabilityName: to.Ptr(vulCandidate.Vulnerability.ID), + Cvss: ConvertVulnCvssToAPIModel(vulCandidate.CVSS), + Description: to.Ptr(vulCandidate.Description), + Distro: ConvertVulnDistroToAPIModel(vulCandidate.Distro), + Fix: ConvertVulnFixToAPIModel(vulCandidate.Fix), + LayerId: to.Ptr(vulCandidate.LayerID), + Links: to.Ptr(vulCandidate.Links), + Package: ConvertVulnPackageToAPIModel(vulCandidate.Package), + Path: to.Ptr(vulCandidate.Path), + Severity: ConvertVulnSeverityToAPIModel(vulCandidate.Severity), + VulnerabilityName: to.Ptr(vulCandidate.ID), } vuls = append(vuls, vul) } diff --git a/cli/presenter/apimodel_test.go b/cli/presenter/apimodel_test.go index ce13e70bd5..2278ab3693 100644 --- a/cli/presenter/apimodel_test.go +++ b/cli/presenter/apimodel_test.go @@ -161,108 +161,98 @@ func Test_ConvertVulnResultToVulnerabilities(t *testing.T) { name: "Vuls", args: args{ result: &vulnerabilities.Result{ - MergedVulnerabilitiesByKey: map[vulnerabilities.VulnerabilityKey][]vulnerabilities.MergedVulnerability{ + VulnerabilitiesByKey: map[vulnerabilities.VulnerabilityKey]vulnerabilities.Vulnerability{ "vulkey1": { - { - ID: "id1", - Vulnerability: vulnerabilities.Vulnerability{ - ID: "CVE-test-test-foo", - Description: "testbleed", - Links: []string{"link1", "link2"}, - Distro: vulnerabilities.Distro{ - Name: "distro1", - Version: "distrov1", - IDLike: []string{"IDLike1", "IDLike2"}, - }, - CVSS: []vulnerabilities.CVSS{ - { - Version: "v1", - Vector: "vector1", - Metrics: vulnerabilities.CvssMetrics{ - BaseScore: 1, - ExploitabilityScore: nil, - ImpactScore: nil, - }, - }, - { - Version: "v2", - Vector: "vector2", - Metrics: vulnerabilities.CvssMetrics{ - BaseScore: 2, - ExploitabilityScore: to.Ptr(2.1), - ImpactScore: to.Ptr(2.2), - }, - }, - }, - Fix: vulnerabilities.Fix{ - Versions: []string{"fv1", "fv2"}, - State: "fixed", + ID: "CVE-test-test-foo", + Description: "testbleed", + Links: []string{"link1", "link2"}, + Distro: vulnerabilities.Distro{ + Name: "distro1", + Version: "distrov1", + IDLike: []string{"IDLike1", "IDLike2"}, + }, + CVSS: []vulnerabilities.CVSS{ + { + Version: "v1", + Vector: "vector1", + Metrics: vulnerabilities.CvssMetrics{ + BaseScore: 1, + ExploitabilityScore: nil, + ImpactScore: nil, }, - Severity: string(apitypes.CRITICAL), - Package: vulnerabilities.Package{ - Name: "package1", - Version: "pv1", - Type: "pt1", - Language: "pl1", - Licenses: []string{"plic1", "plic2"}, - CPEs: []string{"cpe1", "cpe2"}, - PURL: "purl1", + }, + { + Version: "v2", + Vector: "vector2", + Metrics: vulnerabilities.CvssMetrics{ + BaseScore: 2, + ExploitabilityScore: to.Ptr(2.1), + ImpactScore: to.Ptr(2.2), }, - LayerID: "lid1", - Path: "path1", }, }, + Fix: vulnerabilities.Fix{ + Versions: []string{"fv1", "fv2"}, + State: "fixed", + }, + Severity: string(apitypes.CRITICAL), + Package: vulnerabilities.Package{ + Name: "package1", + Version: "pv1", + Type: "pt1", + Language: "pl1", + Licenses: []string{"plic1", "plic2"}, + CPEs: []string{"cpe1", "cpe2"}, + PURL: "purl1", + }, + LayerID: "lid1", + Path: "path1", }, "vulkey2": { - { - ID: "id2", - Vulnerability: vulnerabilities.Vulnerability{ - ID: "CVE-test-test-bar", - Description: "solartest", - Links: []string{"link3", "link4"}, - Distro: vulnerabilities.Distro{ - Name: "distro2", - Version: "distrov2", - IDLike: []string{"IDLike3", "IDLike4"}, - }, - CVSS: []vulnerabilities.CVSS{ - { - Version: "v3", - Vector: "vector3", - Metrics: vulnerabilities.CvssMetrics{ - BaseScore: 3, - ExploitabilityScore: nil, - ImpactScore: nil, - }, - }, - { - Version: "v4", - Vector: "vector4", - Metrics: vulnerabilities.CvssMetrics{ - BaseScore: 4, - ExploitabilityScore: to.Ptr(4.1), - ImpactScore: to.Ptr(4.2), - }, - }, - }, - Fix: vulnerabilities.Fix{ - Versions: []string{"fv3", "fv4"}, - State: "not-fixed", + ID: "CVE-test-test-bar", + Description: "solartest", + Links: []string{"link3", "link4"}, + Distro: vulnerabilities.Distro{ + Name: "distro2", + Version: "distrov2", + IDLike: []string{"IDLike3", "IDLike4"}, + }, + CVSS: []vulnerabilities.CVSS{ + { + Version: "v3", + Vector: "vector3", + Metrics: vulnerabilities.CvssMetrics{ + BaseScore: 3, + ExploitabilityScore: nil, + ImpactScore: nil, }, - Severity: string(apitypes.HIGH), - Package: vulnerabilities.Package{ - Name: "package2", - Version: "pv2", - Type: "pt2", - Language: "pl2", - Licenses: []string{"plic3", "plic4"}, - CPEs: []string{"cpe3", "cpe4"}, - PURL: "purl2", + }, + { + Version: "v4", + Vector: "vector4", + Metrics: vulnerabilities.CvssMetrics{ + BaseScore: 4, + ExploitabilityScore: to.Ptr(4.1), + ImpactScore: to.Ptr(4.2), }, - LayerID: "lid2", - Path: "path2", }, }, + Fix: vulnerabilities.Fix{ + Versions: []string{"fv3", "fv4"}, + State: "not-fixed", + }, + Severity: string(apitypes.HIGH), + Package: vulnerabilities.Package{ + Name: "package2", + Version: "pv2", + Type: "pt2", + Language: "pl2", + Licenses: []string{"plic3", "plic4"}, + CPEs: []string{"cpe3", "cpe4"}, + PURL: "purl2", + }, + LayerID: "lid2", + Path: "path2", }, "vulkey3": {}, }, diff --git a/scanner/families/exploits/family.go b/scanner/families/exploits/family.go index 998a8e402b..ede9cf0aac 100644 --- a/scanner/families/exploits/family.go +++ b/scanner/families/exploits/family.go @@ -81,17 +81,14 @@ func (e Exploits) Run(ctx context.Context, res *families.Results) (*types.Result } // create a comma separated representation of cveIDs array, as an input for the exploits scanners. -func getCVEIDsFromVulnerabilitiesResults(vulnerabilities *vulnerabilitytypes.Result) string { - if vulnerabilities == nil { +func getCVEIDsFromVulnerabilitiesResults(result *vulnerabilitytypes.Result) string { + if result == nil { return "" } cvesMap := make(map[string]bool) - - for _, mergedVulnerabilities := range vulnerabilities.MergedVulnerabilitiesByKey { - for _, vulnerability := range mergedVulnerabilities { - cvesMap[vulnerability.Vulnerability.ID] = true - } + for _, vulnerability := range result.VulnerabilitiesByKey { + cvesMap[vulnerability.ID] = true } cves := strings.Join(to.Keys(cvesMap), ",") diff --git a/scanner/families/exploits/family_test.go b/scanner/families/exploits/family_test.go index 659b95b73c..4e052c20bb 100644 --- a/scanner/families/exploits/family_test.go +++ b/scanner/families/exploits/family_test.go @@ -40,10 +40,10 @@ func Test_getCVEIDsFromVulnerabilitiesResults(t *testing.T) { wantCves: "", }, { - name: "nil MergedVulnerabilitiesByKey", + name: "nil VulnerabilitiesByKey", args: args{ vulnResults: &vulnerabilitytypes.Result{ - MergedVulnerabilitiesByKey: nil, + VulnerabilitiesByKey: nil, }, }, wantCves: "", @@ -52,7 +52,7 @@ func Test_getCVEIDsFromVulnerabilitiesResults(t *testing.T) { name: "no vulnerabilities", args: args{ vulnResults: &vulnerabilitytypes.Result{ - MergedVulnerabilitiesByKey: map[vulnerabilitytypes.VulnerabilityKey][]vulnerabilitytypes.MergedVulnerability{}, + VulnerabilitiesByKey: map[vulnerabilitytypes.VulnerabilityKey]vulnerabilitytypes.Vulnerability{}, }, }, wantCves: "", @@ -61,55 +61,27 @@ func Test_getCVEIDsFromVulnerabilitiesResults(t *testing.T) { name: "sanity", args: args{ vulnResults: &vulnerabilitytypes.Result{ - MergedVulnerabilitiesByKey: map[vulnerabilitytypes.VulnerabilityKey][]vulnerabilitytypes.MergedVulnerability{ - "vul1": { - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve1"}, - }, - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve2"}, - }, - }, - "vul2": { - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve3"}, - }, - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve4"}, - }, - }, + VulnerabilitiesByKey: map[vulnerabilitytypes.VulnerabilityKey]vulnerabilitytypes.Vulnerability{ + "vul1": {ID: "cve1"}, + "vul2": {ID: "cve3"}, }, Source: vulnerabilitytypes.Source{}, }, }, - wantCves: "cve1,cve2,cve3,cve4", + wantCves: "cve1,cve3", }, { name: "same cve id in different vulnerabilities", args: args{ vulnResults: &vulnerabilitytypes.Result{ - MergedVulnerabilitiesByKey: map[vulnerabilitytypes.VulnerabilityKey][]vulnerabilitytypes.MergedVulnerability{ - "vul1": { - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve1"}, - }, - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve2"}, - }, - }, - "vul2": { - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve1"}, - }, - { - Vulnerability: vulnerabilitytypes.Vulnerability{ID: "cve4"}, - }, - }, + VulnerabilitiesByKey: map[vulnerabilitytypes.VulnerabilityKey]vulnerabilitytypes.Vulnerability{ + "vul1": {ID: "cve1"}, + "vul2": {ID: "cve1"}, }, Source: vulnerabilitytypes.Source{}, }, }, - wantCves: "cve1,cve2,cve4", + wantCves: "cve1", }, } for _, tt := range tests { diff --git a/scanner/families/vulnerabilities/types/result.go b/scanner/families/vulnerabilities/types/result.go index 71dccb3c0a..c86abd52fd 100644 --- a/scanner/families/vulnerabilities/types/result.go +++ b/scanner/families/vulnerabilities/types/result.go @@ -33,14 +33,14 @@ type Source struct { } type Result struct { - Metadata families.ScanMetadata `json:"Metadata"` - Source Source `json:"Source"` - MergedVulnerabilitiesByKey map[VulnerabilityKey][]MergedVulnerability `json:"MergedVulnerabilitiesByKey"` + Metadata families.ScanMetadata `json:"Metadata"` + Source Source `json:"Source"` + VulnerabilitiesByKey map[VulnerabilityKey]Vulnerability `json:"VulnerabilitiesByKey"` } func NewResult() *Result { return &Result{ - MergedVulnerabilitiesByKey: make(map[VulnerabilityKey][]MergedVulnerability), + VulnerabilitiesByKey: make(map[VulnerabilityKey]Vulnerability), } } @@ -74,10 +74,10 @@ func (r *Result) GetSourceImageInfo() (*apitypes.ContainerImageInfo, error) { return containerImageInfo, nil } -// ToSlice returns MergedResults in a slice format and not by key. -func (r *Result) ToSlice() [][]MergedVulnerability { - ret := make([][]MergedVulnerability, 0) - for _, vulnerabilities := range r.MergedVulnerabilitiesByKey { +// ToSlice returns Result in a slice format and not by key. +func (r *Result) ToSlice() []Vulnerability { + ret := make([]Vulnerability, 0) + for _, vulnerabilities := range r.VulnerabilitiesByKey { ret = append(ret, vulnerabilities) } @@ -92,19 +92,15 @@ func (r *Result) Merge(meta families.ScanInputMetadata, result *ScannerResult) { return } - otherVulnerabilityByKey := toVulnerabilityByKey(result.Vulnerabilities) + for _, vulnerability := range result.Vulnerabilities { + key := NewVulnerabilityKey(vulnerability) - // go over other vulnerabilities list - // 1. merge mutual vulnerabilities - // 2. add non mutual vulnerabilities - for key, otherVulnerability := range otherVulnerabilityByKey { // look for other vulnerability key in the current merged vulnerabilities list - if mergedVulnerabilities, ok := r.MergedVulnerabilitiesByKey[key]; !ok { - // add non mutual vulnerability + if existingVulnerability, ok := r.VulnerabilitiesByKey[key]; !ok { log.Debugf("Adding new vulnerability results from %v. key=%v", result.Scanner, key) - r.MergedVulnerabilitiesByKey[key] = []MergedVulnerability{*NewMergedVulnerability(otherVulnerability, result.Scanner)} + r.VulnerabilitiesByKey[key] = vulnerability } else { - r.MergedVulnerabilitiesByKey[key] = handleVulnerabilityWithExistingKey(mergedVulnerabilities, otherVulnerability, result.Scanner) + r.VulnerabilitiesByKey[key] = handleVulnerabilityWithExistingKey(existingVulnerability, vulnerability) } } diff --git a/scanner/families/vulnerabilities/types/result_test.go b/scanner/families/vulnerabilities/types/result_test.go index b956625538..38ebac54af 100644 --- a/scanner/families/vulnerabilities/types/result_test.go +++ b/scanner/families/vulnerabilities/types/result_test.go @@ -22,8 +22,6 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/yudai/gojsondiff/formatter" "gotest.tools/assert" - - "github.com/openclarity/vmclarity/scanner/families" ) func Test_handleVulnerabilityWithExistingKey(t *testing.T) { @@ -35,286 +33,50 @@ func Test_handleVulnerabilityWithExistingKey(t *testing.T) { Version: "pkg-version", }, } - lowVul := Vulnerability{ - ID: "highVul", - Severity: "LOW", - Package: Package{ - Name: "pkg-name", - Version: "pkg-version", - }, - } - medVul := Vulnerability{ - ID: "highVul", - Severity: "MEDIUM", - Package: Package{ - Name: "pkg-name", - Version: "pkg-version", - }, - } type args struct { - mergedVulnerabilities []MergedVulnerability - otherVulnerability Vulnerability - otherScannerInfo ScannerInfo + vulnerability Vulnerability + otherVulnerability Vulnerability } tests := []struct { - name string - args args - want []MergedVulnerability - patchMergedVulnerabilityID func([]MergedVulnerability) []MergedVulnerability + name string + args args + want Vulnerability }{ { name: "identical vulnerability", args: args{ - mergedVulnerabilities: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - }, + vulnerability: highVul, otherVulnerability: highVul, - otherScannerInfo: ScannerInfo{ - Name: "scanner2", - }, - }, - want: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - { - Name: "scanner2", - }, - }, - }, }, + want: highVul, }, { - name: "different vulnerability", + name: "different fix versions", args: args{ - mergedVulnerabilities: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - }, - otherVulnerability: lowVul, - otherScannerInfo: ScannerInfo{ - Name: "scanner2", - }, - }, - want: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - { - ID: "2", - Vulnerability: lowVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - }, - Diffs: []VulnerabilityDiff{ - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"HIGH", "LOW"}, - }, - CompareToID: "1", - }, - }, - }, - }, - patchMergedVulnerabilityID: func(vulnerability []MergedVulnerability) []MergedVulnerability { - vulnerability[1].ID = "2" - return vulnerability - }, - }, - { - name: "different vulnerability from first identical to second", - args: args{ - mergedVulnerabilities: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - { - ID: "2", - Vulnerability: lowVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - }, - Diffs: []VulnerabilityDiff{ - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"HIGH", "LOW"}, - }, - CompareToID: "1", - }, - }, - }, - }, - otherVulnerability: lowVul, - otherScannerInfo: ScannerInfo{ - Name: "scanner3", - }, - }, - want: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - { - ID: "2", - Vulnerability: lowVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - { - Name: "scanner3", - }, - }, - Diffs: []VulnerabilityDiff{ - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"HIGH", "LOW"}, - }, - CompareToID: "1", - }, + vulnerability: Vulnerability{ + ID: "highVul", + Fix: Fix{ + Versions: []string{"1", "3"}, }, }, - }, - }, - { - name: "different vulnerability from all", - args: args{ - mergedVulnerabilities: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - { - ID: "2", - Vulnerability: lowVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - }, - Diffs: []VulnerabilityDiff{ - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"HIGH", "LOW"}, - }, - CompareToID: "1", - }, - }, + otherVulnerability: Vulnerability{ + ID: "highVul", + Fix: Fix{ + Versions: []string{"1", "2"}, }, }, - otherVulnerability: medVul, - otherScannerInfo: ScannerInfo{ - Name: "scanner3", - }, }, - want: []MergedVulnerability{ - { - ID: "1", - Vulnerability: highVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - { - ID: "2", - Vulnerability: lowVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - }, - Diffs: []VulnerabilityDiff{ - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"HIGH", "LOW"}, - }, - CompareToID: "1", - }, - }, - }, - { - ID: "3", - Vulnerability: medVul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner3", - }, - }, - Diffs: []VulnerabilityDiff{ - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"HIGH", "MEDIUM"}, - }, - CompareToID: "1", - }, - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"LOW", "MEDIUM"}, - }, - CompareToID: "2", - }, - }, + want: Vulnerability{ + ID: "highVul", + Fix: Fix{ + Versions: []string{"1", "2", "3"}, }, }, - patchMergedVulnerabilityID: func(vulnerability []MergedVulnerability) []MergedVulnerability { - vulnerability[2].ID = "3" - return vulnerability - }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := handleVulnerabilityWithExistingKey(tt.args.mergedVulnerabilities, tt.args.otherVulnerability, tt.args.otherScannerInfo) - if tt.patchMergedVulnerabilityID != nil { - got = tt.patchMergedVulnerabilityID(got) - } + got := handleVulnerabilityWithExistingKey(tt.args.vulnerability, tt.args.otherVulnerability) assert.DeepEqual(t, got, tt.want, cmpopts.IgnoreTypes(VulnerabilityDiff{}.ASCIIDiff)) }) } @@ -505,239 +267,3 @@ func Test_sortArrays(t *testing.T) { }) } } - -func TestMergedResults_Merge(t *testing.T) { - vul := Vulnerability{ - ID: "id1", - Severity: "HIGH", - Package: Package{ - Name: "pkg-name", - Version: "pkg-version", - }, - } - sameVulDifferentSeverity := Vulnerability{ - ID: "id1", - Severity: "LOW", - Package: Package{ - Name: "pkg-name", - Version: "pkg-version", - }, - } - differentVulID := Vulnerability{ - ID: "id2", - Severity: "HIGH", - Package: Package{ - Name: "pkg-name", - Version: "pkg-version", - }, - } - type fields struct { - MergedVulnerabilities map[VulnerabilityKey][]MergedVulnerability - } - tests := []struct { - name string - fields fields - args *ScannerResult - want *Result - patchMergedVulnerabilityID func(map[VulnerabilityKey][]MergedVulnerability) map[VulnerabilityKey][]MergedVulnerability - }{ - { - name: "all are non mutual vulnerabilities", - fields: fields{ - MergedVulnerabilities: NewResult().MergedVulnerabilitiesByKey, - }, - args: &ScannerResult{ - Vulnerabilities: []Vulnerability{ - vul, - differentVulID, - }, - Scanner: ScannerInfo{ - Name: "scanner1", - }, - }, - want: &Result{ - MergedVulnerabilitiesByKey: map[VulnerabilityKey][]MergedVulnerability{ - NewVulnerabilityKey(vul): { - { - ID: "0", - Vulnerability: vul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - }, - NewVulnerabilityKey(differentVulID): { - { - ID: "1", - Vulnerability: differentVulID, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - }, - }, - }, - patchMergedVulnerabilityID: func(v map[VulnerabilityKey][]MergedVulnerability) map[VulnerabilityKey][]MergedVulnerability { - v[NewVulnerabilityKey(vul)][0].ID = "0" - v[NewVulnerabilityKey(differentVulID)][0].ID = "1" - return v - }, - }, - { - name: "1 non mutual vulnerability and 1 mutual vulnerability with no diff", - fields: fields{ - MergedVulnerabilities: map[VulnerabilityKey][]MergedVulnerability{ - NewVulnerabilityKey(vul): { - { - ID: "0", - Vulnerability: vul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - }, - }, - }, - args: &ScannerResult{ - Vulnerabilities: []Vulnerability{ - vul, - differentVulID, - }, - Scanner: ScannerInfo{ - Name: "scanner2", - }, - }, - want: &Result{ - MergedVulnerabilitiesByKey: map[VulnerabilityKey][]MergedVulnerability{ - NewVulnerabilityKey(vul): { - { - ID: "0", - Vulnerability: vul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - { - Name: "scanner2", - }, - }, - }, - }, - NewVulnerabilityKey(differentVulID): { - { - ID: "1", - Vulnerability: differentVulID, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - }, - }, - }, - }, - }, - patchMergedVulnerabilityID: func(v map[VulnerabilityKey][]MergedVulnerability) map[VulnerabilityKey][]MergedVulnerability { - v[NewVulnerabilityKey(vul)][0].ID = "0" - v[NewVulnerabilityKey(differentVulID)][0].ID = "1" - return v - }, - }, - { - name: "1 non mutual vulnerability and 1 mutual vulnerability with diff", - fields: fields{ - MergedVulnerabilities: map[VulnerabilityKey][]MergedVulnerability{ - NewVulnerabilityKey(vul): { - { - ID: "0", - Vulnerability: vul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - }, - }, - }, - args: &ScannerResult{ - Vulnerabilities: []Vulnerability{ - sameVulDifferentSeverity, // mutual vulnerability with diff - differentVulID, // non mutual - }, - Scanner: ScannerInfo{ - Name: "scanner2", - }, - }, - want: &Result{ - MergedVulnerabilitiesByKey: map[VulnerabilityKey][]MergedVulnerability{ - NewVulnerabilityKey(vul): { - { - ID: "0", - Vulnerability: vul, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner1", - }, - }, - }, - { - ID: "1", - Vulnerability: sameVulDifferentSeverity, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - }, - Diffs: []VulnerabilityDiff{ - { - JSONDiff: map[string]interface{}{ - "severity": []interface{}{"HIGH", "LOW"}, - }, - CompareToID: "0", - }, - }, - }, - }, - NewVulnerabilityKey(differentVulID): { - { - ID: "0", - Vulnerability: differentVulID, - ScannersInfo: []ScannerInfo{ - { - Name: "scanner2", - }, - }, - }, - }, - }, - }, - patchMergedVulnerabilityID: func(v map[VulnerabilityKey][]MergedVulnerability) map[VulnerabilityKey][]MergedVulnerability { - v[NewVulnerabilityKey(vul)][0].ID = "0" - v[NewVulnerabilityKey(vul)][1].ID = "1" - v[NewVulnerabilityKey(differentVulID)][0].ID = "0" - return v - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := &Result{ - MergedVulnerabilitiesByKey: tt.fields.MergedVulnerabilities, - } - got.Merge(families.ScanInputMetadata{}, tt.args) - if tt.patchMergedVulnerabilityID != nil { - got.MergedVulnerabilitiesByKey = tt.patchMergedVulnerabilityID(got.MergedVulnerabilitiesByKey) - } - assert.DeepEqual(t, got, tt.want, cmpopts.IgnoreTypes( - families.ScanMetadata{}, - VulnerabilityDiff{}.ASCIIDiff, - )) - }) - } -} diff --git a/scanner/families/vulnerabilities/types/vulnerability.go b/scanner/families/vulnerabilities/types/vulnerability.go index 5b74cd3dde..51c92583a6 100644 --- a/scanner/families/vulnerabilities/types/vulnerability.go +++ b/scanner/families/vulnerabilities/types/vulnerability.go @@ -20,7 +20,6 @@ import ( "fmt" "sort" - "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/yudai/gojsondiff" "github.com/yudai/gojsondiff/formatter" @@ -70,6 +69,16 @@ type Fix struct { State string `json:"state"` } +func (f Fix) enrich(ff Fix) Fix { + f.Versions = mergeSlicesWithoutDuplicates(f.Versions, ff.Versions) + + if f.State == "" || f.State == "unknown" { + f.State = ff.State + } + + return f +} + type Package struct { Name string `json:"name"` Version string `json:"version"` @@ -87,6 +96,20 @@ type Distro struct { IDLike []string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file } +func (d Distro) enrich(dd Distro) Distro { + if d.Name == "" { + d.Name = dd.Name + } + + if d.Version == "" { + d.Version = dd.Version + } + + d.IDLike = mergeSlicesWithoutDuplicates(d.IDLike, dd.IDLike) + + return d +} + type VulnerabilityKey string // Unique identification of a vulnerability ID per package (name and version) func NewVulnerabilityKey(vulnerability Vulnerability) VulnerabilityKey { @@ -99,60 +122,69 @@ type VulnerabilityDiff struct { ASCIIDiff string `json:"asciiDiff"` } -type MergedVulnerability struct { - ID string `json:"id"` // Merged vulnerability ID used in DiffInfo - THIS IS NOT THE CVE ID - Vulnerability Vulnerability `json:"vulnerability"` - Diffs []VulnerabilityDiff `json:"diffs"` - ScannersInfo []ScannerInfo `json:"scanners"` -} - -func (mv *MergedVulnerability) AppendScannerInfo(info ScannerInfo) *MergedVulnerability { - mv.ScannersInfo = append(mv.ScannersInfo, info) - return mv -} +func mergeSlicesWithoutDuplicates[T comparable](slice1, slice2 []T) []T { + elementMap := make(map[T]bool) + result := []T{} -func (mv *MergedVulnerability) AppendDiffInfo(diff VulnerabilityDiff) *MergedVulnerability { - mv.Diffs = append(mv.Diffs, diff) - return mv -} + for _, elem := range slice1 { + if !elementMap[elem] { + elementMap[elem] = true + result = append(result, elem) + } + } -func NewMergedVulnerability(vulnerability Vulnerability, info ScannerInfo) *MergedVulnerability { - return &MergedVulnerability{ - ID: uuid.New().String(), - Vulnerability: vulnerability, - ScannersInfo: []ScannerInfo{info}, + for _, elem := range slice2 { + if !elementMap[elem] { + elementMap[elem] = true + result = append(result, elem) + } } + + return result } -// handleVulnerabilityWithExistingKey will look for an identical vulnerability for the given otherVulnerability in the mergedVulnerabilities list, -// if identical vulnerability was found, the new scanner info (otherScannerInfo) will be added -// if no identical vulnerability was found, a new MergedVulnerability (with all the differences that was found) will be added. -func handleVulnerabilityWithExistingKey(mergedVulnerabilities []MergedVulnerability, otherVulnerability Vulnerability, otherScannerInfo ScannerInfo) []MergedVulnerability { - shouldAppendMergedVulnerabilityCandidate := true - mergedVulnerabilityCandidate := NewMergedVulnerability(otherVulnerability, otherScannerInfo) +func mergeCVSSSlicesWithoutDuplicates(cvss1, cvss2 []CVSS) []CVSS { + cvss := append(cvss1, cvss2...) - for i := range mergedVulnerabilities { - diff, err := getDiff(otherVulnerability, mergedVulnerabilities[i].Vulnerability, mergedVulnerabilities[i].ID) + // remove duplicates from cvss + cvssMap := make(map[string]CVSS) + for _, c := range cvss { + b, err := json.Marshal(c) if err != nil { - log.Warnf("Failed to calculate diff - keeping both vulnerabilities: %v", err) - } else if diff != nil { - // diff found - need to append diff info - log.Debugf("Vulnerability results from %v is different from %v. diff=%+v", mergedVulnerabilities[i].ScannersInfo, otherScannerInfo, diff) - mergedVulnerabilityCandidate = mergedVulnerabilityCandidate.AppendDiffInfo(*diff) - } else { - // no diff - need to append scanner info - log.Debugf("Vulnerability results from %v is equal to %v", mergedVulnerabilities[i].ScannersInfo, otherScannerInfo) - mergedVulnerabilities[i].AppendScannerInfo(otherScannerInfo) - shouldAppendMergedVulnerabilityCandidate = false - break + log.Errorf("failed to marshal cvss. c=%+v: %v", c, err) + continue } + cvssMap[string(b)] = c + } + + cvss = make([]CVSS, 0, len(cvssMap)) + for _, c := range cvssMap { + cvss = append(cvss, c) } - if shouldAppendMergedVulnerabilityCandidate { - mergedVulnerabilities = append(mergedVulnerabilities, *mergedVulnerabilityCandidate) + return cvss +} + +// handleVulnerabilityWithExistingKey merges two vulnerabilities with the same key. +func handleVulnerabilityWithExistingKey(vulnerability Vulnerability, otherVulnerability Vulnerability) Vulnerability { + // Adopt longest description + description := vulnerability.Description + if len(otherVulnerability.Description) > len(description) { + description = otherVulnerability.Description } - return mergedVulnerabilities + return Vulnerability{ + ID: vulnerability.ID, // Keep the original ID + Description: description, + Links: mergeSlicesWithoutDuplicates(vulnerability.Links, otherVulnerability.Links), + Distro: vulnerability.Distro.enrich(otherVulnerability.Distro), + CVSS: mergeCVSSSlicesWithoutDuplicates(vulnerability.CVSS, otherVulnerability.CVSS), + Fix: vulnerability.Fix.enrich(otherVulnerability.Fix), + Severity: vulnerability.Severity, // Keep the original severity + Package: vulnerability.Package, // Keep the original package + LayerID: vulnerability.LayerID, // Keep the original layerID + Path: vulnerability.Path, // Keep the original path + } } func getDiff(vulnerability, compareToVulnerability Vulnerability, compareToID string) (*VulnerabilityDiff, error) { @@ -216,28 +248,3 @@ func getASCIIFormatDiff(compareToVulnerabilityB []byte, diff gojsondiff.Diff) (s return asciiDiff, nil } - -func toVulnerabilityByKey(vulnerabilities []Vulnerability) map[VulnerabilityKey]Vulnerability { - ret := make(map[VulnerabilityKey]Vulnerability, len(vulnerabilities)) - for _, vulnerability := range vulnerabilities { - key := NewVulnerabilityKey(vulnerability) - if log.IsLevelEnabled(log.DebugLevel) { - if vul, ok := ret[key]; ok { - diff, err := getDiff(vul, vulnerability, "") - if err != nil { - // nolint:errchkjson - vulB, _ := json.Marshal(vul) - // nolint:errchkjson - newVulB, _ := json.Marshal(vulnerability) - log.Debugf("Existing vul with the same key %q. vul=%s, newVul=%s", key, vulB, newVulB) - } else if diff != nil { - log.Debugf("Existing vul with the same key %q. diff.JSONDiff=%+v", key, diff.JSONDiff) - } else { - log.Debugf("Existing vul with the same key %q - no diff", key) - } - } - } - ret[key] = vulnerability - } - return ret -}