feat: implement secondary sorting for default json output (#1403)

* feat: implement secondary sorting for default json output
---------
Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Christopher Angelo Phillips 2023-07-26 13:40:20 -04:00 committed by GitHub
parent eb6c3b0acd
commit 05edf62e62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 58 additions and 35 deletions

View file

@ -4,6 +4,7 @@ import (
"bytes"
"flag"
"regexp"
"sort"
"testing"
"github.com/stretchr/testify/assert"
@ -126,6 +127,18 @@ func TestEmptyJsonPresenter(t *testing.T) {
}
func TestPresenter_Present_NewDocumentSorted(t *testing.T) {
matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource)
doc, err := models.NewDocument(packages, context, matches, nil, metadataProvider, appConfig, dbStatus)
if err != nil {
t.Fatal(err)
}
if !sort.IsSorted(models.MatchSort(doc.Matches)) {
t.Errorf("expected matches to be sorted")
}
}
func redact(content []byte) []byte {
return timestampRegexp.ReplaceAll(content, []byte(`"timestamp":""`))
}

View file

@ -2,6 +2,7 @@ package models
import (
"fmt"
"sort"
"time"
"github.com/anchore/grype/grype/match"
@ -43,6 +44,8 @@ func NewDocument(packages []pkg.Package, context pkg.Context, matches match.Matc
findings = append(findings, *matchModel)
}
sort.Sort(MatchSort(findings))
var src *source
if context.Source != nil {
theSrc, err := newSource(*context.Source)

View file

@ -91,7 +91,7 @@ func TestPackagesAreSorted(t *testing.T) {
actualVulnerabilities = append(actualVulnerabilities, m.Vulnerability.ID)
}
assert.Equal(t, []string{"CVE-1999-0001", "CVE-1999-0002", "CVE-1999-0003"}, actualVulnerabilities)
assert.Equal(t, []string{"CVE-1999-0003", "CVE-1999-0002", "CVE-1999-0001"}, actualVulnerabilities)
}
func TestTimestampValidFormat(t *testing.T) {

View file

@ -60,30 +60,36 @@ func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.Metad
}, nil
}
var _ sort.Interface = (*ByName)(nil)
var _ sort.Interface = (*MatchSort)(nil)
type ByName []Match
type MatchSort []Match
// Len is the number of elements in the collection.
func (m ByName) Len() int {
func (m MatchSort) Len() int {
return len(m)
}
// Less reports whether the element with index i should sort before the element with index j.
func (m ByName) Less(i, j int) bool {
if m[i].Artifact.Name == m[j].Artifact.Name {
if m[i].Vulnerability.ID == m[j].Vulnerability.ID {
if m[i].Artifact.Version == m[j].Artifact.Version {
return m[i].Artifact.Type < m[j].Artifact.Type
// sort should consistent across presenters: name, version, type, severity, vulnerability
func (m MatchSort) Less(i, j int) bool {
matchI := m[i]
matchJ := m[j]
if matchI.Artifact.Name == matchJ.Artifact.Name {
if matchI.Artifact.Version == matchJ.Artifact.Version {
if matchI.Artifact.Type == matchJ.Artifact.Type {
if SeverityScore(matchI.Vulnerability.Severity) == SeverityScore(matchJ.Vulnerability.Severity) {
return matchI.Vulnerability.ID > matchJ.Vulnerability.ID
}
return SeverityScore(matchI.Vulnerability.Severity) > SeverityScore(matchJ.Vulnerability.Severity)
}
return m[i].Artifact.Version < m[j].Artifact.Version
return matchI.Artifact.Type < matchJ.Artifact.Type
}
return m[i].Vulnerability.ID < m[j].Vulnerability.ID
return matchI.Artifact.Version < matchJ.Artifact.Version
}
return m[i].Artifact.Name < m[j].Artifact.Name
return matchI.Artifact.Name < matchJ.Artifact.Name
}
// Swap swaps the elements with indexes i and j.
func (m ByName) Swap(i, j int) {
func (m MatchSort) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}

View file

@ -35,3 +35,23 @@ func NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Meta
Cvss: NewCVSS(metadata),
}
}
// returns severtiy score for presenter sorting purposes
func SeverityScore(severtiy string) int {
switch severtiy {
case "Unknown":
return 0
case "Negligible":
return 1
case "Low":
return 2
case "Medium":
return 3
case "High":
return 4
case "Critical":
return 5
default:
return 0
}
}

View file

@ -109,12 +109,12 @@ func sortRows(rows [][]string) [][]string {
if rows[i][name] == rows[j][name] {
if rows[i][ver] == rows[j][ver] {
if rows[i][packageType] == rows[j][packageType] {
if sevScore(rows[i][sev]) == sevScore(rows[j][sev]) {
if models.SeverityScore(rows[i][sev]) == models.SeverityScore(rows[j][sev]) {
// we use > here to get the most recently filed vulnerabilities
// to show at the top of the severity
return rows[i][vuln] > rows[j][vuln]
}
return sevScore(rows[i][sev]) > sevScore(rows[j][sev])
return models.SeverityScore(rows[i][sev]) > models.SeverityScore(rows[j][sev])
}
return rows[i][packageType] < rows[j][packageType]
}
@ -126,25 +126,6 @@ func sortRows(rows [][]string) [][]string {
return rows
}
func sevScore(sev string) int {
switch sev {
case "Unknown":
return 0
case "Negligible":
return 1
case "Low":
return 2
case "Medium":
return 3
case "High":
return 4
case "Critical":
return 5
default:
return 0
}
}
func removeDuplicateRows(items [][]string) [][]string {
seen := map[string][]string{}
var result [][]string

View file

@ -91,7 +91,7 @@ var FuncMap = func() template.FuncMap {
return collection
}
sort.Sort(models.ByName(matches))
sort.Sort(models.MatchSort(matches))
return matches
}
return f