Skip to content

Commit

Permalink
Merge indirect matches with direct matches (#2241)
Browse files Browse the repository at this point in the history
* allow for merging similar indirect matches to existing direct matches

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* address PR review comments

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
wagoodman authored Nov 7, 2024
1 parent d64b663 commit 787aae1
Show file tree
Hide file tree
Showing 11 changed files with 854 additions and 76 deletions.
36 changes: 36 additions & 0 deletions grype/match/details.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package match

import (
"fmt"
"strings"

"github.com/mitchellh/hashstructure/v2"
)
Expand Down Expand Up @@ -52,3 +53,38 @@ func (m Detail) ID() string {

return fmt.Sprintf("%x", f)
}

func (m Details) Len() int {
return len(m)
}

func (m Details) Less(i, j int) bool {
a := m[i]
b := m[j]

if a.Type != b.Type {
// exact-direct-match < exact-indirect-match < cpe-match

at := typeOrder[a.Type]
bt := typeOrder[b.Type]
if at == 0 {
return false
} else if bt == 0 {
return true
}
return at < bt
}

// sort by confidence
if a.Confidence != b.Confidence {
// flipped comparison since we want higher confidence to be first
return a.Confidence > b.Confidence
}

// if the types are the same, then sort by the ID (costly, but deterministic)
return strings.Compare(a.ID(), b.ID()) < 0
}

func (m Details) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
158 changes: 158 additions & 0 deletions grype/match/details_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package match

import (
"sort"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDetails_Sorting(t *testing.T) {

detailExactDirectHigh := Detail{
Type: ExactDirectMatch,
Confidence: 0.9,
SearchedBy: "attribute1",
Found: "value1",
Matcher: "matcher1",
}
detailExactDirectLow := Detail{
Type: ExactDirectMatch,
Confidence: 0.5,
SearchedBy: "attribute1",
Found: "value1",
Matcher: "matcher1",
}
detailExactIndirect := Detail{
Type: ExactIndirectMatch,
Confidence: 0.7,
SearchedBy: "attribute2",
Found: "value2",
Matcher: "matcher2",
}
detailCPEMatch := Detail{
Type: CPEMatch,
Confidence: 0.8,
SearchedBy: "attribute3",
Found: "value3",
Matcher: "matcher3",
}

tests := []struct {
name string
details Details
expected Details
}{
{
name: "sorts by type first, then by confidence",
details: Details{
detailCPEMatch,
detailExactDirectHigh,
detailExactIndirect,
detailExactDirectLow,
},
expected: Details{
detailExactDirectHigh,
detailExactDirectLow,
detailExactIndirect,
detailCPEMatch,
},
},
{
name: "sorts by confidence within the same type",
details: Details{
detailExactDirectLow,
detailExactDirectHigh,
},
expected: Details{
detailExactDirectHigh,
detailExactDirectLow,
},
},
{
name: "sorts by ID when type and confidence are the same",
details: Details{
// clone of detailExactDirectLow with slight difference to enforce ID sorting
{
Type: ExactDirectMatch,
Confidence: 0.5,
SearchedBy: "attribute2",
Found: "value2",
Matcher: "matcher2",
},
detailExactDirectLow,
},
expected: Details{
detailExactDirectLow,
{
Type: ExactDirectMatch,
Confidence: 0.5,
SearchedBy: "attribute2",
Found: "value2",
Matcher: "matcher2",
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sort.Sort(tt.details)
require.Equal(t, tt.expected, tt.details)
})
}
}

func TestHasExclusivelyAnyMatchTypes(t *testing.T) {
tests := []struct {
name string
details Details
types []Type
expected bool
}{
{
name: "all types allowed",
details: Details{{Type: "A"}, {Type: "B"}},
types: []Type{"A", "B"},
expected: true,
},
{
name: "mixed types with disallowed",
details: Details{{Type: "A"}, {Type: "B"}, {Type: "C"}},
types: []Type{"A", "B"},
expected: false,
},
{
name: "single allowed type",
details: Details{{Type: "A"}},
types: []Type{"A"},
expected: true,
},
{
name: "empty details",
details: Details{},
types: []Type{"A"},
expected: false,
},
{
name: "empty types list",
details: Details{{Type: "A"}},
types: []Type{},
expected: false,
},
{
name: "no match with disallowed type",
details: Details{{Type: "C"}},
types: []Type{"A", "B"},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasExclusivelyAnyMatchTypes(tt.details, tt.types...)
assert.Equal(t, tt.expected, result)
})
}
}
6 changes: 5 additions & 1 deletion grype/match/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import (
)

type Fingerprint struct {
coreFingerprint
vulnerabilityFixes string
}

type coreFingerprint struct {
vulnerabilityID string
vulnerabilityNamespace string
vulnerabilityFixes string
packageID pkg.ID // note: this encodes package name, version, type, location
}

Expand Down
18 changes: 8 additions & 10 deletions grype/match/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var ErrCannotMerge = fmt.Errorf("unable to merge vulnerability matches")
type Match struct {
Vulnerability vulnerability.Vulnerability // The vulnerability details of the match.
Package pkg.Package // The package used to search for a match.
Details Details // all ways in which how this particular match was made.
Details Details // all the ways this particular match was made.
}

// String is the string representation of select match fields.
Expand All @@ -28,10 +28,12 @@ func (m Match) String() string {

func (m Match) Fingerprint() Fingerprint {
return Fingerprint{
vulnerabilityID: m.Vulnerability.ID,
vulnerabilityNamespace: m.Vulnerability.Namespace,
vulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, ","),
packageID: m.Package.ID,
coreFingerprint: coreFingerprint{
vulnerabilityID: m.Vulnerability.ID,
vulnerabilityNamespace: m.Vulnerability.Namespace,
packageID: m.Package.ID,
},
vulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, ","),
}
}

Expand Down Expand Up @@ -73,11 +75,7 @@ func (m *Match) Merge(other Match) error {
}

// for stable output
sort.Slice(m.Details, func(i, j int) bool {
a := m.Details[i]
b := m.Details[j]
return strings.Compare(a.ID(), b.ID()) < 0
})
sort.Sort(m.Details)

// retain all unique CPEs for consistent output
m.Vulnerability.CPEs = cpe.Merge(m.Vulnerability.CPEs, other.Vulnerability.CPEs)
Expand Down
Loading

0 comments on commit 787aae1

Please sign in to comment.