From 5f285f3aaa07ab18416b1d2d89617acfdc8603b8 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 12 Jun 2023 16:02:21 -0400 Subject: [PATCH 01/49] WIP - shell to work on explain functionality Signed-off-by: Will Murphy WIP Signed-off-by: Will Murphy WIP: working explain command Signed-off-by: Will Murphy WIP track explanation model Signed-off-by: Will Murphy Working template for exact-indirect-matches Signed-off-by: Will Murphy Format CPE matches; add datasource URL Signed-off-by: Will Murphy some cleanup Signed-off-by: Will Murphy WIP: a few more details Signed-off-by: Will Murphy --- cmd/explain.go | 48 +++++++ cmd/grype/cli/legacy/cmd.go | 5 + grype/presenter/models/explain_cve.tmpl | 10 ++ grype/presenter/models/explanation.go | 161 ++++++++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 cmd/explain.go create mode 100644 grype/presenter/models/explain_cve.tmpl create mode 100644 grype/presenter/models/explanation.go diff --git a/cmd/explain.go b/cmd/explain.go new file mode 100644 index 00000000000..29404389392 --- /dev/null +++ b/cmd/explain.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/anchore/grype/grype/presenter/models" + "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/log" + "github.com/spf13/cobra" +) + +var cveIDs []string + +var explainCmd = &cobra.Command{ + Use: "explain --cve [CVE ID]", + Short: "Ask grype to explain a set of findings", + RunE: func(cmd *cobra.Command, args []string) error { + isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() + if err != nil { + log.Warnf("unable to determine if there is piped input: %+v", err) + isStdinPipeOrRedirect = false + } + if isStdinPipeOrRedirect { + // TODO: eventually detect different types of input; for now assume grype json + var parseResult models.Document + decoder := json.NewDecoder(os.Stdin) + err := decoder.Decode(&parseResult) + if err != nil { + return fmt.Errorf("unable to parse piped input: %+v", err) + } + explainer := models.NewVulnerabilityExplainer(parseResult, os.Stdout) + return explainer.ExplainByID(cveIDs) + } + // perform a scan, then explain requested CVEs + // TODO: implement + return fmt.Errorf("not implemented") + }, +} + +func init() { + setExplainFlags(explainCmd) +} + +func setExplainFlags(cmd *cobra.Command) { + cmd.Flags().StringArrayVarP(&cveIDs, "cve", "", nil, "CVE ID to explain") +} diff --git a/cmd/grype/cli/legacy/cmd.go b/cmd/grype/cli/legacy/cmd.go index 9c5520c2db5..93be965d880 100644 --- a/cmd/grype/cli/legacy/cmd.go +++ b/cmd/grype/cli/legacy/cmd.go @@ -38,6 +38,11 @@ func init() { ) } +func NewCli() *cobra.Command { + rootCmd.AddCommand(explainCmd) + return rootCmd +} + func Execute() { if err := rootCmd.Execute(); err != nil { _ = stderrPrintLnf(err.Error()) diff --git a/grype/presenter/models/explain_cve.tmpl b/grype/presenter/models/explain_cve.tmpl new file mode 100644 index 00000000000..ac889bb38b3 --- /dev/null +++ b/grype/presenter/models/explain_cve.tmpl @@ -0,0 +1,10 @@ +{{ .VulnerabilityID }} ({{ .Severity }}) for namespace "{{ .Namespace }}": +{{ .Description }} + matched artifacts: {{ range .MatchedPackages }} + package: {{ .Package.Name }}, aritfact ID: {{ .Package.ID }}, version: {{ .Package.Version }} + PURL: {{ .Package.PURL }}{{if .Explanation }} + {{ .Explanation }}{{end}} + Locations: {{ range .Package.Locations }} + - {{ .RealPath }}{{ end }}{{ end }} + URLs: {{ range .URLs }} + - {{ . }}{{ end }} diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go new file mode 100644 index 00000000000..8d7b2578a53 --- /dev/null +++ b/grype/presenter/models/explanation.go @@ -0,0 +1,161 @@ +package models + +import ( + _ "embed" + "fmt" + "io" + "text/template" + + "github.com/anchore/grype/grype/match" +) + +//go:embed explain_cve.tmpl +var templ string + +// ExplainedVulnerability explains a vulnerability match. +// It includes details about all matched artifacts and how they were +// matched. +type ExplainedVulnerability struct { + VulnerabilityID string + Severity string + Namespace string + Description string + MatchedPackages []MatchedPackage + URLs []string +} + +// TODO: this is basically a slice of matches. Is it needed? +// Maybe I need a different way to orient the slice of matches? +// having nice map/reduce functions is the thing that +// trips me up most writing Go code. + +type MatchedPackage struct { + Package Package + Details []MatchDetails + Explanation string +} + +type VulnerabilityExplainer interface { + ExplainByID(IDs []string) error + ExplainBySeverity(severity string) error + ExplainAll() error +} + +type vulnerabilityExplainer struct { + doc Document + w io.Writer + templ *template.Template +} + +func NewVulnerabilityExplainer(doc Document, w io.Writer) VulnerabilityExplainer { + return &vulnerabilityExplainer{ + doc: doc, + w: w, + templ: template.Must(template.New("explanation").Parse(templ)), + } +} + +func (e *vulnerabilityExplainer) ExplainByID(IDs []string) error { + toExplain := make(map[string]ExplainedVulnerability) + for _, id := range IDs { + explained := NewExplainedVulnerability(id, e.doc) + if explained != nil { + toExplain[id] = *explained + } + } + for _, id := range IDs { + explained, ok := toExplain[id] + if !ok { + continue + } + if err := e.templ.Execute(e.w, explained); err != nil { + return fmt.Errorf("unable to execute template: %w", err) + } + } + return nil +} + +func (e *vulnerabilityExplainer) ExplainBySeverity(severity string) error { + // TODO: implement + return nil +} + +func (e *vulnerabilityExplainer) ExplainAll() error { + return nil +} + +// NewExplainedVulnerability creates a new explained vulnerability. +func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedVulnerability { + relevantMatches := make([]Match, 0) + for _, m := range doc.Matches { + if m.Vulnerability.ID == vulnerabilityID { + relevantMatches = append(relevantMatches, m) + } else { + for _, r := range m.RelatedVulnerabilities { + if r.ID == vulnerabilityID { + relevantMatches = append(relevantMatches, m) + } + } + } + } + if len(relevantMatches) == 0 { + return nil + } + packages := make([]MatchedPackage, len(relevantMatches)) + for i, m := range relevantMatches { + packages[i] = ToMatchedPackage(m) + } + var URLs []string + for _, m := range relevantMatches { + URLs = append(URLs, m.Vulnerability.VulnerabilityMetadata.URLs...) + } + return &ExplainedVulnerability{ + VulnerabilityID: vulnerabilityID, + Severity: relevantMatches[0].Vulnerability.Severity, + Namespace: relevantMatches[0].Vulnerability.Namespace, + Description: relevantMatches[0].Vulnerability.Description, + MatchedPackages: packages, + URLs: append([]string{relevantMatches[0].Vulnerability.DataSource}, URLs...), + } +} + +func ToMatchedPackage(m Match) MatchedPackage { + explanation := "" + if len(m.MatchDetails) > 0 { + switch m.MatchDetails[0].Type { + case string(match.CPEMatch): + explanation = formatCPEExplanation(m) + case string(match.ExactIndirectMatch): + explanation = fmt.Sprintf("This CVE is reported against %s, the %s of this %s package.", m.Artifact.Upstreams[0].Name, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) + } + } + return MatchedPackage{ + Package: m.Artifact, + Details: m.MatchDetails, + Explanation: explanation, + } +} + +func formatCPEExplanation(m Match) string { + found := m.MatchDetails[0].Found + if mapResult, ok := found.(map[string]interface{}); ok { + if cpes, ok := mapResult["cpes"]; ok { + if cpeSlice, ok := cpes.([]interface{}); ok { + if len(cpeSlice) > 0 { + return fmt.Sprintf("CPE match on `%s`", cpeSlice[0]) + } + } + } + } + return "" +} + +func nameForUpstream(typ string) string { + switch typ { + case "deb": + return "origin" + case "rpm": + return "source RPM" + } + return "upstream" +} From 9a128d46060a42caa569274710410ef5d4243da3 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 26 Jun 2023 14:14:07 -0400 Subject: [PATCH 02/49] Add more info to explain output Signed-off-by: Will Murphy --- grype/presenter/models/explain_cve.tmpl | 3 +- grype/presenter/models/explanation.go | 75 ++++++++++++++++++------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/grype/presenter/models/explain_cve.tmpl b/grype/presenter/models/explain_cve.tmpl index ac889bb38b3..3fed09286a1 100644 --- a/grype/presenter/models/explain_cve.tmpl +++ b/grype/presenter/models/explain_cve.tmpl @@ -1,5 +1,6 @@ {{ .VulnerabilityID }} ({{ .Severity }}) for namespace "{{ .Namespace }}": -{{ .Description }} +{{ .Description }}{{ if .VersionConstraint }} +Version Constraint: {{ .VersionConstraint }}{{ end }} matched artifacts: {{ range .MatchedPackages }} package: {{ .Package.Name }}, aritfact ID: {{ .Package.ID }}, version: {{ .Package.Version }} PURL: {{ .Package.PURL }}{{if .Explanation }} diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go index 8d7b2578a53..04e5c2c867c 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/models/explanation.go @@ -16,12 +16,13 @@ var templ string // It includes details about all matched artifacts and how they were // matched. type ExplainedVulnerability struct { - VulnerabilityID string - Severity string - Namespace string - Description string - MatchedPackages []MatchedPackage - URLs []string + VulnerabilityID string + Severity string + Namespace string + Description string + VersionConstraint string + MatchedPackages []MatchedPackage + URLs []string } // TODO: this is basically a slice of matches. Is it needed? @@ -86,36 +87,51 @@ func (e *vulnerabilityExplainer) ExplainAll() error { // NewExplainedVulnerability creates a new explained vulnerability. func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedVulnerability { - relevantMatches := make([]Match, 0) + var specificMatches []Match + var relatedMatches []Match for _, m := range doc.Matches { if m.Vulnerability.ID == vulnerabilityID { - relevantMatches = append(relevantMatches, m) + specificMatches = append(specificMatches, m) } else { for _, r := range m.RelatedVulnerabilities { if r.ID == vulnerabilityID { - relevantMatches = append(relevantMatches, m) + relatedMatches = append(relatedMatches, m) } } } } - if len(relevantMatches) == 0 { + if len(specificMatches) == 0 { return nil } - packages := make([]MatchedPackage, len(relevantMatches)) - for i, m := range relevantMatches { + packages := make([]MatchedPackage, len(specificMatches)) + for i, m := range specificMatches { packages[i] = ToMatchedPackage(m) } var URLs []string - for _, m := range relevantMatches { + for _, m := range specificMatches { URLs = append(URLs, m.Vulnerability.VulnerabilityMetadata.URLs...) } + var versionConstraint string + for _, m := range specificMatches { + for _, d := range m.MatchDetails { + if mapResult, ok := d.Found.(map[string]interface{}); ok { + if version, ok := mapResult["versionConstraint"]; ok { + if stringVersion, ok := version.(string); ok { + versionConstraint = stringVersion + } + } + } + } + } + return &ExplainedVulnerability{ - VulnerabilityID: vulnerabilityID, - Severity: relevantMatches[0].Vulnerability.Severity, - Namespace: relevantMatches[0].Vulnerability.Namespace, - Description: relevantMatches[0].Vulnerability.Description, - MatchedPackages: packages, - URLs: append([]string{relevantMatches[0].Vulnerability.DataSource}, URLs...), + VulnerabilityID: vulnerabilityID, + Severity: specificMatches[0].Vulnerability.Severity, + Namespace: specificMatches[0].Vulnerability.Namespace, + Description: specificMatches[0].Vulnerability.Description, + VersionConstraint: versionConstraint, + MatchedPackages: packages, + URLs: append([]string{specificMatches[0].Vulnerability.DataSource}, URLs...), } } @@ -126,7 +142,8 @@ func ToMatchedPackage(m Match) MatchedPackage { case string(match.CPEMatch): explanation = formatCPEExplanation(m) case string(match.ExactIndirectMatch): - explanation = fmt.Sprintf("This CVE is reported against %s, the %s of this %s package.", m.Artifact.Upstreams[0].Name, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) + sourceName, sourceVersion := sourcePackageNameAndVersion(m.MatchDetails[0]) + explanation = fmt.Sprintf("Indirect match on source package: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) } } return MatchedPackage{ @@ -136,6 +153,24 @@ func ToMatchedPackage(m Match) MatchedPackage { } } +func sourcePackageNameAndVersion(md MatchDetails) (string, string) { + var name string + var version string + if mapResult, ok := md.SearchedBy.(map[string]interface{}); ok { + if sourcePackage, ok := mapResult["package"]; ok { + if sourceMap, ok := sourcePackage.(map[string]interface{}); ok { + if maybeName, ok := sourceMap["name"]; ok { + name, _ = maybeName.(string) + } + if maybeVersion, ok := sourceMap["version"]; ok { + version, _ = maybeVersion.(string) + } + } + } + } + return name, version +} + func formatCPEExplanation(m Match) string { found := m.MatchDetails[0].Found if mapResult, ok := found.(map[string]interface{}); ok { From de599228bca47f07c946d94cb8f99b2c5f87d499 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 26 Jun 2023 14:18:39 -0400 Subject: [PATCH 03/49] Dedube URLs and include related CVEs Signed-off-by: Will Murphy --- grype/presenter/models/explanation.go | 34 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go index 04e5c2c867c..40b5fcb2fd1 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/models/explanation.go @@ -87,11 +87,10 @@ func (e *vulnerabilityExplainer) ExplainAll() error { // NewExplainedVulnerability creates a new explained vulnerability. func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedVulnerability { - var specificMatches []Match var relatedMatches []Match for _, m := range doc.Matches { if m.Vulnerability.ID == vulnerabilityID { - specificMatches = append(specificMatches, m) + relatedMatches = append(relatedMatches, m) } else { for _, r := range m.RelatedVulnerabilities { if r.ID == vulnerabilityID { @@ -100,19 +99,19 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } } } - if len(specificMatches) == 0 { + if len(relatedMatches) == 0 { return nil } - packages := make([]MatchedPackage, len(specificMatches)) - for i, m := range specificMatches { + packages := make([]MatchedPackage, len(relatedMatches)) + for i, m := range relatedMatches { packages[i] = ToMatchedPackage(m) } var URLs []string - for _, m := range specificMatches { + for _, m := range relatedMatches { URLs = append(URLs, m.Vulnerability.VulnerabilityMetadata.URLs...) } var versionConstraint string - for _, m := range specificMatches { + for _, m := range relatedMatches { for _, d := range m.MatchDetails { if mapResult, ok := d.Found.(map[string]interface{}); ok { if version, ok := mapResult["versionConstraint"]; ok { @@ -126,15 +125,28 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV return &ExplainedVulnerability{ VulnerabilityID: vulnerabilityID, - Severity: specificMatches[0].Vulnerability.Severity, - Namespace: specificMatches[0].Vulnerability.Namespace, - Description: specificMatches[0].Vulnerability.Description, + Severity: relatedMatches[0].Vulnerability.Severity, + Namespace: relatedMatches[0].Vulnerability.Namespace, + Description: relatedMatches[0].Vulnerability.Description, VersionConstraint: versionConstraint, MatchedPackages: packages, - URLs: append([]string{specificMatches[0].Vulnerability.DataSource}, URLs...), + URLs: dedupeURLs(relatedMatches[0].Vulnerability.DataSource, URLs), } } +func dedupeURLs(showFirst string, rest []string) []string { + var result []string + result = append(result, showFirst) + deduplicate := make(map[string]bool) + for _, u := range rest { + if _, ok := deduplicate[u]; !ok && u != showFirst { + result = append(result, u) + deduplicate[u] = true + } + } + return result +} + func ToMatchedPackage(m Match) MatchedPackage { explanation := "" if len(m.MatchDetails) > 0 { From 6e1cd7ddb3c0bf14599a2f33ac45bd9348c73a82 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 26 Jun 2023 16:21:22 -0400 Subject: [PATCH 04/49] Fix CPE; clarify found vs searchedBy Signed-off-by: Will Murphy --- grype/presenter/models/explanation.go | 4 ++-- grype/presenter/models/match.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go index 40b5fcb2fd1..b11d1afc4c9 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/models/explanation.go @@ -184,8 +184,8 @@ func sourcePackageNameAndVersion(md MatchDetails) (string, string) { } func formatCPEExplanation(m Match) string { - found := m.MatchDetails[0].Found - if mapResult, ok := found.(map[string]interface{}); ok { + searchedBy := m.MatchDetails[0].SearchedBy + if mapResult, ok := searchedBy.(map[string]interface{}); ok { if cpes, ok := mapResult["cpes"]; ok { if cpeSlice, ok := cpes.([]interface{}); ok { if len(cpeSlice) > 0 { diff --git a/grype/presenter/models/match.go b/grype/presenter/models/match.go index 6b60aaa1b9b..333c8047070 100644 --- a/grype/presenter/models/match.go +++ b/grype/presenter/models/match.go @@ -21,8 +21,8 @@ type Match struct { type MatchDetails struct { Type string `json:"type"` Matcher string `json:"matcher"` - SearchedBy interface{} `json:"searchedBy"` - Found interface{} `json:"found"` + SearchedBy interface{} `json:"searchedBy"` // The specific attributes that were used to search (other than package name and version) --this indicates "how" the match was made. + Found interface{} `json:"found"` // The specific attributes on the vulnerability object that were matched with --this indicates "what" was matched on / within. } func newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.MetadataProvider) (*Match, error) { From 342c18170261dfa2f905438326be7f3441eaa705 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 27 Jun 2023 14:10:49 -0400 Subject: [PATCH 05/49] Group packages by PURL Previously they were grouped by artifact ID, which was noisy because it made a lot of similar, small-ish groups. Signed-off-by: Will Murphy --- grype/presenter/models/explain_cve.tmpl | 12 ++-- grype/presenter/models/explanation.go | 74 +++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/grype/presenter/models/explain_cve.tmpl b/grype/presenter/models/explain_cve.tmpl index 3fed09286a1..ee1054987ae 100644 --- a/grype/presenter/models/explain_cve.tmpl +++ b/grype/presenter/models/explain_cve.tmpl @@ -1,11 +1,11 @@ {{ .VulnerabilityID }} ({{ .Severity }}) for namespace "{{ .Namespace }}": {{ .Description }}{{ if .VersionConstraint }} Version Constraint: {{ .VersionConstraint }}{{ end }} - matched artifacts: {{ range .MatchedPackages }} - package: {{ .Package.Name }}, aritfact ID: {{ .Package.ID }}, version: {{ .Package.Version }} - PURL: {{ .Package.PURL }}{{if .Explanation }} - {{ .Explanation }}{{end}} - Locations: {{ range .Package.Locations }} - - {{ .RealPath }}{{ end }}{{ end }} + matched packages: {{ range .MatchedPackages }} + package: {{ .Name }}, version: {{ .Version }} + PURL: {{ .PURL }} + {{if .Explanation }}{{ .Explanation }}{{end}} + Evidenced by: {{ range .Locations }} + - (artifact ID: {{.ArtifactId}}): {{ .Location }}{{ end }}{{ end }} URLs: {{ range .URLs }} - {{ . }}{{ end }} diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go index b11d1afc4c9..c3d4ee1c4ac 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/models/explanation.go @@ -21,14 +21,17 @@ type ExplainedVulnerability struct { Namespace string Description string VersionConstraint string - MatchedPackages []MatchedPackage - URLs []string + // MatchedPackages map[string][]MatchedPackage // map of PURL to MatchedPackage + MatchedPackages []*ExplainedPackageMatch + URLs []string } // TODO: this is basically a slice of matches. Is it needed? // Maybe I need a different way to orient the slice of matches? // having nice map/reduce functions is the thing that // trips me up most writing Go code. +// Actually what we will do is build a slice of artifacts with +// the same PURL and then group them by PURL. type MatchedPackage struct { Package Package @@ -36,6 +39,19 @@ type MatchedPackage struct { Explanation string } +type ExplainedPackageMatch struct { + PURL string + Name string + Version string + Explanation string + Locations []LocatedArtifact +} + +type LocatedArtifact struct { + Location string + ArtifactId string +} + type VulnerabilityExplainer interface { ExplainByID(IDs []string) error ExplainBySeverity(severity string) error @@ -57,6 +73,8 @@ func NewVulnerabilityExplainer(doc Document, w io.Writer) VulnerabilityExplainer } func (e *vulnerabilityExplainer) ExplainByID(IDs []string) error { + // TODO: consider grouping all vulnerabilities by CVE ID + // and then doing this stuff toExplain := make(map[string]ExplainedVulnerability) for _, id := range IDs { explained := NewExplainedVulnerability(id, e.doc) @@ -102,9 +120,22 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV if len(relatedMatches) == 0 { return nil } - packages := make([]MatchedPackage, len(relatedMatches)) - for i, m := range relatedMatches { - packages[i] = ToMatchedPackage(m) + packages := make(map[string]*ExplainedPackageMatch) + for _, m := range relatedMatches { + if m.Artifact.PURL == "" { + continue + } + if explained, ok := packages[m.Artifact.PURL]; ok { + for _, location := range m.Artifact.Locations { + explained.Locations = append(explained.Locations, LocatedArtifact{ + Location: location.RealPath, + ArtifactId: m.Artifact.ID, + }) + } + } else { + explained := startExplainedPackageMatch(m) + packages[m.Artifact.PURL] = &explained + } } var URLs []string for _, m := range relatedMatches { @@ -122,6 +153,10 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } } } + var matchedPackages []*ExplainedPackageMatch + for _, explained := range packages { + matchedPackages = append(matchedPackages, explained) + } return &ExplainedVulnerability{ VulnerabilityID: vulnerabilityID, @@ -129,11 +164,38 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV Namespace: relatedMatches[0].Vulnerability.Namespace, Description: relatedMatches[0].Vulnerability.Description, VersionConstraint: versionConstraint, - MatchedPackages: packages, + MatchedPackages: matchedPackages, URLs: dedupeURLs(relatedMatches[0].Vulnerability.DataSource, URLs), } } +func startExplainedPackageMatch(m Match) ExplainedPackageMatch { + explanation := "" + if len(m.MatchDetails) > 0 { + switch m.MatchDetails[0].Type { + case string(match.CPEMatch): + explanation = formatCPEExplanation(m) + case string(match.ExactIndirectMatch): + sourceName, sourceVersion := sourcePackageNameAndVersion(m.MatchDetails[0]) + explanation = fmt.Sprintf("Indirect match on source package: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) + } + } + var locatedArtifacts []LocatedArtifact + for _, location := range m.Artifact.Locations { + locatedArtifacts = append(locatedArtifacts, LocatedArtifact{ + Location: location.RealPath, + ArtifactId: m.Artifact.ID, + }) + } + return ExplainedPackageMatch{ + PURL: m.Artifact.PURL, + Name: m.Artifact.Name, + Version: m.Artifact.Version, + Explanation: explanation, + Locations: locatedArtifacts, + } +} + func dedupeURLs(showFirst string, rest []string) []string { var result []string result = append(result, showFirst) From 9dbd15a476b18eb9292810b31419b35625082fba Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 28 Jun 2023 16:14:49 -0400 Subject: [PATCH 06/49] Implement formatting suggestions Signed-off-by: Will Murphy --- grype/presenter/models/explain_cve.tmpl | 15 +++++++------ grype/presenter/models/explanation.go | 30 ++++++++++++++++--------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/grype/presenter/models/explain_cve.tmpl b/grype/presenter/models/explain_cve.tmpl index ee1054987ae..8705ab70c61 100644 --- a/grype/presenter/models/explain_cve.tmpl +++ b/grype/presenter/models/explain_cve.tmpl @@ -1,11 +1,12 @@ {{ .VulnerabilityID }} ({{ .Severity }}) for namespace "{{ .Namespace }}": -{{ .Description }}{{ if .VersionConstraint }} -Version Constraint: {{ .VersionConstraint }}{{ end }} +{{ .Description }} + +URLs: {{ range .URLs }} + - {{ . }}{{ end }}{{ if .VersionConstraint }} +Versions Affected: {{ .VersionConstraint }}{{ end }} matched packages: {{ range .MatchedPackages }} package: {{ .Name }}, version: {{ .Version }} - PURL: {{ .PURL }} - {{if .Explanation }}{{ .Explanation }}{{end}} + PURL: {{ .PURL }}{{if .Explanation }} + {{ .Explanation }}{{end}} Evidenced by: {{ range .Locations }} - - (artifact ID: {{.ArtifactId}}): {{ .Location }}{{ end }}{{ end }} - URLs: {{ range .URLs }} - - {{ . }}{{ end }} + - (artifact ID: {{.ArtifactId}}): {{ .Location }}{{ if .ViaVulnID }}(via {{ .ViaVulnID }}){{ end }}{{ end }}{{ end }} diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go index c3d4ee1c4ac..812371715bf 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/models/explanation.go @@ -4,6 +4,7 @@ import ( _ "embed" "fmt" "io" + "strings" "text/template" "github.com/anchore/grype/grype/match" @@ -50,6 +51,7 @@ type ExplainedPackageMatch struct { type LocatedArtifact struct { Location string ArtifactId string + ViaVulnID string } type VulnerabilityExplainer interface { @@ -127,9 +129,14 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } if explained, ok := packages[m.Artifact.PURL]; ok { for _, location := range m.Artifact.Locations { + via := "" + if m.Vulnerability.ID != vulnerabilityID { + via = m.Vulnerability.ID + } explained.Locations = append(explained.Locations, LocatedArtifact{ Location: location.RealPath, ArtifactId: m.Artifact.ID, + ViaVulnID: via, }) } } else { @@ -143,14 +150,13 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } var versionConstraint string for _, m := range relatedMatches { - for _, d := range m.MatchDetails { - if mapResult, ok := d.Found.(map[string]interface{}); ok { - if version, ok := mapResult["versionConstraint"]; ok { - if stringVersion, ok := version.(string); ok { - versionConstraint = stringVersion - } - } - } + // TODO: which version constraint should we use? + // in other words, which match should win? + if len(m.Vulnerability.Fix.Versions) == 0 { + versionConstraint = "all versions" + } + if len(m.Vulnerability.Fix.Versions) == 1 { + versionConstraint = fmt.Sprintf("< %s", m.Vulnerability.Fix.Versions[0]) } } var matchedPackages []*ExplainedPackageMatch @@ -159,10 +165,12 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } return &ExplainedVulnerability{ - VulnerabilityID: vulnerabilityID, + VulnerabilityID: vulnerabilityID, + // TODO: which severity should we use? + // in other words, which match should win? Severity: relatedMatches[0].Vulnerability.Severity, Namespace: relatedMatches[0].Vulnerability.Namespace, - Description: relatedMatches[0].Vulnerability.Description, + Description: strings.TrimSpace(relatedMatches[0].Vulnerability.Description), VersionConstraint: versionConstraint, MatchedPackages: matchedPackages, URLs: dedupeURLs(relatedMatches[0].Vulnerability.DataSource, URLs), @@ -177,7 +185,7 @@ func startExplainedPackageMatch(m Match) ExplainedPackageMatch { explanation = formatCPEExplanation(m) case string(match.ExactIndirectMatch): sourceName, sourceVersion := sourcePackageNameAndVersion(m.MatchDetails[0]) - explanation = fmt.Sprintf("Indirect match on source package: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) + explanation = fmt.Sprintf("Note: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) } } var locatedArtifacts []LocatedArtifact From 79180bca9a192b30c40a15a541d465b5c0173887 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 28 Jun 2023 17:16:16 -0400 Subject: [PATCH 07/49] Distinguish direct and related matches Signed-off-by: Will Murphy --- grype/presenter/models/explain_cve.tmpl | 6 ++-- grype/presenter/models/explanation.go | 47 ++++++++++++++++++------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/grype/presenter/models/explain_cve.tmpl b/grype/presenter/models/explain_cve.tmpl index 8705ab70c61..1dcf00c862e 100644 --- a/grype/presenter/models/explain_cve.tmpl +++ b/grype/presenter/models/explain_cve.tmpl @@ -3,9 +3,9 @@ URLs: {{ range .URLs }} - {{ . }}{{ end }}{{ if .VersionConstraint }} -Versions Affected: {{ .VersionConstraint }}{{ end }} - matched packages: {{ range .MatchedPackages }} - package: {{ .Name }}, version: {{ .Version }} +Versions affected: {{ .VersionConstraint }}{{ end }} + Matched packages: {{ range .MatchedPackages }} + Package: {{ .Name }}, version: {{ .Version }} PURL: {{ .PURL }}{{if .Explanation }} {{ .Explanation }}{{end}} Evidenced by: {{ range .Locations }} diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go index 812371715bf..78e5927ae8c 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/models/explanation.go @@ -96,21 +96,41 @@ func (e *vulnerabilityExplainer) ExplainByID(IDs []string) error { return nil } -func (e *vulnerabilityExplainer) ExplainBySeverity(severity string) error { - // TODO: implement - return nil +func (e *vulnerabilityExplainer) ExplainBySeverity(minSeverity string) error { + uniqueSevereIDs := make(map[string]bool) + severity := vulnerability.ParseSeverity(minSeverity) + for _, m := range e.doc.Matches { + if vulnerability.ParseSeverity(metadata.Severity) >= severity { + uniqueSevereIDs[m.Vulnerability.ID] = true + } + } + var IDs []string + for id := range uniqueSevereIDs { + IDs = append(IDs, id) + } + return e.ExplainByID(IDs) } func (e *vulnerabilityExplainer) ExplainAll() error { - return nil + uniqueIDs := make(map[string]bool) + for _, m := range e.doc.Matches { + uniqueIDs[m.Vulnerability.ID] = true + } + var IDs []string + for id := range uniqueIDs { + IDs = append(IDs, id) + } + return e.ExplainByID(IDs) } // NewExplainedVulnerability creates a new explained vulnerability. func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedVulnerability { + var directMatches []Match var relatedMatches []Match for _, m := range doc.Matches { + // TODO: make the one that matches on the ID always be first? if m.Vulnerability.ID == vulnerabilityID { - relatedMatches = append(relatedMatches, m) + directMatches = append(directMatches, m) } else { for _, r := range m.RelatedVulnerabilities { if r.ID == vulnerabilityID { @@ -119,11 +139,12 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } } } - if len(relatedMatches) == 0 { + if len(directMatches) == 0 { return nil } packages := make(map[string]*ExplainedPackageMatch) - for _, m := range relatedMatches { + directAndRelatedMatches := append(directMatches, relatedMatches...) + for _, m := directAndRelatedMatches { if m.Artifact.PURL == "" { continue } @@ -145,11 +166,11 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } } var URLs []string - for _, m := range relatedMatches { + for _, m := range directAndRelatedMatches { URLs = append(URLs, m.Vulnerability.VulnerabilityMetadata.URLs...) } var versionConstraint string - for _, m := range relatedMatches { + for _, m := range directMatches { // TODO: which version constraint should we use? // in other words, which match should win? if len(m.Vulnerability.Fix.Versions) == 0 { @@ -168,12 +189,12 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV VulnerabilityID: vulnerabilityID, // TODO: which severity should we use? // in other words, which match should win? - Severity: relatedMatches[0].Vulnerability.Severity, - Namespace: relatedMatches[0].Vulnerability.Namespace, - Description: strings.TrimSpace(relatedMatches[0].Vulnerability.Description), + Severity: directMatches[0].Vulnerability.Severity, + Namespace: directMatches[0].Vulnerability.Namespace, + Description: strings.TrimSpace(directMatches[0].Vulnerability.Description), VersionConstraint: versionConstraint, MatchedPackages: matchedPackages, - URLs: dedupeURLs(relatedMatches[0].Vulnerability.DataSource, URLs), + URLs: dedupeURLs(directMatches[0].Vulnerability.DataSource, URLs), } } From 8cb78a7381917b0fae14d1c37de008218cbdc7c4 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Jun 2023 07:32:14 -0400 Subject: [PATCH 08/49] WIP: add new explain files Signed-off-by: Will Murphy --- grype/presenter/models/explain.go | 76 +++++++++++++++++++++ grype/presenter/models/explain_cve_new.tmpl | 19 ++++++ 2 files changed, 95 insertions(+) create mode 100644 grype/presenter/models/explain.go create mode 100644 grype/presenter/models/explain_cve_new.tmpl diff --git a/grype/presenter/models/explain.go b/grype/presenter/models/explain.go new file mode 100644 index 00000000000..db3ba3ab005 --- /dev/null +++ b/grype/presenter/models/explain.go @@ -0,0 +1,76 @@ +package models + +//go:embed explain_cve_new.tmpl +var explainTemplate string + +// TODO: basically re-write a lot of this +// Build a structure where an ExplainedVulnerability +// is basically the nvd:cpe record, plus a list of +// records that relate up to it. The convert that +// record to the ExplainViewModel building a list +// of artifacts we matched and why, and then +// render it either as JSON or as the template. + +type ExplainViewModel struct { + PrimaryVulnerability Vulnerability + RelatedVulnerabilities []Vulnerability + MatchedPackages []*ExplainedPackage + URLs []string +} + +type ExplainViewModelBuilder struct { + PrimaryMatch Match + RelatedMatches []Match +} + +type ExplainedFindings map[string]ExplainViewModel + +type ExplainedPackage struct { + PURL string + Name string + Version string // TODO: is there only going to be one of these? + Explanation string + MatchedOnID string + MatchedOnNamespace string + Locations []LocatedArtifact +} + +func NewExplainedVulnerabilityBuilder() *ExplainViewModelBuilder { + return &ExplainViewModelBuilder{} +} + +func (b *ExplainViewModelBuilder) WithPrimaryVulnerability(m Match) *ExplainViewModelBuilder { + b.PrimaryMatch = m + return b +} + +func (b *ExplainViewModelBuilder) WithRelatedVulnerability(m Match) *ExplainViewModelBuilder { + b.RelatedMatches = append(b.RelatedMatches, m) + return b +} + +func (b *ExplainViewModelBuilder) Build() ExplainViewModel { + primaryURL := b.PrimaryMatch.Vulnerability.DataSource + URLs := b.PrimaryMatch.Vulnerability.URLs + for _, m := range b.RelatedMatches { + URLs = append(URLs, m.Vulnerability.URLs...) + } + + // pURLsToMatchDetails := make(map[string][]MatchDetails) + + var relatedVulnerabilities []Vulnerability + for _, m := range b.RelatedMatches { + relatedVulnerabilities = append(relatedVulnerabilities, m.Vulnerability) + } + + // var explainedPackages []*ExplainedPackage + // for k, v := range pURLsToMatchDetails { + + // } + + return ExplainViewModel{ + PrimaryVulnerability: b.PrimaryMatch.Vulnerability, + RelatedVulnerabilities: relatedVulnerabilities, + URLs: dedupeURLs(primaryURL, URLs), + } +} diff --git a/grype/presenter/models/explain_cve_new.tmpl b/grype/presenter/models/explain_cve_new.tmpl new file mode 100644 index 00000000000..d81ee4bb72b --- /dev/null +++ b/grype/presenter/models/explain_cve_new.tmpl @@ -0,0 +1,19 @@ +{{ .PrimaryVulnerability.ID }} ({{ .PrimaryVulnerability.Severity }} + +{{ .PrimaryVulnerability.Description }} +{{ if .RelatedVulnerabilities }}Related vulnerabilities: +{{ range .RelatedVulnerabilities }} + - {{.Namespace}}: {{ .ID }} {{ .Severity }}{{end}}{{end}} + +URLs: {{ range .URLs }} + - {{ . }}{{ end }} + +# can be from one or more matches... + +Matched packages: {{ range .MatchedPackages }} + Package: {{ .Name }}, version: {{ .Version }} + Matched on : by version criteria + PURL: {{ .PURL }}{{if .Explanation }} + {{ .Explanation }}{{end}} + Evidenced by: {{ range .Locations }} + - (artifact ID: {{.ArtifactId}}): {{ .Location }}{{ if .ViaVulnID }}(via {{ .ViaVulnID }}){{ end }}{{ end }}{{ end }} From 5dc174ef1aaf093a7a7ff6114b9f111d34a82b8a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Jun 2023 13:47:28 -0400 Subject: [PATCH 09/49] WIP: trying to get match details and locations explained Signed-off-by: Will Murphy --- cmd/explain.go | 2 +- grype/presenter/models/explain.go | 235 +++++++++++++++++++- grype/presenter/models/explain_cve_new.tmpl | 18 +- grype/presenter/models/explanation.go | 6 +- 4 files changed, 239 insertions(+), 22 deletions(-) diff --git a/cmd/explain.go b/cmd/explain.go index 29404389392..981604faec0 100644 --- a/cmd/explain.go +++ b/cmd/explain.go @@ -30,7 +30,7 @@ var explainCmd = &cobra.Command{ if err != nil { return fmt.Errorf("unable to parse piped input: %+v", err) } - explainer := models.NewVulnerabilityExplainer(parseResult, os.Stdout) + explainer := models.NewBetterVulnerabilityExplainer(os.Stdout, &parseResult) return explainer.ExplainByID(cveIDs) } // perform a scan, then explain requested CVEs diff --git a/grype/presenter/models/explain.go b/grype/presenter/models/explain.go index db3ba3ab005..a6a67892ae4 100644 --- a/grype/presenter/models/explain.go +++ b/grype/presenter/models/explain.go @@ -1,5 +1,15 @@ package models +import ( + _ "embed" + "fmt" + "io" + "sort" + "text/template" + + "github.com/anchore/grype/grype/match" +) + //go:embed explain_cve_new.tmpl var explainTemplate string @@ -29,22 +39,156 @@ type ExplainedPackage struct { PURL string Name string Version string // TODO: is there only going to be one of these? - Explanation string MatchedOnID string MatchedOnNamespace string - Locations []LocatedArtifact + Locations []ExplainedEvidence +} + +type ExplainedEvidence struct { + Location string + Explanation string + ArtifactId string + ViaVulnID string + ViaNamespace string +} + +type betterVulnerabilityExplainer struct { + w io.Writer + doc *Document +} + +func NewBetterVulnerabilityExplainer(w io.Writer, doc *Document) VulnerabilityExplainer { + return &betterVulnerabilityExplainer{ + w: w, + doc: doc, + } +} + +func (e *betterVulnerabilityExplainer) ExplainByID(IDs []string) error { + // TODO: requested ID is always the primary match + findings, err := ExplainDoc(e.doc, IDs) + if err != nil { + return err + } + t := template.Must(template.New("explanation").Parse(explainTemplate)) + for _, id := range IDs { + finding, ok := findings[id] + if !ok { + continue + } + if err := t.Execute(e.w, finding); err != nil { + return fmt.Errorf("unable to execute template: %w", err) + } + } + return nil +} + +func (e *betterVulnerabilityExplainer) ExplainBySeverity(severity string) error { + return fmt.Errorf("not implemented") +} + +func (e *betterVulnerabilityExplainer) ExplainAll() error { + findings, err := ExplainDoc(e.doc, nil) + if err != nil { + return err + } + t := template.Must(template.New("explanation").Parse(explainTemplate)) + + return t.Execute(e.w, findings) +} + +func ExplainDoc(doc *Document, requestedIDs []string) (ExplainedFindings, error) { + result := make(ExplainedFindings) + builders := make(map[string]*ExplainViewModelBuilder) + for _, m := range doc.Matches { + key := m.Vulnerability.ID + existing, ok := builders[key] + if !ok { + existing = NewExplainedVulnerabilityBuilder() + builders[m.Vulnerability.ID] = existing + } + existing.WithMatch(m, requestedIDs, false) + } + for _, m := range doc.Matches { + for _, related := range m.RelatedVulnerabilities { + key := related.ID + existing, ok := builders[key] + if !ok { + existing = NewExplainedVulnerabilityBuilder() + builders[key] = existing + } + existing.WithMatch(m, requestedIDs, false) + } + } + for k, v := range builders { + result[k] = v.Build() + } + return result, nil } func NewExplainedVulnerabilityBuilder() *ExplainViewModelBuilder { return &ExplainViewModelBuilder{} } -func (b *ExplainViewModelBuilder) WithPrimaryVulnerability(m Match) *ExplainViewModelBuilder { +// WithMatch adds a match to the builder +// accepting enough information to determine whether the match is a primary match or a related match +func (b *ExplainViewModelBuilder) WithMatch(m Match, userRequestedIDs []string, graphIsByCVE bool) { + // TODO: check if primary match + // First match is always primary + // Next match is "more primary" if it has the nvd:cpe namespace and no related vulnerabilities + if b.isPrimaryAdd(m, userRequestedIDs, graphIsByCVE) { + // Demote the current primary match to related match + // if it exists + if b.PrimaryMatch.Vulnerability.ID != "" { + b.WithRelatedMatch(b.PrimaryMatch) + } + b.WithPrimaryMatch(m) + } else { + b.WithRelatedMatch(m) + } +} + +func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate Match, userRequestedIDs []string, graphIsByCVE bool) bool { + // if there's not currently any match, make this one primary since we don't know any better + if b.PrimaryMatch.Vulnerability.ID == "" { + return true + } + // There is a primary match, so we need to determine if the candidate is "more primary" + if graphIsByCVE { + panic("not implemented") // by-cve graphs are upside down. + } + idWasRequested := false + for _, id := range userRequestedIDs { + if candidate.Vulnerability.ID == id { + idWasRequested = true + break + } + } + // We're making graphs of specifically requested IDs, and the user didn't ask about + // this ID, so it can't be primary + if !idWasRequested && len(userRequestedIDs) > 0 { + return false + } + // Either the user didn't ask for specific IDs, or the candidate has an ID the user asked for. + currentPrimaryIsChildOfCandidate := false + for _, related := range b.PrimaryMatch.RelatedVulnerabilities { + if related.ID == candidate.Vulnerability.ID { + currentPrimaryIsChildOfCandidate = true + break + } + } + if currentPrimaryIsChildOfCandidate { + return true + } + return false +} + +func (b *ExplainViewModelBuilder) WithPrimaryMatch(m Match) *ExplainViewModelBuilder { b.PrimaryMatch = m return b } -func (b *ExplainViewModelBuilder) WithRelatedVulnerability(m Match) *ExplainViewModelBuilder { +func (b *ExplainViewModelBuilder) WithRelatedMatch(m Match) *ExplainViewModelBuilder { b.RelatedMatches = append(b.RelatedMatches, m) return b } @@ -56,11 +200,73 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { URLs = append(URLs, m.Vulnerability.URLs...) } - // pURLsToMatchDetails := make(map[string][]MatchDetails) + pURLsToMatchDetails := make(map[string]*ExplainedPackage) + for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { + key := m.Artifact.PURL + // TODO: match details can match multiple packages + var newLocations []ExplainedEvidence + for _, l := range m.Artifact.Locations { + newLocations = append(newLocations, ExplainedEvidence{ + Location: l.RealPath, + ArtifactId: m.Artifact.ID, + ViaVulnID: m.Vulnerability.ID, + ViaNamespace: m.Vulnerability.Namespace, + }) + } + // TODO: how can match details explain locations? + // Like, I have N matchDetails, and N locations, but I don't know which matchDetail explains which location + var newExplanations []string + for i := range m.MatchDetails { + explanation := explainMatchDetail(m, i) + if explanation != "" { + newExplanations = append(newExplanations, explanation) + } + } + e, ok := pURLsToMatchDetails[key] + if !ok { + e = &ExplainedPackage{ + PURL: m.Artifact.PURL, + Name: m.Artifact.Name, + Version: m.Artifact.Version, + MatchedOnID: m.Vulnerability.ID, + MatchedOnNamespace: m.Vulnerability.Namespace, + Locations: newLocations, + } + pURLsToMatchDetails[key] = e + } else { + // TODO: what if MatchedOnID and MatchedOnNamespace are different? + e.Locations = append(e.Locations, newLocations...) + // e.Explanations = append(e.Explanations, newExplanations...) + // if e.MatchedOnID != m.Vulnerability.ID || e.MatchedOnNamespace != m.Vulnerability.Namespace { + // // TODO: do something smart. + // panic("matched on different vulnerabilities") + // } + } + } + var sortPURLs []string + for k := range pURLsToMatchDetails { + sortPURLs = append(sortPURLs, k) + } + sort.Strings(sortPURLs) + var explainedPackages []*ExplainedPackage + for _, k := range sortPURLs { + explainedPackages = append(explainedPackages, pURLsToMatchDetails[k]) + } var relatedVulnerabilities []Vulnerability + var dedupeRelatedVulnerabilities = make(map[string]Vulnerability) + var sortDedupedRelatedVulnerabilities []string for _, m := range b.RelatedMatches { - relatedVulnerabilities = append(relatedVulnerabilities, m.Vulnerability) + key := fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID) + dedupeRelatedVulnerabilities[key] = m.Vulnerability + } + + for k := range dedupeRelatedVulnerabilities { + sortDedupedRelatedVulnerabilities = append(sortDedupedRelatedVulnerabilities, k) + } + sort.Strings(sortDedupedRelatedVulnerabilities) + for _, k := range sortDedupedRelatedVulnerabilities { + relatedVulnerabilities = append(relatedVulnerabilities, dedupeRelatedVulnerabilities[k]) } // var explainedPackages []*ExplainedPackage @@ -71,6 +277,23 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { return ExplainViewModel{ PrimaryVulnerability: b.PrimaryMatch.Vulnerability, RelatedVulnerabilities: relatedVulnerabilities, + MatchedPackages: explainedPackages, URLs: dedupeURLs(primaryURL, URLs), } } + +func explainMatchDetail(m Match, index int) string { + if len(m.MatchDetails) <= index { + return "" + } + md := m.MatchDetails[index] + explanation := "" + switch md.Type { + case string(match.CPEMatch): + explanation = formatCPEExplanation(m) + case string(match.ExactIndirectMatch): + sourceName, sourceVersion := sourcePackageNameAndVersion(md) + explanation = fmt.Sprintf("Note: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) + } + return explanation +} diff --git a/grype/presenter/models/explain_cve_new.tmpl b/grype/presenter/models/explain_cve_new.tmpl index d81ee4bb72b..33c9124de57 100644 --- a/grype/presenter/models/explain_cve_new.tmpl +++ b/grype/presenter/models/explain_cve_new.tmpl @@ -1,19 +1,13 @@ -{{ .PrimaryVulnerability.ID }} ({{ .PrimaryVulnerability.Severity }} - +{{ .PrimaryVulnerability.ID }} ({{ .PrimaryVulnerability.Severity }}) {{ .PrimaryVulnerability.Description }} {{ if .RelatedVulnerabilities }}Related vulnerabilities: {{ range .RelatedVulnerabilities }} - - {{.Namespace}}: {{ .ID }} {{ .Severity }}{{end}}{{end}} - + - {{.Namespace}} {{ .ID }} {{ .Severity }}{{end}}{{end}} URLs: {{ range .URLs }} - {{ . }}{{ end }} - -# can be from one or more matches... - Matched packages: {{ range .MatchedPackages }} Package: {{ .Name }}, version: {{ .Version }} - Matched on : by version criteria - PURL: {{ .PURL }}{{if .Explanation }} - {{ .Explanation }}{{end}} - Evidenced by: {{ range .Locations }} - - (artifact ID: {{.ArtifactId}}): {{ .Location }}{{ if .ViaVulnID }}(via {{ .ViaVulnID }}){{ end }}{{ end }}{{ end }} + PURL: {{ .PURL }} + Evidenced by: {{ range .Locations }}: + - {{ .ViaNamespace }}:{{ .ViaVulnID }} found {{ .ArtifactId }} at {{ .Location }}{{ if .Explanation }} + {{ .Explanation }}{{ end }}{{ end }}{{ end }} \ No newline at end of file diff --git a/grype/presenter/models/explanation.go b/grype/presenter/models/explanation.go index 78e5927ae8c..f9381c16189 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/models/explanation.go @@ -8,6 +8,7 @@ import ( "text/template" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/vulnerability" ) //go:embed explain_cve.tmpl @@ -47,7 +48,6 @@ type ExplainedPackageMatch struct { Explanation string Locations []LocatedArtifact } - type LocatedArtifact struct { Location string ArtifactId string @@ -100,7 +100,7 @@ func (e *vulnerabilityExplainer) ExplainBySeverity(minSeverity string) error { uniqueSevereIDs := make(map[string]bool) severity := vulnerability.ParseSeverity(minSeverity) for _, m := range e.doc.Matches { - if vulnerability.ParseSeverity(metadata.Severity) >= severity { + if vulnerability.ParseSeverity(m.Vulnerability.Severity) >= severity { uniqueSevereIDs[m.Vulnerability.ID] = true } } @@ -144,7 +144,7 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } packages := make(map[string]*ExplainedPackageMatch) directAndRelatedMatches := append(directMatches, relatedMatches...) - for _, m := directAndRelatedMatches { + for _, m := range directAndRelatedMatches { if m.Artifact.PURL == "" { continue } From 6553fe4b53f2ae897b69cace3df1d920692c9934 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Jun 2023 14:24:03 -0400 Subject: [PATCH 10/49] Fixed explanations Signed-off-by: Will Murphy --- grype/presenter/models/explain.go | 56 ++++++++++++++------- grype/presenter/models/explain_cve_new.tmpl | 12 ++--- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/grype/presenter/models/explain.go b/grype/presenter/models/explain.go index a6a67892ae4..ea2d6f8a0f4 100644 --- a/grype/presenter/models/explain.go +++ b/grype/presenter/models/explain.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "sort" + "strings" "text/template" "github.com/anchore/grype/grype/match" @@ -36,17 +37,18 @@ type ExplainViewModelBuilder struct { type ExplainedFindings map[string]ExplainViewModel type ExplainedPackage struct { - PURL string - Name string - Version string // TODO: is there only going to be one of these? - MatchedOnID string - MatchedOnNamespace string - Locations []ExplainedEvidence + PURL string + Name string + Version string // TODO: is there only going to be one of these? + MatchedOnID string + MatchedOnNamespace string + IndirectExplanation string + CPEExplanation string + Locations []ExplainedEvidence } type ExplainedEvidence struct { Location string - Explanation string ArtifactId string ViaVulnID string ViaNamespace string @@ -64,13 +66,17 @@ func NewBetterVulnerabilityExplainer(w io.Writer, doc *Document) VulnerabilityEx } } +var funcs = template.FuncMap{ + "trim": strings.TrimSpace, +} + func (e *betterVulnerabilityExplainer) ExplainByID(IDs []string) error { // TODO: requested ID is always the primary match findings, err := ExplainDoc(e.doc, IDs) if err != nil { return err } - t := template.Must(template.New("explanation").Parse(explainTemplate)) + t := template.Must(template.New("explanation").Funcs(funcs).Parse(explainTemplate)) for _, id := range IDs { finding, ok := findings[id] if !ok { @@ -92,7 +98,7 @@ func (e *betterVulnerabilityExplainer) ExplainAll() error { if err != nil { return err } - t := template.Must(template.New("explanation").Parse(explainTemplate)) + t := template.Must(template.New("explanation").Funcs(funcs).Parse(explainTemplate)) return t.Execute(e.w, findings) } @@ -215,27 +221,41 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } // TODO: how can match details explain locations? // Like, I have N matchDetails, and N locations, but I don't know which matchDetail explains which location - var newExplanations []string - for i := range m.MatchDetails { + var indirectExplanation string + var cpeExplanation string + for i, md := range m.MatchDetails { explanation := explainMatchDetail(m, i) if explanation != "" { - newExplanations = append(newExplanations, explanation) + if md.Type == string(match.CPEMatch) { + cpeExplanation = explanation + } + if md.Type == string(match.ExactIndirectMatch) { + indirectExplanation = explanation + } } } e, ok := pURLsToMatchDetails[key] if !ok { e = &ExplainedPackage{ - PURL: m.Artifact.PURL, - Name: m.Artifact.Name, - Version: m.Artifact.Version, - MatchedOnID: m.Vulnerability.ID, - MatchedOnNamespace: m.Vulnerability.Namespace, - Locations: newLocations, + PURL: m.Artifact.PURL, + Name: m.Artifact.Name, + Version: m.Artifact.Version, + MatchedOnID: m.Vulnerability.ID, + MatchedOnNamespace: m.Vulnerability.Namespace, + IndirectExplanation: indirectExplanation, + CPEExplanation: cpeExplanation, + Locations: newLocations, } pURLsToMatchDetails[key] = e } else { // TODO: what if MatchedOnID and MatchedOnNamespace are different? e.Locations = append(e.Locations, newLocations...) + if e.CPEExplanation == "" { + e.CPEExplanation = cpeExplanation + } + if e.IndirectExplanation == "" { + e.IndirectExplanation = indirectExplanation + } // e.Explanations = append(e.Explanations, newExplanations...) // if e.MatchedOnID != m.Vulnerability.ID || e.MatchedOnNamespace != m.Vulnerability.Namespace { // // TODO: do something smart. diff --git a/grype/presenter/models/explain_cve_new.tmpl b/grype/presenter/models/explain_cve_new.tmpl index 33c9124de57..2c984b12a8e 100644 --- a/grype/presenter/models/explain_cve_new.tmpl +++ b/grype/presenter/models/explain_cve_new.tmpl @@ -1,13 +1,13 @@ {{ .PrimaryVulnerability.ID }} ({{ .PrimaryVulnerability.Severity }}) -{{ .PrimaryVulnerability.Description }} -{{ if .RelatedVulnerabilities }}Related vulnerabilities: -{{ range .RelatedVulnerabilities }} +{{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} +Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} {{ .Severity }}{{end}}{{end}} URLs: {{ range .URLs }} - {{ . }}{{ end }} Matched packages: {{ range .MatchedPackages }} Package: {{ .Name }}, version: {{ .Version }} - PURL: {{ .PURL }} + PURL: {{ .PURL }}{{ if .CPEExplanation }} + {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} + {{ .IndirectExplanation }}{{ end }} Evidenced by: {{ range .Locations }}: - - {{ .ViaNamespace }}:{{ .ViaVulnID }} found {{ .ArtifactId }} at {{ .Location }}{{ if .Explanation }} - {{ .Explanation }}{{ end }}{{ end }}{{ end }} \ No newline at end of file + - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} From 1eeecb32a6fbbf508b0a3994ecab7bf74e8be958 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Jun 2023 14:27:41 -0400 Subject: [PATCH 11/49] Remove primary vulnerability from related ones Signed-off-by: Will Murphy --- grype/presenter/models/explain.go | 2 ++ grype/presenter/models/explain_cve_new.tmpl | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/grype/presenter/models/explain.go b/grype/presenter/models/explain.go index ea2d6f8a0f4..0b95445bd5d 100644 --- a/grype/presenter/models/explain.go +++ b/grype/presenter/models/explain.go @@ -280,6 +280,8 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { key := fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID) dedupeRelatedVulnerabilities[key] = m.Vulnerability } + // delete the primary vulnerability from the related vulnerabilities + delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", b.PrimaryMatch.Vulnerability.Namespace, b.PrimaryMatch.Vulnerability.ID)) for k := range dedupeRelatedVulnerabilities { sortDedupedRelatedVulnerabilities = append(sortDedupedRelatedVulnerabilities, k) diff --git a/grype/presenter/models/explain_cve_new.tmpl b/grype/presenter/models/explain_cve_new.tmpl index 2c984b12a8e..c3a677c0eb6 100644 --- a/grype/presenter/models/explain_cve_new.tmpl +++ b/grype/presenter/models/explain_cve_new.tmpl @@ -1,9 +1,9 @@ -{{ .PrimaryVulnerability.ID }} ({{ .PrimaryVulnerability.Severity }}) +{{ .PrimaryVulnerability.ID }} from {{ .PrimaryVulnerability.Namespace }} ({{ .PrimaryVulnerability.Severity }}) {{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} {{ .Severity }}{{end}}{{end}} URLs: {{ range .URLs }} - - {{ . }}{{ end }} + - {{ . }}{{ end }} Matched packages: {{ range .MatchedPackages }} Package: {{ .Name }}, version: {{ .Version }} PURL: {{ .PURL }}{{ if .CPEExplanation }} From c724909d9b973f7beee0d229f4f45d6ebbc9dc4c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Jun 2023 15:41:42 -0400 Subject: [PATCH 12/49] Re-arrange primary vs related Previous code assumed primary was a property of a match, but really it's a property of a vulnerability. Rearrange the code to choose a pimary vulnerability. Signed-off-by: Will Murphy --- grype/presenter/models/explain.go | 51 +++++++++++++-------- grype/presenter/models/explain_cve_new.tmpl | 2 +- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/grype/presenter/models/explain.go b/grype/presenter/models/explain.go index 0b95445bd5d..9dd6c05754c 100644 --- a/grype/presenter/models/explain.go +++ b/grype/presenter/models/explain.go @@ -23,15 +23,16 @@ var explainTemplate string // render it either as JSON or as the template. type ExplainViewModel struct { - PrimaryVulnerability Vulnerability - RelatedVulnerabilities []Vulnerability + PrimaryVulnerability VulnerabilityMetadata + RelatedVulnerabilities []VulnerabilityMetadata MatchedPackages []*ExplainedPackage URLs []string } type ExplainViewModelBuilder struct { - PrimaryMatch Match - RelatedMatches []Match + PrimaryVulnerability Vulnerability // this is the vulnerability we're trying to explain + PrimaryMatch Match + RelatedMatches []Match } type ExplainedFindings map[string]ExplainViewModel @@ -139,9 +140,8 @@ func NewExplainedVulnerabilityBuilder() *ExplainViewModelBuilder { // WithMatch adds a match to the builder // accepting enough information to determine whether the match is a primary match or a related match func (b *ExplainViewModelBuilder) WithMatch(m Match, userRequestedIDs []string, graphIsByCVE bool) { - // TODO: check if primary match - // First match is always primary - // Next match is "more primary" if it has the nvd:cpe namespace and no related vulnerabilities + // TODO: check if it's a primary vulnerability + // (the below checks if it's a primary _match_, which is wrong) if b.isPrimaryAdd(m, userRequestedIDs, graphIsByCVE) { // Demote the current primary match to related match // if it exists @@ -155,6 +155,7 @@ func (b *ExplainViewModelBuilder) WithMatch(m Match, userRequestedIDs []string, } func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate Match, userRequestedIDs []string, graphIsByCVE bool) bool { + // TODO: "primary" is a property of a vulnerability, not a match // if there's not currently any match, make this one primary since we don't know any better if b.PrimaryMatch.Vulnerability.ID == "" { return true @@ -214,7 +215,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { for _, l := range m.Artifact.Locations { newLocations = append(newLocations, ExplainedEvidence{ Location: l.RealPath, - ArtifactId: m.Artifact.ID, + ArtifactId: m.Artifact.ID, // TODO: this is sometimes blank. Why? ViaVulnID: m.Vulnerability.ID, ViaNamespace: m.Vulnerability.Namespace, }) @@ -273,16 +274,31 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { explainedPackages = append(explainedPackages, pURLsToMatchDetails[k]) } - var relatedVulnerabilities []Vulnerability - var dedupeRelatedVulnerabilities = make(map[string]Vulnerability) + // TODO: this isn't right at all. + // We need to be able to add related vulnerabilities + var relatedVulnerabilities []VulnerabilityMetadata + var dedupeRelatedVulnerabilities = make(map[string]VulnerabilityMetadata) var sortDedupedRelatedVulnerabilities []string - for _, m := range b.RelatedMatches { + for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { key := fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID) - dedupeRelatedVulnerabilities[key] = m.Vulnerability + dedupeRelatedVulnerabilities[key] = m.Vulnerability.VulnerabilityMetadata + for _, r := range m.RelatedVulnerabilities { + key := fmt.Sprintf("%s:%s", r.Namespace, r.ID) + dedupeRelatedVulnerabilities[key] = r + } + } + var primaryVulnerability VulnerabilityMetadata + for _, r := range dedupeRelatedVulnerabilities { + if r.ID == b.PrimaryMatch.Vulnerability.ID && r.Namespace == "nvd:cpe" { + primaryVulnerability = r + } + } + if primaryVulnerability.ID == "" { + primaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata } - // delete the primary vulnerability from the related vulnerabilities - delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", b.PrimaryMatch.Vulnerability.Namespace, b.PrimaryMatch.Vulnerability.ID)) + // delete the primary vulnerability from the related vulnerabilities + delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", primaryVulnerability.Namespace, primaryVulnerability.ID)) for k := range dedupeRelatedVulnerabilities { sortDedupedRelatedVulnerabilities = append(sortDedupedRelatedVulnerabilities, k) } @@ -291,13 +307,8 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { relatedVulnerabilities = append(relatedVulnerabilities, dedupeRelatedVulnerabilities[k]) } - // var explainedPackages []*ExplainedPackage - // for k, v := range pURLsToMatchDetails { - - // } - return ExplainViewModel{ - PrimaryVulnerability: b.PrimaryMatch.Vulnerability, + PrimaryVulnerability: primaryVulnerability, RelatedVulnerabilities: relatedVulnerabilities, MatchedPackages: explainedPackages, URLs: dedupeURLs(primaryURL, URLs), diff --git a/grype/presenter/models/explain_cve_new.tmpl b/grype/presenter/models/explain_cve_new.tmpl index c3a677c0eb6..92df2dedf8d 100644 --- a/grype/presenter/models/explain_cve_new.tmpl +++ b/grype/presenter/models/explain_cve_new.tmpl @@ -9,5 +9,5 @@ Matched packages: {{ range .MatchedPackages }} PURL: {{ .PURL }}{{ if .CPEExplanation }} {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} {{ .IndirectExplanation }}{{ end }} - Evidenced by: {{ range .Locations }}: + Evidenced by: {{ range .Locations }} - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} From 7dca892a3f6046fac840c6c18e9c71d613a66dfa Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Jun 2023 15:55:12 -0400 Subject: [PATCH 13/49] fix primary url Signed-off-by: Will Murphy --- grype/presenter/models/explain.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/grype/presenter/models/explain.go b/grype/presenter/models/explain.go index 9dd6c05754c..f56d6311a34 100644 --- a/grype/presenter/models/explain.go +++ b/grype/presenter/models/explain.go @@ -201,10 +201,18 @@ func (b *ExplainViewModelBuilder) WithRelatedMatch(m Match) *ExplainViewModelBui } func (b *ExplainViewModelBuilder) Build() ExplainViewModel { - primaryURL := b.PrimaryMatch.Vulnerability.DataSource URLs := b.PrimaryMatch.Vulnerability.URLs + URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) + for _, v := range b.PrimaryMatch.RelatedVulnerabilities { + URLs = append(URLs, v.URLs...) + URLs = append(URLs, v.DataSource) + } for _, m := range b.RelatedMatches { URLs = append(URLs, m.Vulnerability.URLs...) + for _, v := range m.RelatedVulnerabilities { + URLs = append(URLs, v.URLs...) + URLs = append(URLs, v.DataSource) + } } pURLsToMatchDetails := make(map[string]*ExplainedPackage) @@ -296,6 +304,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { if primaryVulnerability.ID == "" { primaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata } + primaryURL := primaryVulnerability.DataSource // delete the primary vulnerability from the related vulnerabilities delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", primaryVulnerability.Namespace, primaryVulnerability.ID)) From 7ce87f3726e153696936a3f06ef2d8a0eb178393 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 12 Jul 2023 10:09:36 -0400 Subject: [PATCH 14/49] Deduplicate evidence by path Signed-off-by: Will Murphy --- grype/presenter/models/explain.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/grype/presenter/models/explain.go b/grype/presenter/models/explain.go index f56d6311a34..8675ee8be51 100644 --- a/grype/presenter/models/explain.go +++ b/grype/presenter/models/explain.go @@ -273,8 +273,20 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } } var sortPURLs []string - for k := range pURLsToMatchDetails { + for k, v := range pURLsToMatchDetails { sortPURLs = append(sortPURLs, k) + dedupeLocations := make(map[string]ExplainedEvidence) + for _, l := range v.Locations { + dedupeLocations[l.Location] = l + } + var uniqueLocations []ExplainedEvidence + for _, l := range dedupeLocations { + uniqueLocations = append(uniqueLocations, l) + } + sort.Slice(uniqueLocations, func(i, j int) bool { + return uniqueLocations[i].Location < uniqueLocations[j].Location + }) + v.Locations = uniqueLocations } sort.Strings(sortPURLs) var explainedPackages []*ExplainedPackage @@ -285,7 +297,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { // TODO: this isn't right at all. // We need to be able to add related vulnerabilities var relatedVulnerabilities []VulnerabilityMetadata - var dedupeRelatedVulnerabilities = make(map[string]VulnerabilityMetadata) + dedupeRelatedVulnerabilities := make(map[string]VulnerabilityMetadata) var sortDedupedRelatedVulnerabilities []string for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { key := fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID) From df647309ff9e89394f09bcb28480dd83580ee116 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 14 Jul 2023 11:41:35 -0400 Subject: [PATCH 15/49] Rebase on new package structure Signed-off-by: Will Murphy --- cmd/{ => grype/cli/legacy}/explain.go | 2 +- cmd/grype/main.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename cmd/{ => grype/cli/legacy}/explain.go (98%) diff --git a/cmd/explain.go b/cmd/grype/cli/legacy/explain.go similarity index 98% rename from cmd/explain.go rename to cmd/grype/cli/legacy/explain.go index 981604faec0..b633f8da065 100644 --- a/cmd/explain.go +++ b/cmd/grype/cli/legacy/explain.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "encoding/json" diff --git a/cmd/grype/main.go b/cmd/grype/main.go index e4cb2d5f579..31d3348641a 100644 --- a/cmd/grype/main.go +++ b/cmd/grype/main.go @@ -5,5 +5,6 @@ import ( ) func main() { - legacy.Execute() + cli := legacy.NewCli() + cli.Execute() } From fc57e31e0db180811f371f066b55e9acddef8697 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 31 Aug 2023 14:22:32 -0400 Subject: [PATCH 16/49] rename flag to --id Signed-off-by: Will Murphy --- cmd/grype/cli/legacy/explain.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/grype/cli/legacy/explain.go b/cmd/grype/cli/legacy/explain.go index b633f8da065..70731d3da51 100644 --- a/cmd/grype/cli/legacy/explain.go +++ b/cmd/grype/cli/legacy/explain.go @@ -11,10 +11,10 @@ import ( "github.com/spf13/cobra" ) -var cveIDs []string +var vulnIDs []string var explainCmd = &cobra.Command{ - Use: "explain --cve [CVE ID]", + Use: "explain --id [VULNERABILITY ID]", Short: "Ask grype to explain a set of findings", RunE: func(cmd *cobra.Command, args []string) error { isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() @@ -31,7 +31,7 @@ var explainCmd = &cobra.Command{ return fmt.Errorf("unable to parse piped input: %+v", err) } explainer := models.NewBetterVulnerabilityExplainer(os.Stdout, &parseResult) - return explainer.ExplainByID(cveIDs) + return explainer.ExplainByID(vulnIDs) } // perform a scan, then explain requested CVEs // TODO: implement @@ -44,5 +44,5 @@ func init() { } func setExplainFlags(cmd *cobra.Command) { - cmd.Flags().StringArrayVarP(&cveIDs, "cve", "", nil, "CVE ID to explain") + cmd.Flags().StringArrayVarP(&vulnIDs, "id", "", nil, "CVE ID to explain") } From 8ae351e3330469d5af0624e2f4277c313fd1ebcf Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 31 Aug 2023 14:22:50 -0400 Subject: [PATCH 17/49] put URLs after evidence Signed-off-by: Will Murphy --- grype/presenter/models/explain_cve_new.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grype/presenter/models/explain_cve_new.tmpl b/grype/presenter/models/explain_cve_new.tmpl index 92df2dedf8d..8888d35b047 100644 --- a/grype/presenter/models/explain_cve_new.tmpl +++ b/grype/presenter/models/explain_cve_new.tmpl @@ -2,8 +2,6 @@ {{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} {{ .Severity }}{{end}}{{end}} -URLs: {{ range .URLs }} - - {{ . }}{{ end }} Matched packages: {{ range .MatchedPackages }} Package: {{ .Name }}, version: {{ .Version }} PURL: {{ .PURL }}{{ if .CPEExplanation }} @@ -11,3 +9,5 @@ Matched packages: {{ range .MatchedPackages }} {{ .IndirectExplanation }}{{ end }} Evidenced by: {{ range .Locations }} - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} +URLs: {{ range .URLs }} + - {{ . }}{{ end }} \ No newline at end of file From acbc0e11bb27e446972d36a106056d9e35c4a748 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 31 Aug 2023 14:29:45 -0400 Subject: [PATCH 18/49] warn prototype and handle errors Signed-off-by: Will Murphy --- cmd/grype/cli/legacy/explain.go | 9 +++++---- cmd/grype/main.go | 9 ++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/grype/cli/legacy/explain.go b/cmd/grype/cli/legacy/explain.go index 70731d3da51..8a8547dbae4 100644 --- a/cmd/grype/cli/legacy/explain.go +++ b/cmd/grype/cli/legacy/explain.go @@ -11,12 +11,13 @@ import ( "github.com/spf13/cobra" ) -var vulnIDs []string +var cveIDs []string var explainCmd = &cobra.Command{ Use: "explain --id [VULNERABILITY ID]", Short: "Ask grype to explain a set of findings", RunE: func(cmd *cobra.Command, args []string) error { + log.Warn("grype explain is a prototype feature and is subject to change") isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() if err != nil { log.Warnf("unable to determine if there is piped input: %+v", err) @@ -31,11 +32,11 @@ var explainCmd = &cobra.Command{ return fmt.Errorf("unable to parse piped input: %+v", err) } explainer := models.NewBetterVulnerabilityExplainer(os.Stdout, &parseResult) - return explainer.ExplainByID(vulnIDs) + return explainer.ExplainByID(cveIDs) } // perform a scan, then explain requested CVEs // TODO: implement - return fmt.Errorf("not implemented") + return fmt.Errorf("requires grype json on stdin, please run 'grype -o json ... | grype explain ...'") }, } @@ -44,5 +45,5 @@ func init() { } func setExplainFlags(cmd *cobra.Command) { - cmd.Flags().StringArrayVarP(&vulnIDs, "id", "", nil, "CVE ID to explain") + cmd.Flags().StringArrayVarP(&cveIDs, "id", "", nil, "CVE ID to explain") } diff --git a/cmd/grype/main.go b/cmd/grype/main.go index 31d3348641a..b648df4a7e8 100644 --- a/cmd/grype/main.go +++ b/cmd/grype/main.go @@ -1,10 +1,17 @@ package main import ( + "os" + "github.com/anchore/grype/cmd/grype/cli/legacy" + "github.com/anchore/grype/internal/log" ) func main() { cli := legacy.NewCli() - cli.Execute() + err := cli.Execute() + if err != nil { + log.Error(err) + os.Exit(1) + } } From 76444a6e8fc7f7a6c66dee1bb2e5dde7f13ad66e Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 31 Aug 2023 14:50:07 -0400 Subject: [PATCH 19/49] move new code to own presenter Signed-off-by: Will Murphy --- cmd/grype/cli/legacy/explain.go | 3 +- .../presenter/{models => explain}/explain.go | 35 ++++++++++--------- .../{models => explain}/explain_cve.tmpl | 0 .../{models => explain}/explain_cve_new.tmpl | 0 .../{models => explain}/explanation.go | 25 ++++++------- 5 files changed, 33 insertions(+), 30 deletions(-) rename grype/presenter/{models => explain}/explain.go (89%) rename grype/presenter/{models => explain}/explain_cve.tmpl (100%) rename grype/presenter/{models => explain}/explain_cve_new.tmpl (100%) rename grype/presenter/{models => explain}/explanation.go (92%) diff --git a/cmd/grype/cli/legacy/explain.go b/cmd/grype/cli/legacy/explain.go index 8a8547dbae4..f46a287c454 100644 --- a/cmd/grype/cli/legacy/explain.go +++ b/cmd/grype/cli/legacy/explain.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/anchore/grype/grype/presenter/explain" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" @@ -31,7 +32,7 @@ var explainCmd = &cobra.Command{ if err != nil { return fmt.Errorf("unable to parse piped input: %+v", err) } - explainer := models.NewBetterVulnerabilityExplainer(os.Stdout, &parseResult) + explainer := explain.NewBetterVulnerabilityExplainer(os.Stdout, &parseResult) return explainer.ExplainByID(cveIDs) } // perform a scan, then explain requested CVEs diff --git a/grype/presenter/models/explain.go b/grype/presenter/explain/explain.go similarity index 89% rename from grype/presenter/models/explain.go rename to grype/presenter/explain/explain.go index 8675ee8be51..18b3a5971ee 100644 --- a/grype/presenter/models/explain.go +++ b/grype/presenter/explain/explain.go @@ -1,4 +1,4 @@ -package models +package explain import ( _ "embed" @@ -9,6 +9,7 @@ import ( "text/template" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/presenter/models" ) //go:embed explain_cve_new.tmpl @@ -23,16 +24,16 @@ var explainTemplate string // render it either as JSON or as the template. type ExplainViewModel struct { - PrimaryVulnerability VulnerabilityMetadata - RelatedVulnerabilities []VulnerabilityMetadata + PrimaryVulnerability models.VulnerabilityMetadata + RelatedVulnerabilities []models.VulnerabilityMetadata MatchedPackages []*ExplainedPackage URLs []string } type ExplainViewModelBuilder struct { - PrimaryVulnerability Vulnerability // this is the vulnerability we're trying to explain - PrimaryMatch Match - RelatedMatches []Match + PrimaryVulnerability models.Vulnerability // this is the vulnerability we're trying to explain + PrimaryMatch models.Match + RelatedMatches []models.Match } type ExplainedFindings map[string]ExplainViewModel @@ -57,10 +58,10 @@ type ExplainedEvidence struct { type betterVulnerabilityExplainer struct { w io.Writer - doc *Document + doc *models.Document } -func NewBetterVulnerabilityExplainer(w io.Writer, doc *Document) VulnerabilityExplainer { +func NewBetterVulnerabilityExplainer(w io.Writer, doc *models.Document) VulnerabilityExplainer { return &betterVulnerabilityExplainer{ w: w, doc: doc, @@ -104,7 +105,7 @@ func (e *betterVulnerabilityExplainer) ExplainAll() error { return t.Execute(e.w, findings) } -func ExplainDoc(doc *Document, requestedIDs []string) (ExplainedFindings, error) { +func ExplainDoc(doc *models.Document, requestedIDs []string) (ExplainedFindings, error) { result := make(ExplainedFindings) builders := make(map[string]*ExplainViewModelBuilder) for _, m := range doc.Matches { @@ -139,7 +140,7 @@ func NewExplainedVulnerabilityBuilder() *ExplainViewModelBuilder { // WithMatch adds a match to the builder // accepting enough information to determine whether the match is a primary match or a related match -func (b *ExplainViewModelBuilder) WithMatch(m Match, userRequestedIDs []string, graphIsByCVE bool) { +func (b *ExplainViewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string, graphIsByCVE bool) { // TODO: check if it's a primary vulnerability // (the below checks if it's a primary _match_, which is wrong) if b.isPrimaryAdd(m, userRequestedIDs, graphIsByCVE) { @@ -154,7 +155,7 @@ func (b *ExplainViewModelBuilder) WithMatch(m Match, userRequestedIDs []string, } } -func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate Match, userRequestedIDs []string, graphIsByCVE bool) bool { +func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string, graphIsByCVE bool) bool { // TODO: "primary" is a property of a vulnerability, not a match // if there's not currently any match, make this one primary since we don't know any better if b.PrimaryMatch.Vulnerability.ID == "" { @@ -190,12 +191,12 @@ func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate Match, userRequestedIDs return false } -func (b *ExplainViewModelBuilder) WithPrimaryMatch(m Match) *ExplainViewModelBuilder { +func (b *ExplainViewModelBuilder) WithPrimaryMatch(m models.Match) *ExplainViewModelBuilder { b.PrimaryMatch = m return b } -func (b *ExplainViewModelBuilder) WithRelatedMatch(m Match) *ExplainViewModelBuilder { +func (b *ExplainViewModelBuilder) WithRelatedMatch(m models.Match) *ExplainViewModelBuilder { b.RelatedMatches = append(b.RelatedMatches, m) return b } @@ -296,8 +297,8 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { // TODO: this isn't right at all. // We need to be able to add related vulnerabilities - var relatedVulnerabilities []VulnerabilityMetadata - dedupeRelatedVulnerabilities := make(map[string]VulnerabilityMetadata) + var relatedVulnerabilities []models.VulnerabilityMetadata + dedupeRelatedVulnerabilities := make(map[string]models.VulnerabilityMetadata) var sortDedupedRelatedVulnerabilities []string for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { key := fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID) @@ -307,7 +308,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { dedupeRelatedVulnerabilities[key] = r } } - var primaryVulnerability VulnerabilityMetadata + var primaryVulnerability models.VulnerabilityMetadata for _, r := range dedupeRelatedVulnerabilities { if r.ID == b.PrimaryMatch.Vulnerability.ID && r.Namespace == "nvd:cpe" { primaryVulnerability = r @@ -336,7 +337,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } } -func explainMatchDetail(m Match, index int) string { +func explainMatchDetail(m models.Match, index int) string { if len(m.MatchDetails) <= index { return "" } diff --git a/grype/presenter/models/explain_cve.tmpl b/grype/presenter/explain/explain_cve.tmpl similarity index 100% rename from grype/presenter/models/explain_cve.tmpl rename to grype/presenter/explain/explain_cve.tmpl diff --git a/grype/presenter/models/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl similarity index 100% rename from grype/presenter/models/explain_cve_new.tmpl rename to grype/presenter/explain/explain_cve_new.tmpl diff --git a/grype/presenter/models/explanation.go b/grype/presenter/explain/explanation.go similarity index 92% rename from grype/presenter/models/explanation.go rename to grype/presenter/explain/explanation.go index f9381c16189..ae3b1611e81 100644 --- a/grype/presenter/models/explanation.go +++ b/grype/presenter/explain/explanation.go @@ -1,4 +1,4 @@ -package models +package explain import ( _ "embed" @@ -8,6 +8,7 @@ import ( "text/template" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" ) @@ -36,8 +37,8 @@ type ExplainedVulnerability struct { // the same PURL and then group them by PURL. type MatchedPackage struct { - Package Package - Details []MatchDetails + Package models.Package + Details []models.MatchDetails Explanation string } @@ -61,12 +62,12 @@ type VulnerabilityExplainer interface { } type vulnerabilityExplainer struct { - doc Document + doc models.Document w io.Writer templ *template.Template } -func NewVulnerabilityExplainer(doc Document, w io.Writer) VulnerabilityExplainer { +func NewVulnerabilityExplainer(doc models.Document, w io.Writer) VulnerabilityExplainer { return &vulnerabilityExplainer{ doc: doc, w: w, @@ -124,9 +125,9 @@ func (e *vulnerabilityExplainer) ExplainAll() error { } // NewExplainedVulnerability creates a new explained vulnerability. -func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedVulnerability { - var directMatches []Match - var relatedMatches []Match +func NewExplainedVulnerability(vulnerabilityID string, doc models.Document) *ExplainedVulnerability { + var directMatches []models.Match + var relatedMatches []models.Match for _, m := range doc.Matches { // TODO: make the one that matches on the ID always be first? if m.Vulnerability.ID == vulnerabilityID { @@ -198,7 +199,7 @@ func NewExplainedVulnerability(vulnerabilityID string, doc Document) *ExplainedV } } -func startExplainedPackageMatch(m Match) ExplainedPackageMatch { +func startExplainedPackageMatch(m models.Match) ExplainedPackageMatch { explanation := "" if len(m.MatchDetails) > 0 { switch m.MatchDetails[0].Type { @@ -238,7 +239,7 @@ func dedupeURLs(showFirst string, rest []string) []string { return result } -func ToMatchedPackage(m Match) MatchedPackage { +func ToMatchedPackage(m models.Match) MatchedPackage { explanation := "" if len(m.MatchDetails) > 0 { switch m.MatchDetails[0].Type { @@ -256,7 +257,7 @@ func ToMatchedPackage(m Match) MatchedPackage { } } -func sourcePackageNameAndVersion(md MatchDetails) (string, string) { +func sourcePackageNameAndVersion(md models.MatchDetails) (string, string) { var name string var version string if mapResult, ok := md.SearchedBy.(map[string]interface{}); ok { @@ -274,7 +275,7 @@ func sourcePackageNameAndVersion(md MatchDetails) (string, string) { return name, version } -func formatCPEExplanation(m Match) string { +func formatCPEExplanation(m models.Match) string { searchedBy := m.MatchDetails[0].SearchedBy if mapResult, ok := searchedBy.(map[string]interface{}); ok { if cpes, ok := mapResult["cpes"]; ok { From 4329894fe94e20df479370236ed995355e86c65c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 31 Aug 2023 15:24:13 -0400 Subject: [PATCH 20/49] nits in template - newline and conditional PURL line Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 1 + grype/presenter/explain/explain_cve_new.tmpl | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 18b3a5971ee..2eff2185c15 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -289,6 +289,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { }) v.Locations = uniqueLocations } + // TODO: put the primary match first sort.Strings(sortPURLs) var explainedPackages []*ExplainedPackage for _, k := range sortPURLs { diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl index 8888d35b047..c6d8cb2e602 100644 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ b/grype/presenter/explain/explain_cve_new.tmpl @@ -1,13 +1,13 @@ {{ .PrimaryVulnerability.ID }} from {{ .PrimaryVulnerability.Namespace }} ({{ .PrimaryVulnerability.Severity }}) {{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} Related vulnerabilities:{{ range .RelatedVulnerabilities }} - - {{.Namespace}} {{ .ID }} {{ .Severity }}{{end}}{{end}} + - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} Matched packages: {{ range .MatchedPackages }} - Package: {{ .Name }}, version: {{ .Version }} - PURL: {{ .PURL }}{{ if .CPEExplanation }} + Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} + PURL: {{ .PURL }}{{ end }}{{ if .CPEExplanation }} {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} {{ .IndirectExplanation }}{{ end }} Evidenced by: {{ range .Locations }} - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} URLs: {{ range .URLs }} - - {{ . }}{{ end }} \ No newline at end of file + - {{ . }}{{ end }} From 9c9a6aa4eaf06665aea7a899f97f0b9e642bea2b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 07:49:36 -0400 Subject: [PATCH 21/49] move still useful code out of old file and delete Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 61 +++++ grype/presenter/explain/explanation.go | 300 ------------------------- 2 files changed, 61 insertions(+), 300 deletions(-) delete mode 100644 grype/presenter/explain/explanation.go diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 2eff2185c15..c359a1d2016 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -15,6 +15,12 @@ import ( //go:embed explain_cve_new.tmpl var explainTemplate string +type VulnerabilityExplainer interface { + ExplainByID(IDs []string) error + ExplainBySeverity(severity string) error + ExplainAll() error +} + // TODO: basically re-write a lot of this // Build a structure where an ExplainedVulnerability // is basically the nvd:cpe record, plus a list of @@ -353,3 +359,58 @@ func explainMatchDetail(m models.Match, index int) string { } return explanation } + +func dedupeURLs(showFirst string, rest []string) []string { + var result []string + result = append(result, showFirst) + deduplicate := make(map[string]bool) + for _, u := range rest { + if _, ok := deduplicate[u]; !ok && u != showFirst { + result = append(result, u) + deduplicate[u] = true + } + } + return result +} + +func formatCPEExplanation(m models.Match) string { + searchedBy := m.MatchDetails[0].SearchedBy + if mapResult, ok := searchedBy.(map[string]interface{}); ok { + if cpes, ok := mapResult["cpes"]; ok { + if cpeSlice, ok := cpes.([]interface{}); ok { + if len(cpeSlice) > 0 { + return fmt.Sprintf("CPE match on `%s`", cpeSlice[0]) + } + } + } + } + return "" +} + +func sourcePackageNameAndVersion(md models.MatchDetails) (string, string) { + var name string + var version string + if mapResult, ok := md.SearchedBy.(map[string]interface{}); ok { + if sourcePackage, ok := mapResult["package"]; ok { + if sourceMap, ok := sourcePackage.(map[string]interface{}); ok { + if maybeName, ok := sourceMap["name"]; ok { + name, _ = maybeName.(string) + } + if maybeVersion, ok := sourceMap["version"]; ok { + version, _ = maybeVersion.(string) + } + } + } + } + return name, version +} + +func nameForUpstream(typ string) string { + switch typ { + case "deb": + return "origin" + case "rpm": + return "source RPM" + } + return "upstream" +} diff --git a/grype/presenter/explain/explanation.go b/grype/presenter/explain/explanation.go deleted file mode 100644 index ae3b1611e81..00000000000 --- a/grype/presenter/explain/explanation.go +++ /dev/null @@ -1,300 +0,0 @@ -package explain - -import ( - _ "embed" - "fmt" - "io" - "strings" - "text/template" - - "github.com/anchore/grype/grype/match" - "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/grype/grype/vulnerability" -) - -//go:embed explain_cve.tmpl -var templ string - -// ExplainedVulnerability explains a vulnerability match. -// It includes details about all matched artifacts and how they were -// matched. -type ExplainedVulnerability struct { - VulnerabilityID string - Severity string - Namespace string - Description string - VersionConstraint string - // MatchedPackages map[string][]MatchedPackage // map of PURL to MatchedPackage - MatchedPackages []*ExplainedPackageMatch - URLs []string -} - -// TODO: this is basically a slice of matches. Is it needed? -// Maybe I need a different way to orient the slice of matches? -// having nice map/reduce functions is the thing that -// trips me up most writing Go code. -// Actually what we will do is build a slice of artifacts with -// the same PURL and then group them by PURL. - -type MatchedPackage struct { - Package models.Package - Details []models.MatchDetails - Explanation string -} - -type ExplainedPackageMatch struct { - PURL string - Name string - Version string - Explanation string - Locations []LocatedArtifact -} -type LocatedArtifact struct { - Location string - ArtifactId string - ViaVulnID string -} - -type VulnerabilityExplainer interface { - ExplainByID(IDs []string) error - ExplainBySeverity(severity string) error - ExplainAll() error -} - -type vulnerabilityExplainer struct { - doc models.Document - w io.Writer - templ *template.Template -} - -func NewVulnerabilityExplainer(doc models.Document, w io.Writer) VulnerabilityExplainer { - return &vulnerabilityExplainer{ - doc: doc, - w: w, - templ: template.Must(template.New("explanation").Parse(templ)), - } -} - -func (e *vulnerabilityExplainer) ExplainByID(IDs []string) error { - // TODO: consider grouping all vulnerabilities by CVE ID - // and then doing this stuff - toExplain := make(map[string]ExplainedVulnerability) - for _, id := range IDs { - explained := NewExplainedVulnerability(id, e.doc) - if explained != nil { - toExplain[id] = *explained - } - } - for _, id := range IDs { - explained, ok := toExplain[id] - if !ok { - continue - } - if err := e.templ.Execute(e.w, explained); err != nil { - return fmt.Errorf("unable to execute template: %w", err) - } - } - return nil -} - -func (e *vulnerabilityExplainer) ExplainBySeverity(minSeverity string) error { - uniqueSevereIDs := make(map[string]bool) - severity := vulnerability.ParseSeverity(minSeverity) - for _, m := range e.doc.Matches { - if vulnerability.ParseSeverity(m.Vulnerability.Severity) >= severity { - uniqueSevereIDs[m.Vulnerability.ID] = true - } - } - var IDs []string - for id := range uniqueSevereIDs { - IDs = append(IDs, id) - } - return e.ExplainByID(IDs) -} - -func (e *vulnerabilityExplainer) ExplainAll() error { - uniqueIDs := make(map[string]bool) - for _, m := range e.doc.Matches { - uniqueIDs[m.Vulnerability.ID] = true - } - var IDs []string - for id := range uniqueIDs { - IDs = append(IDs, id) - } - return e.ExplainByID(IDs) -} - -// NewExplainedVulnerability creates a new explained vulnerability. -func NewExplainedVulnerability(vulnerabilityID string, doc models.Document) *ExplainedVulnerability { - var directMatches []models.Match - var relatedMatches []models.Match - for _, m := range doc.Matches { - // TODO: make the one that matches on the ID always be first? - if m.Vulnerability.ID == vulnerabilityID { - directMatches = append(directMatches, m) - } else { - for _, r := range m.RelatedVulnerabilities { - if r.ID == vulnerabilityID { - relatedMatches = append(relatedMatches, m) - } - } - } - } - if len(directMatches) == 0 { - return nil - } - packages := make(map[string]*ExplainedPackageMatch) - directAndRelatedMatches := append(directMatches, relatedMatches...) - for _, m := range directAndRelatedMatches { - if m.Artifact.PURL == "" { - continue - } - if explained, ok := packages[m.Artifact.PURL]; ok { - for _, location := range m.Artifact.Locations { - via := "" - if m.Vulnerability.ID != vulnerabilityID { - via = m.Vulnerability.ID - } - explained.Locations = append(explained.Locations, LocatedArtifact{ - Location: location.RealPath, - ArtifactId: m.Artifact.ID, - ViaVulnID: via, - }) - } - } else { - explained := startExplainedPackageMatch(m) - packages[m.Artifact.PURL] = &explained - } - } - var URLs []string - for _, m := range directAndRelatedMatches { - URLs = append(URLs, m.Vulnerability.VulnerabilityMetadata.URLs...) - } - var versionConstraint string - for _, m := range directMatches { - // TODO: which version constraint should we use? - // in other words, which match should win? - if len(m.Vulnerability.Fix.Versions) == 0 { - versionConstraint = "all versions" - } - if len(m.Vulnerability.Fix.Versions) == 1 { - versionConstraint = fmt.Sprintf("< %s", m.Vulnerability.Fix.Versions[0]) - } - } - var matchedPackages []*ExplainedPackageMatch - for _, explained := range packages { - matchedPackages = append(matchedPackages, explained) - } - - return &ExplainedVulnerability{ - VulnerabilityID: vulnerabilityID, - // TODO: which severity should we use? - // in other words, which match should win? - Severity: directMatches[0].Vulnerability.Severity, - Namespace: directMatches[0].Vulnerability.Namespace, - Description: strings.TrimSpace(directMatches[0].Vulnerability.Description), - VersionConstraint: versionConstraint, - MatchedPackages: matchedPackages, - URLs: dedupeURLs(directMatches[0].Vulnerability.DataSource, URLs), - } -} - -func startExplainedPackageMatch(m models.Match) ExplainedPackageMatch { - explanation := "" - if len(m.MatchDetails) > 0 { - switch m.MatchDetails[0].Type { - case string(match.CPEMatch): - explanation = formatCPEExplanation(m) - case string(match.ExactIndirectMatch): - sourceName, sourceVersion := sourcePackageNameAndVersion(m.MatchDetails[0]) - explanation = fmt.Sprintf("Note: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) - } - } - var locatedArtifacts []LocatedArtifact - for _, location := range m.Artifact.Locations { - locatedArtifacts = append(locatedArtifacts, LocatedArtifact{ - Location: location.RealPath, - ArtifactId: m.Artifact.ID, - }) - } - return ExplainedPackageMatch{ - PURL: m.Artifact.PURL, - Name: m.Artifact.Name, - Version: m.Artifact.Version, - Explanation: explanation, - Locations: locatedArtifacts, - } -} - -func dedupeURLs(showFirst string, rest []string) []string { - var result []string - result = append(result, showFirst) - deduplicate := make(map[string]bool) - for _, u := range rest { - if _, ok := deduplicate[u]; !ok && u != showFirst { - result = append(result, u) - deduplicate[u] = true - } - } - return result -} - -func ToMatchedPackage(m models.Match) MatchedPackage { - explanation := "" - if len(m.MatchDetails) > 0 { - switch m.MatchDetails[0].Type { - case string(match.CPEMatch): - explanation = formatCPEExplanation(m) - case string(match.ExactIndirectMatch): - sourceName, sourceVersion := sourcePackageNameAndVersion(m.MatchDetails[0]) - explanation = fmt.Sprintf("Indirect match on source package: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) - } - } - return MatchedPackage{ - Package: m.Artifact, - Details: m.MatchDetails, - Explanation: explanation, - } -} - -func sourcePackageNameAndVersion(md models.MatchDetails) (string, string) { - var name string - var version string - if mapResult, ok := md.SearchedBy.(map[string]interface{}); ok { - if sourcePackage, ok := mapResult["package"]; ok { - if sourceMap, ok := sourcePackage.(map[string]interface{}); ok { - if maybeName, ok := sourceMap["name"]; ok { - name, _ = maybeName.(string) - } - if maybeVersion, ok := sourceMap["version"]; ok { - version, _ = maybeVersion.(string) - } - } - } - } - return name, version -} - -func formatCPEExplanation(m models.Match) string { - searchedBy := m.MatchDetails[0].SearchedBy - if mapResult, ok := searchedBy.(map[string]interface{}); ok { - if cpes, ok := mapResult["cpes"]; ok { - if cpeSlice, ok := cpes.([]interface{}); ok { - if len(cpeSlice) > 0 { - return fmt.Sprintf("CPE match on `%s`", cpeSlice[0]) - } - } - } - } - return "" -} - -func nameForUpstream(typ string) string { - switch typ { - case "deb": - return "origin" - case "rpm": - return "source RPM" - } - return "upstream" -} From f559463dfba5cbe48fc88e1ffce0b3d50dd1065d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 08:33:43 -0400 Subject: [PATCH 22/49] add snapshot test and fixtures Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 4 + .../explain/explain_snapshot_test.go | 36 + .../explain/test-fixtures/keycloak-test.json | 839 ++++++++++++++++++ 3 files changed, 879 insertions(+) create mode 100755 grype/presenter/explain/__snapshots__/explain_snapshot_test.snap create mode 100644 grype/presenter/explain/explain_snapshot_test.go create mode 100644 grype/presenter/explain/test-fixtures/keycloak-test.json diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap new file mode 100755 index 00000000000..70da37a076d --- /dev/null +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -0,0 +1,4 @@ + +[TestExplainSnapshot - 1] +[]uint8(nil) +--- diff --git a/grype/presenter/explain/explain_snapshot_test.go b/grype/presenter/explain/explain_snapshot_test.go new file mode 100644 index 00000000000..59243e2da4f --- /dev/null +++ b/grype/presenter/explain/explain_snapshot_test.go @@ -0,0 +1,36 @@ +package explain_test + +import ( + "bufio" + "bytes" + "encoding/json" + "os" + "testing" + + "github.com/anchore/grype/grype/presenter/explain" + "github.com/anchore/grype/grype/presenter/models" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" +) + +func TestExplainSnapshot(t *testing.T) { + // load sample json + // TODO: make and commit minimal sample JSON + r, err := os.Open("./test-fixtures/keycloak-test.json") + require.NoError(t, err) + + // parse to models.Document + doc := models.Document{} + decoder := json.NewDecoder(r) + err = decoder.Decode(&doc) + require.NoError(t, err) + // create explain.VulnerabilityExplainer + var b bytes.Buffer + w := bufio.NewWriter(&b) + explainer := explain.NewVulnerabilityExplainer(w, &doc) + // call ExplainByID + err = explainer.ExplainByID([]string{"CVE-2020-12413"}) + require.NoError(t, err) + // assert output + snaps.MatchSnapshot(t, b.Bytes()) +} diff --git a/grype/presenter/explain/test-fixtures/keycloak-test.json b/grype/presenter/explain/test-fixtures/keycloak-test.json new file mode 100644 index 00000000000..c14db8cb62b --- /dev/null +++ b/grype/presenter/explain/test-fixtures/keycloak-test.json @@ -0,0 +1,839 @@ +{ + "matches": [{ + "vulnerability": { + "id": "CVE-2020-12413", + "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", + "namespace": "redhat:distro:redhat:9", + "severity": "Low", + "urls": [ + "https://access.redhat.com/security/cve/CVE-2020-12413" + ], + "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": { + "base_severity": "Medium", + "status": "draft" + } + } + ], + "fix": { + "versions": [], + "state": "wont-fix" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2020-12413", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", + "https://raccoon-attack.com/" + ], + "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-indirect-match", + "matcher": "rpm-matcher", + "searchedBy": { + "distro": { + "type": "redhat", + "version": "9.1" + }, + "namespace": "redhat:distro:redhat:9", + "package": { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + }, + "found": { + "versionConstraint": "none (rpm)", + "vulnerabilityID": "CVE-2020-12413" + } + } + ], + "artifact": { + "id": "ff2aefb138ebd4bf", + "name": "nspr", + "version": "4.34.0-17.el9_1", + "type": "rpm", + "locations": [ + { + "path": "/var/lib/rpm/rpmdb.sqlite", + "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" + } + ], + "language": "", + "licenses": [ + "MPLv2.0" + ], + "cpes": [ + "cpe:2.3:a:redhat:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nspr:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*" + ], + "purl": "pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", + "upstreams": [ + { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + ], + "metadataType": "RpmMetadata", + "metadata": { + "epoch": null, + "modularityLabel": "" + } + } + }, + { + "vulnerability": { + "id": "CVE-2020-12413", + "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", + "namespace": "redhat:distro:redhat:9", + "severity": "Low", + "urls": [ + "https://access.redhat.com/security/cve/CVE-2020-12413" + ], + "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": { + "base_severity": "Medium", + "status": "draft" + } + } + ], + "fix": { + "versions": [], + "state": "wont-fix" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2020-12413", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", + "https://raccoon-attack.com/" + ], + "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-direct-match", + "matcher": "rpm-matcher", + "searchedBy": { + "distro": { + "type": "redhat", + "version": "9.1" + }, + "namespace": "redhat:distro:redhat:9", + "package": { + "name": "nss", + "version": "0:3.79.0-17.el9_1" + } + }, + "found": { + "versionConstraint": "none (rpm)", + "vulnerabilityID": "CVE-2020-12413" + } + } + ], + "artifact": { + "id": "840f8a931c86688f", + "name": "nss", + "version": "3.79.0-17.el9_1", + "type": "rpm", + "locations": [ + { + "path": "/var/lib/rpm/rpmdb.sqlite", + "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" + } + ], + "language": "", + "licenses": [ + "MPLv2.0" + ], + "cpes": [ + "cpe:2.3:a:redhat:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*" + ], + "purl": "pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", + "upstreams": [], + "metadataType": "RpmMetadata", + "metadata": { + "epoch": null, + "modularityLabel": "" + } + } + }, + { + "vulnerability": { + "id": "CVE-2020-12413", + "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", + "namespace": "redhat:distro:redhat:9", + "severity": "Low", + "urls": [ + "https://access.redhat.com/security/cve/CVE-2020-12413" + ], + "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": { + "base_severity": "Medium", + "status": "draft" + } + } + ], + "fix": { + "versions": [], + "state": "wont-fix" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2020-12413", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", + "https://raccoon-attack.com/" + ], + "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-indirect-match", + "matcher": "rpm-matcher", + "searchedBy": { + "distro": { + "type": "redhat", + "version": "9.1" + }, + "namespace": "redhat:distro:redhat:9", + "package": { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + }, + "found": { + "versionConstraint": "none (rpm)", + "vulnerabilityID": "CVE-2020-12413" + } + } + ], + "artifact": { + "id": "7d1c659d9eb00024", + "name": "nss-softokn", + "version": "3.79.0-17.el9_1", + "type": "rpm", + "locations": [ + { + "path": "/var/lib/rpm/rpmdb.sqlite", + "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" + } + ], + "language": "", + "licenses": [ + "MPLv2.0" + ], + "cpes": [ + "cpe:2.3:a:nss-softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss-softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*" + ], + "purl": "pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", + "upstreams": [ + { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + ], + "metadataType": "RpmMetadata", + "metadata": { + "epoch": null, + "modularityLabel": "" + } + } + }, + { + "vulnerability": { + "id": "CVE-2020-12413", + "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", + "namespace": "redhat:distro:redhat:9", + "severity": "Low", + "urls": [ + "https://access.redhat.com/security/cve/CVE-2020-12413" + ], + "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": { + "base_severity": "Medium", + "status": "draft" + } + } + ], + "fix": { + "versions": [], + "state": "wont-fix" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2020-12413", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", + "https://raccoon-attack.com/" + ], + "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-indirect-match", + "matcher": "rpm-matcher", + "searchedBy": { + "distro": { + "type": "redhat", + "version": "9.1" + }, + "namespace": "redhat:distro:redhat:9", + "package": { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + }, + "found": { + "versionConstraint": "none (rpm)", + "vulnerabilityID": "CVE-2020-12413" + } + } + ], + "artifact": { + "id": "cb1f96627e29924e", + "name": "nss-softokn-freebl", + "version": "3.79.0-17.el9_1", + "type": "rpm", + "locations": [ + { + "path": "/var/lib/rpm/rpmdb.sqlite", + "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" + } + ], + "language": "", + "licenses": [ + "MPLv2.0" + ], + "cpes": [ + "cpe:2.3:a:nss-softokn-freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss-softokn-freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_softokn_freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_softokn_freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss-softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss-softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*" + ], + "purl": "pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", + "upstreams": [ + { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + ], + "metadataType": "RpmMetadata", + "metadata": { + "epoch": null, + "modularityLabel": "" + } + } + }, + { + "vulnerability": { + "id": "CVE-2020-12413", + "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", + "namespace": "redhat:distro:redhat:9", + "severity": "Low", + "urls": [ + "https://access.redhat.com/security/cve/CVE-2020-12413" + ], + "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": { + "base_severity": "Medium", + "status": "draft" + } + } + ], + "fix": { + "versions": [], + "state": "wont-fix" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2020-12413", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", + "https://raccoon-attack.com/" + ], + "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-indirect-match", + "matcher": "rpm-matcher", + "searchedBy": { + "distro": { + "type": "redhat", + "version": "9.1" + }, + "namespace": "redhat:distro:redhat:9", + "package": { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + }, + "found": { + "versionConstraint": "none (rpm)", + "vulnerabilityID": "CVE-2020-12413" + } + } + ], + "artifact": { + "id": "d096d490e4fccf36", + "name": "nss-sysinit", + "version": "3.79.0-17.el9_1", + "type": "rpm", + "locations": [ + { + "path": "/var/lib/rpm/rpmdb.sqlite", + "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" + } + ], + "language": "", + "licenses": [ + "MPLv2.0" + ], + "cpes": [ + "cpe:2.3:a:nss-sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss-sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*" + ], + "purl": "pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", + "upstreams": [ + { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + ], + "metadataType": "RpmMetadata", + "metadata": { + "epoch": null, + "modularityLabel": "" + } + } + }, + { + "vulnerability": { + "id": "CVE-2020-12413", + "dataSource": "https://access.redhat.com/security/cve/CVE-2020-12413", + "namespace": "redhat:distro:redhat:9", + "severity": "Low", + "urls": [ + "https://access.redhat.com/security/cve/CVE-2020-12413" + ], + "description": "A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": { + "base_severity": "Medium", + "status": "draft" + } + } + ], + "fix": { + "versions": [], + "state": "wont-fix" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2020-12413", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2020-12413", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413", + "https://raccoon-attack.com/" + ], + "description": "The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "metrics": { + "baseScore": 5.9, + "exploitabilityScore": 2.2, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-indirect-match", + "matcher": "rpm-matcher", + "searchedBy": { + "distro": { + "type": "redhat", + "version": "9.1" + }, + "namespace": "redhat:distro:redhat:9", + "package": { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + }, + "found": { + "versionConstraint": "none (rpm)", + "vulnerabilityID": "CVE-2020-12413" + } + } + ], + "artifact": { + "id": "641950c22b3f5035", + "name": "nss-util", + "version": "3.79.0-17.el9_1", + "type": "rpm", + "locations": [ + { + "path": "/var/lib/rpm/rpmdb.sqlite", + "layerID": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b" + } + ], + "language": "", + "licenses": [ + "MPLv2.0" + ], + "cpes": [ + "cpe:2.3:a:nss-util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss-util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss_util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:redhat:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*", + "cpe:2.3:a:nss:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*" + ], + "purl": "pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1", + "upstreams": [ + { + "name": "nss", + "version": "3.79.0-17.el9_1" + } + ], + "metadataType": "RpmMetadata", + "metadata": { + "epoch": null, + "modularityLabel": "" + } + } + } + ], + "source": { + "type": "image", + "target": { + "userInput": "docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50", + "imageID": "sha256:8cf8fd2be2ded92962d52adff75ad06a4c30f69c66facbdf223364c6c9e33b8c", + "manifestDigest": "sha256:d1630d3eb8285a978301bcefc5b223e564ae300750af2fc9ea3f413c5376a47e", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [], + "imageSize": 433836339, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:4e37aeaccb4c8016e381d6e2b2a0f22ea59985a7b9b8eca674726e8c60f2f51d", + "size": 24302817 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b", + "size": 222622091 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:8329e422b4fd63ffd06518346e5f1f7b33e8190a79e5c321f9c50aba8651d30c", + "size": 186910556 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:987fd030ab2cd62944cb487df846c312b64dbb2a6a3131a81253c15a9da2a26c", + "size": 875 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NTY5LCJkaWdlc3QiOiJzaGEyNTY6OGNmOGZkMmJlMmRlZDkyOTYyZDUyYWRmZjc1YWQwNmE0YzMwZjY5YzY2ZmFjYmRmMjIzMzY0YzZjOWUzM2I4YyJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyNjEyNzg3MiwiZGlnZXN0Ijoic2hhMjU2OjRlMzdhZWFjY2I0YzgwMTZlMzgxZDZlMmIyYTBmMjJlYTU5OTg1YTdiOWI4ZWNhNjc0NzI2ZThjNjBmMmY1MWQifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMjUxMTI1NzYsImRpZ2VzdCI6InNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MTg3MzE1MjAwLCJkaWdlc3QiOiJzaGEyNTY6ODMyOWU0MjJiNGZkNjNmZmQwNjUxODM0NmU1ZjFmN2IzM2U4MTkwYTc5ZTVjMzIxZjljNTBhYmE4NjUxZDMwYyJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQwOTYsImRpZ2VzdCI6InNoYTI1Njo5ODdmZDAzMGFiMmNkNjI5NDRjYjQ4N2RmODQ2YzMxMmI2NGRiYjJhNmEzMTMxYTgxMjUzYzE1YTlkYTJhMjZjIn1dfQ==", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJVc2VyIjoiMTAwMCIsIkV4cG9zZWRQb3J0cyI6eyI4MDgwL3RjcCI6e30sIjg0NDMvdGNwIjp7fX0sIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIkxBTkc9ZW5fVVMuVVRGLTgiXSwiRW50cnlwb2ludCI6WyIvb3B0L2tleWNsb2FrL2Jpbi9rYy5zaCJdLCJMYWJlbHMiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiYnVpbGQtZGF0ZSI6IjIwMjMtMDItMjJUMTM6NTQ6MjUiLCJjb20ucmVkaGF0LmNvbXBvbmVudCI6InViaTktbWljcm8tY29udGFpbmVyIiwiY29tLnJlZGhhdC5saWNlbnNlX3Rlcm1zIjoiaHR0cHM6Ly93d3cucmVkaGF0LmNvbS9lbi9hYm91dC9yZWQtaGF0LWVuZC11c2VyLWxpY2Vuc2UtYWdyZWVtZW50cyNVQkkiLCJkZXNjcmlwdGlvbiI6IlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuIiwiZGlzdHJpYnV0aW9uLXNjb3BlIjoicHVibGljIiwiaW8uYnVpbGRhaC52ZXJzaW9uIjoiMS4yNy4zIiwiaW8uazhzLmRlc2NyaXB0aW9uIjoiVmVyeSBzbWFsbCBpbWFnZSB3aGljaCBkb2Vzbid0IGluc3RhbGwgdGhlIHBhY2thZ2UgbWFuYWdlci4iLCJpby5rOHMuZGlzcGxheS1uYW1lIjoiVWJpOS1taWNybyIsImlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXMiOiIiLCJtYWludGFpbmVyIjoiUmVkIEhhdCwgSW5jLiIsIm5hbWUiOiJ1Ymk5L3ViaS1taWNybyIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5jcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMjoxOC45ODVaIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uIjoiIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzIjoiQXBhY2hlLTIuMCIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5yZXZpc2lvbiI6ImIzNTJhMWY2ZThiYTkyYTA0NWI1OWNjOGRlZDE4NWUzYjFkMjYxNTUiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGUiOiJrZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVyc2lvbiI6IjIxLjAuMiIsInJlbGVhc2UiOiIxNSIsInN1bW1hcnkiOiJ1Ymk5IG1pY3JvIGltYWdlIiwidXJsIjoiaHR0cHM6Ly9hY2Nlc3MucmVkaGF0LmNvbS9jb250YWluZXJzLyMvcmVnaXN0cnkuYWNjZXNzLnJlZGhhdC5jb20vdWJpOS91YmktbWljcm8vaW1hZ2VzLzkuMS4wLTE1IiwidmNzLXJlZiI6ImM1NjNlMDkxZTBjN2JkNWE2OWIyYTQ2OTkwZGRhNGY1OTU5NWFhMzciLCJ2Y3MtdHlwZSI6ImdpdCIsInZlbmRvciI6IlJlZCBIYXQsIEluYy4iLCJ2ZXJzaW9uIjoiOS4xLjAifSwiT25CdWlsZCI6bnVsbH0sImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc2NTUxODNaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIG1haW50YWluZXI9XCJSZWQgSGF0LCBJbmMuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc3ODIxNDlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGNvbS5yZWRoYXQuY29tcG9uZW50PVwidWJpOS1taWNyby1jb250YWluZXJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjAzLjkzNzgwODc4M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgbmFtZT1cInViaTkvdWJpLW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc4NTM2MzVaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHZlcnNpb249XCI5LjEuMFwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTMwMjE1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBjb20ucmVkaGF0LmxpY2Vuc2VfdGVybXM9XCJodHRwczovL3d3dy5yZWRoYXQuY29tL2VuL2Fib3V0L3JlZC1oYXQtZW5kLXVzZXItbGljZW5zZS1hZ3JlZW1lbnRzI1VCSVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTY5MTY5WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBzdW1tYXJ5PVwidWJpOSBtaWNybyBpbWFnZVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM4MDAzNDAyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBkZXNjcmlwdGlvbj1cIlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgyMjU4ODRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLms4cy5kaXNwbGF5LW5hbWU9XCJVYmk5LW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgzMjAxOTlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXM9XCJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA0LjkxOTA1OTE2OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBkaXI6MWMzNjc0ODA2NTg5ZWRhYmNiNTg1NGIxOThhMjlkZGU3ODZhYmI3MmU4YTU5NTA4OGRmNTI3Mjg1MzVjMTk5ZiBpbiAvICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1LjE4OTA4MTIyM1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOmVlYzczZjg1OWM2ZTdmNmM4YTk0MjdlY2M1MjQ5NTA0ZmU4OWFlNTRkYzNhMTUyMWI0NDI2NzRhOTA0OTdkMzIgaW4gL2V0Yy95dW0ucmVwb3MuZC91YmkucmVwbyAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS4xODkxMTM0MDRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIENNRCAvYmluL3NoIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuMTg5MTkyNDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHJlbGVhc2U9MTUiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS40NTY0OTQ0MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjI0ODgwMDQ4ZTQ5YzMwZDJlNWQ2ZGQ3M2VmNzg5NGU2OTIyMTczN2M5MmM0YWNjNDgxYWMxNmY1NDZjZWE3OTEgaW4gL3Jvb3QvYnVpbGRpbmZvL2NvbnRlbnRfbWFuaWZlc3RzL3ViaTktbWljcm8tY29udGFpbmVyLTkuMS4wLTE1Lmpzb24gIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuNzM0NTA1MjUyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTpjYTYxZjYyNzUwMTQ4MWFlZjYyNzcwZjM4ZmY0NjQyNzY2MzcwZDBiYjE2ZjM0MzBlZjI4NzkyNzcxMDAwYjE0IGluIC9yb290L2J1aWxkaW5mby9Eb2NrZXJmaWxlLXViaTktdWJpLW1pY3JvLTkuMS4wLTE1ICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1Ljk3NjcyMjE4OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgXCJkaXN0cmlidXRpb24tc2NvcGVcIj1cInB1YmxpY1wiIFwidmVuZG9yXCI9XCJSZWQgSGF0LCBJbmMuXCIgXCJidWlsZC1kYXRlXCI9XCIyMDIzLTAyLTIyVDEzOjU0OjI1XCIgXCJhcmNoaXRlY3R1cmVcIj1cIng4Nl82NFwiIFwidmNzLXR5cGVcIj1cImdpdFwiIFwidmNzLXJlZlwiPVwiYzU2M2UwOTFlMGM3YmQ1YTY5YjJhNDY5OTBkZGE0ZjU5NTk1YWEzN1wiIFwiaW8uazhzLmRlc2NyaXB0aW9uXCI9XCJWZXJ5IHNtYWxsIGltYWdlIHdoaWNoIGRvZXNuJ3QgaW5zdGFsbCB0aGUgcGFja2FnZSBtYW5hZ2VyLlwiIFwidXJsXCI9XCJodHRwczovL2FjY2Vzcy5yZWRoYXQuY29tL2NvbnRhaW5lcnMvIy9yZWdpc3RyeS5hY2Nlc3MucmVkaGF0LmNvbS91Ymk5L3ViaS1taWNyby9pbWFnZXMvOS4xLjAtMTVcIiJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJFTlYgTEFORz1lbl9VUy5VVEYtOCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJDT1BZIC90bXAvbnVsbC9yb290ZnMvIC8gIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC41NDg0MDQ5NzZaIiwiY3JlYXRlZF9ieSI6IkNPUFkgL29wdC9rZXljbG9hayAvb3B0L2tleWNsb2FrICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTQuNjI5OTY2MDk0WiIsImNyZWF0ZWRfYnkiOiJSVU4gL2Jpbi9zaCAtYyBlY2hvIFwia2V5Y2xvYWs6eDowOnJvb3RcIiBcdTAwM2VcdTAwM2UgL2V0Yy9ncm91cCBcdTAwMjZcdTAwMjYgICAgIGVjaG8gXCJrZXljbG9hazp4OjEwMDA6MDprZXljbG9hayB1c2VyOi9vcHQva2V5Y2xvYWs6L3NiaW4vbm9sb2dpblwiIFx1MDAzZVx1MDAzZSAvZXRjL3Bhc3N3ZCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJjcmVhdGVkX2J5IjoiVVNFUiAxMDAwIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODA4MC90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODQ0My90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVOVFJZUE9JTlQgW1wiL29wdC9rZXljbG9hay9iaW4va2Muc2hcIl0iLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfV0sIm1vYnkuYnVpbGRraXQuYnVpbGRpbmZvLnYxIjoiZXlKbWNtOXVkR1Z1WkNJNkltUnZZMnRsY21acGJHVXVkakFpTENKemIzVnlZMlZ6SWpwYmV5SjBlWEJsSWpvaVpHOWphMlZ5TFdsdFlXZGxJaXdpY21WbUlqb2ljbVZuYVhOMGNua3VZV05qWlhOekxuSmxaR2hoZEM1amIyMHZkV0pwT1MxdGFXTnlienBzWVhSbGMzUWlMQ0p3YVc0aU9pSnphR0V5TlRZNk5UTTJOemszTVRRNVlqSmxOakUwWXpFMll6RTRaRE00WVdVM1pqVXdPRGc1TWprM1pUa3lZVGRqWXpSaE5qQXlORGt4TkRjM1pHUmpNMkppTURZeFppSjlMSHNpZEhsd1pTSTZJbVJ2WTJ0bGNpMXBiV0ZuWlNJc0luSmxaaUk2SW5KbFoybHpkSEo1TG1GalkyVnpjeTV5WldSb1lYUXVZMjl0TDNWaWFUazZiR0YwWlhOMElpd2ljR2x1SWpvaWMyaGhNalUyT21OaU16QXpOREEwWlRVM05tWm1OVFV5T0dRMFpqQTRZakV5WVdRNE5XWmhZamhtTmpGbVlUbGxOV1JpWVRZM1lqTTNZakV4T1dSaU1qUTROalZrWmpNaWZWMTkiLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NGUzN2FlYWNjYjRjODAxNmUzODFkNmUyYjJhMGYyMmVhNTk5ODVhN2I5YjhlY2E2NzQ3MjZlOGM2MGYyZjUxZCIsInNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIiwic2hhMjU2OjgzMjllNDIyYjRmZDYzZmZkMDY1MTgzNDZlNWYxZjdiMzNlODE5MGE3OWU1YzMyMWY5YzUwYWJhODY1MWQzMGMiLCJzaGEyNTY6OTg3ZmQwMzBhYjJjZDYyOTQ0Y2I0ODdkZjg0NmMzMTJiNjRkYmIyYTZhMzEzMWE4MTI1M2MxNWE5ZGEyYTI2YyJdfX0=", + "repoDigests": [ + "keycloak/keycloak@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50" + ], + "architecture": "amd64", + "os": "linux" + } + }, + "distro": { + "name": "redhat", + "version": "9.1", + "idLike": [ + "fedora" + ] + }, + "descriptor": { + "name": "grype", + "version": "0.66.0", + "configuration": { + "configPath": "", + "verbosity": 0, + "output": [ + "json" + ], + "file": "", + "distro": "", + "add-cpes-if-none": false, + "output-template-file": "", + "quiet": true, + "check-for-app-update": true, + "only-fixed": false, + "only-notfixed": false, + "platform": "", + "search": { + "scope": "Squashed", + "unindexed-archives": false, + "indexed-archives": true + }, + "ignore": null, + "exclude": [], + "db": { + "cache-dir": "/Users/willmurphy/Library/Caches/grype/db", + "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json", + "ca-cert": "", + "auto-update": true, + "validate-by-hash-on-start": false, + "validate-age": true, + "max-allowed-built-age": 432000000000000 + }, + "externalSources": { + "enable": false, + "maven": { + "searchUpstreamBySha1": true, + "baseUrl": "https://search.maven.org/solrsearch/select" + } + }, + "match": { + "java": { + "using-cpes": true + }, + "dotnet": { + "using-cpes": true + }, + "golang": { + "using-cpes": true + }, + "javascript": { + "using-cpes": false + }, + "python": { + "using-cpes": true + }, + "ruby": { + "using-cpes": true + }, + "stock": { + "using-cpes": true + } + }, + "dev": { + "profile-cpu": false, + "profile-mem": false + }, + "fail-on-severity": "", + "registry": { + "insecure-skip-tls-verify": false, + "insecure-use-http": false, + "auth": [], + "ca-cert": "" + }, + "log": { + "structured": false, + "level": "", + "file": "" + }, + "show-suppressed": false, + "by-cve": false, + "name": "", + "default-image-pull-source": "" + }, + "db": { + "built": "2023-09-01T01:26:55Z", + "schemaVersion": 5, + "location": "/Users/willmurphy/Library/Caches/grype/db/5", + "checksum": "sha256:5db8bddae95f375db7186527c7554311e9ddc41e815ef2dbc28dc5d206ef2c7b", + "error": null + }, + "timestamp": "2023-09-01T08:13:42.20194-04:00" + } +} From 03ed0a0ac834ca5b8ffce6009718340ce2f45860 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 08:34:20 -0400 Subject: [PATCH 23/49] rename to get better out of struct name Signed-off-by: Will Murphy --- cmd/grype/cli/legacy/explain.go | 2 +- grype/presenter/explain/explain.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/grype/cli/legacy/explain.go b/cmd/grype/cli/legacy/explain.go index f46a287c454..0e75c732c64 100644 --- a/cmd/grype/cli/legacy/explain.go +++ b/cmd/grype/cli/legacy/explain.go @@ -32,7 +32,7 @@ var explainCmd = &cobra.Command{ if err != nil { return fmt.Errorf("unable to parse piped input: %+v", err) } - explainer := explain.NewBetterVulnerabilityExplainer(os.Stdout, &parseResult) + explainer := explain.NewVulnerabilityExplainer(os.Stdout, &parseResult) return explainer.ExplainByID(cveIDs) } // perform a scan, then explain requested CVEs diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index c359a1d2016..c43beeb2243 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -62,13 +62,13 @@ type ExplainedEvidence struct { ViaNamespace string } -type betterVulnerabilityExplainer struct { +type vulnerabilityExplainer struct { w io.Writer doc *models.Document } -func NewBetterVulnerabilityExplainer(w io.Writer, doc *models.Document) VulnerabilityExplainer { - return &betterVulnerabilityExplainer{ +func NewVulnerabilityExplainer(w io.Writer, doc *models.Document) VulnerabilityExplainer { + return &vulnerabilityExplainer{ w: w, doc: doc, } @@ -78,7 +78,7 @@ var funcs = template.FuncMap{ "trim": strings.TrimSpace, } -func (e *betterVulnerabilityExplainer) ExplainByID(IDs []string) error { +func (e *vulnerabilityExplainer) ExplainByID(IDs []string) error { // TODO: requested ID is always the primary match findings, err := ExplainDoc(e.doc, IDs) if err != nil { @@ -97,11 +97,11 @@ func (e *betterVulnerabilityExplainer) ExplainByID(IDs []string) error { return nil } -func (e *betterVulnerabilityExplainer) ExplainBySeverity(severity string) error { +func (e *vulnerabilityExplainer) ExplainBySeverity(severity string) error { return fmt.Errorf("not implemented") } -func (e *betterVulnerabilityExplainer) ExplainAll() error { +func (e *vulnerabilityExplainer) ExplainAll() error { findings, err := ExplainDoc(e.doc, nil) if err != nil { return err From 6856652f3a86018184ea71df18998b51bee5b41b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 08:34:41 -0400 Subject: [PATCH 24/49] group by artifact ID not purl, since purls are not unique Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index c43beeb2243..44443e892d1 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -222,9 +222,11 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } } - pURLsToMatchDetails := make(map[string]*ExplainedPackage) + // TODO: use package ID + idsToMatchDetails := make(map[string]*ExplainedPackage) for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { - key := m.Artifact.PURL + // key := m.Artifact.PURL + key := m.Artifact.ID // TODO: match details can match multiple packages var newLocations []ExplainedEvidence for _, l := range m.Artifact.Locations { @@ -250,7 +252,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } } } - e, ok := pURLsToMatchDetails[key] + e, ok := idsToMatchDetails[key] if !ok { e = &ExplainedPackage{ PURL: m.Artifact.PURL, @@ -262,7 +264,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { CPEExplanation: cpeExplanation, Locations: newLocations, } - pURLsToMatchDetails[key] = e + idsToMatchDetails[key] = e } else { // TODO: what if MatchedOnID and MatchedOnNamespace are different? e.Locations = append(e.Locations, newLocations...) @@ -279,9 +281,9 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { // } } } - var sortPURLs []string - for k, v := range pURLsToMatchDetails { - sortPURLs = append(sortPURLs, k) + var sortIDs []string + for k, v := range idsToMatchDetails { + sortIDs = append(sortIDs, k) dedupeLocations := make(map[string]ExplainedEvidence) for _, l := range v.Locations { dedupeLocations[l.Location] = l @@ -296,10 +298,18 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { v.Locations = uniqueLocations } // TODO: put the primary match first - sort.Strings(sortPURLs) + // sort.Slice(sortIDs, func(i, j int) bool { + // iKey := sortIDs[i] + // jKey := sortIDs[j] + // iMatch := idsToMatchDetails[iKey] + // jMatch := idsToMatchDetails[jKey] + // // Sort by type, exact-direct < cpe < exact-indirect + + // // if same type, sort alpha by PURL + // }) var explainedPackages []*ExplainedPackage - for _, k := range sortPURLs { - explainedPackages = append(explainedPackages, pURLsToMatchDetails[k]) + for _, k := range sortIDs { + explainedPackages = append(explainedPackages, idsToMatchDetails[k]) } // TODO: this isn't right at all. From 5f618530d6c64fda0a0e685a06ad628295615585 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 08:34:54 -0400 Subject: [PATCH 25/49] make output more yaml-y Signed-off-by: Will Murphy --- grype/presenter/explain/explain_cve_new.tmpl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl index c6d8cb2e602..857370aa84c 100644 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ b/grype/presenter/explain/explain_cve_new.tmpl @@ -3,11 +3,11 @@ Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} Matched packages: {{ range .MatchedPackages }} - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} - PURL: {{ .PURL }}{{ end }}{{ if .CPEExplanation }} - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} - {{ .IndirectExplanation }}{{ end }} - Evidenced by: {{ range .Locations }} - - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} + - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} + PURL: {{ .PURL }}{{ end }}{{ if .CPEExplanation }} + {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} + {{ .IndirectExplanation }}{{ end }} + Evidenced by: {{ range .Locations }} + - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} URLs: {{ range .URLs }} - {{ . }}{{ end }} From 004b3579e0d8c3fed7ff7a3029baae0a9c7fdf14 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 09:17:02 -0400 Subject: [PATCH 26/49] fix snapshot test with stable order Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 41 ++++++++++++++++++- grype/presenter/explain/explain.go | 34 +++++++++------ grype/presenter/explain/explain_cve_new.tmpl | 6 +-- .../explain/explain_snapshot_test.go | 6 +-- 4 files changed, 67 insertions(+), 20 deletions(-) diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index 70da37a076d..d9e3b772c4a 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -1,4 +1,43 @@ [TestExplainSnapshot - 1] -[]uint8(nil) +CVE-2020-12413 from nvd:cpe (Medium) +The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites. +Related vulnerabilities: + - redhat:distro:redhat:9 CVE-2020-12413 (Low) +Matched packages: + - Package: nspr, version: 4.34.0-17.el9_1 + PURL: pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 + Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Evidenced by: + - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: ff2aefb138ebd4bf) + - Package: nss, version: 3.79.0-17.el9_1 + PURL: pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 + Evidenced by: + - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 840f8a931c86688f) + - Package: nss-softokn, version: 3.79.0-17.el9_1 + PURL: pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 + Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Evidenced by: + - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 7d1c659d9eb00024) + - Package: nss-softokn-freebl, version: 3.79.0-17.el9_1 + PURL: pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 + Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Evidenced by: + - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: cb1f96627e29924e) + - Package: nss-sysinit, version: 3.79.0-17.el9_1 + PURL: pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 + Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Evidenced by: + - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: d096d490e4fccf36) + - Package: nss-util, version: 3.79.0-17.el9_1 + PURL: pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 + Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Evidenced by: + - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 641950c22b3f5035) +URLs: + - https://nvd.nist.gov/vuln/detail/CVE-2020-12413 + - https://access.redhat.com/security/cve/CVE-2020-12413 + - https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413 + - https://raccoon-attack.com/ + --- diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 44443e892d1..8399e0f9354 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -53,6 +53,7 @@ type ExplainedPackage struct { IndirectExplanation string CPEExplanation string Locations []ExplainedEvidence + displayRank int // how early in output should this appear? } type ExplainedEvidence struct { @@ -241,14 +242,18 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { // Like, I have N matchDetails, and N locations, but I don't know which matchDetail explains which location var indirectExplanation string var cpeExplanation string + var displayRank int for i, md := range m.MatchDetails { explanation := explainMatchDetail(m, i) if explanation != "" { if md.Type == string(match.CPEMatch) { cpeExplanation = explanation - } - if md.Type == string(match.ExactIndirectMatch) { + displayRank = 1 + } else if md.Type == string(match.ExactIndirectMatch) { indirectExplanation = explanation + displayRank = 0 // display indirect explanations explanations of main matched packages + } else { + displayRank = 2 } } } @@ -263,17 +268,20 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { IndirectExplanation: indirectExplanation, CPEExplanation: cpeExplanation, Locations: newLocations, + displayRank: displayRank, } idsToMatchDetails[key] = e } else { // TODO: what if MatchedOnID and MatchedOnNamespace are different? e.Locations = append(e.Locations, newLocations...) + // TODO: why are these checks needed? What if a package is matched by more than one way? if e.CPEExplanation == "" { e.CPEExplanation = cpeExplanation } if e.IndirectExplanation == "" { e.IndirectExplanation = indirectExplanation } + e.displayRank += displayRank // e.Explanations = append(e.Explanations, newExplanations...) // if e.MatchedOnID != m.Vulnerability.ID || e.MatchedOnNamespace != m.Vulnerability.Namespace { // // TODO: do something smart. @@ -297,16 +305,18 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { }) v.Locations = uniqueLocations } - // TODO: put the primary match first - // sort.Slice(sortIDs, func(i, j int) bool { - // iKey := sortIDs[i] - // jKey := sortIDs[j] - // iMatch := idsToMatchDetails[iKey] - // jMatch := idsToMatchDetails[jKey] - // // Sort by type, exact-direct < cpe < exact-indirect - - // // if same type, sort alpha by PURL - // }) + + sort.Slice(sortIDs, func(i, j int) bool { + iKey := sortIDs[i] + jKey := sortIDs[j] + iMatch := idsToMatchDetails[iKey] + jMatch := idsToMatchDetails[jKey] + // reverse by display rank + if iMatch.displayRank != jMatch.displayRank { + return jMatch.displayRank < iMatch.displayRank + } + return iMatch.Name < jMatch.Name + }) var explainedPackages []*ExplainedPackage for _, k := range sortIDs { explainedPackages = append(explainedPackages, idsToMatchDetails[k]) diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl index 857370aa84c..21c078f183b 100644 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ b/grype/presenter/explain/explain_cve_new.tmpl @@ -2,12 +2,12 @@ {{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} -Matched packages: {{ range .MatchedPackages }} +Matched packages:{{ range .MatchedPackages }} - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} PURL: {{ .PURL }}{{ end }}{{ if .CPEExplanation }} {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} {{ .IndirectExplanation }}{{ end }} - Evidenced by: {{ range .Locations }} + Evidenced by:{{ range .Locations }} - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} -URLs: {{ range .URLs }} +URLs:{{ range .URLs }} - {{ . }}{{ end }} diff --git a/grype/presenter/explain/explain_snapshot_test.go b/grype/presenter/explain/explain_snapshot_test.go index 59243e2da4f..73e75a75ea8 100644 --- a/grype/presenter/explain/explain_snapshot_test.go +++ b/grype/presenter/explain/explain_snapshot_test.go @@ -1,7 +1,6 @@ package explain_test import ( - "bufio" "bytes" "encoding/json" "os" @@ -25,12 +24,11 @@ func TestExplainSnapshot(t *testing.T) { err = decoder.Decode(&doc) require.NoError(t, err) // create explain.VulnerabilityExplainer - var b bytes.Buffer - w := bufio.NewWriter(&b) + w := bytes.NewBufferString("") explainer := explain.NewVulnerabilityExplainer(w, &doc) // call ExplainByID err = explainer.ExplainByID([]string{"CVE-2020-12413"}) require.NoError(t, err) // assert output - snaps.MatchSnapshot(t, b.Bytes()) + snaps.MatchSnapshot(t, w.String()) } From ee6238caafcd8b561f95e295cdc70f0782eef2fe Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 09:18:23 -0400 Subject: [PATCH 27/49] auto-lint Signed-off-by: Will Murphy --- cmd/grype/cli/legacy/explain.go | 3 ++- grype/presenter/explain/explain_snapshot_test.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/grype/cli/legacy/explain.go b/cmd/grype/cli/legacy/explain.go index 0e75c732c64..d248957e8f6 100644 --- a/cmd/grype/cli/legacy/explain.go +++ b/cmd/grype/cli/legacy/explain.go @@ -5,11 +5,12 @@ import ( "fmt" "os" + "github.com/spf13/cobra" + "github.com/anchore/grype/grype/presenter/explain" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" - "github.com/spf13/cobra" ) var cveIDs []string diff --git a/grype/presenter/explain/explain_snapshot_test.go b/grype/presenter/explain/explain_snapshot_test.go index 73e75a75ea8..2afbab318b4 100644 --- a/grype/presenter/explain/explain_snapshot_test.go +++ b/grype/presenter/explain/explain_snapshot_test.go @@ -6,10 +6,11 @@ import ( "os" "testing" - "github.com/anchore/grype/grype/presenter/explain" - "github.com/anchore/grype/grype/presenter/models" "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/require" + + "github.com/anchore/grype/grype/presenter/explain" + "github.com/anchore/grype/grype/presenter/models" ) func TestExplainSnapshot(t *testing.T) { From d62b645419eea9823e733e9aa5e83189e13fd5b1 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 14:29:47 -0400 Subject: [PATCH 28/49] add additional test case Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 32 +- .../explain/explain_snapshot_test.go | 52 ++- .../test-fixtures/chainguard-ruby-test.json | 429 ++++++++++++++++++ 3 files changed, 495 insertions(+), 18 deletions(-) create mode 100644 grype/presenter/explain/test-fixtures/chainguard-ruby-test.json diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index d9e3b772c4a..656d18bdc8b 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -1,5 +1,4 @@ - -[TestExplainSnapshot - 1] +[TestExplainSnapshot/keycloak-CVE-2020-12413 - 1] CVE-2020-12413 from nvd:cpe (Medium) The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites. Related vulnerabilities: @@ -41,3 +40,32 @@ URLs: - https://raccoon-attack.com/ --- + +[TestExplainSnapshot/chainguard-ruby-CVE-2023-28755 - 1] +CVE-2023-28755 from nvd:cpe (High) +A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1. +Related vulnerabilities: + - github:language:ruby GHSA-hv5j-3h9f-99c2 (High) + - wolfi:distro:wolfi:rolling CVE-2023-28755 (High) +Matched packages: + - Package: ruby-3.0, version: 3.0.4-r1 + PURL: pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118 + CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*` + Note: This CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package. + Evidenced by: + - wolfi:distro:wolfi:rolling:CVE-2023-28755 evidence at /lib/apk/db/installed (artifact ID: ) + - nvd:cpe:CVE-2023-28755 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec (artifact ID: ) +URLs: + - https://nvd.nist.gov/vuln/detail/CVE-2023-28755 + - https://github.com/ruby/uri/releases/ + - https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/ + - https://www.ruby-lang.org/en/downloads/releases/ + - https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/ + - https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/ + - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755 + - https://github.com/advisories/GHSA-hv5j-3h9f-99c2 + +--- diff --git a/grype/presenter/explain/explain_snapshot_test.go b/grype/presenter/explain/explain_snapshot_test.go index 2afbab318b4..7834c3588f5 100644 --- a/grype/presenter/explain/explain_snapshot_test.go +++ b/grype/presenter/explain/explain_snapshot_test.go @@ -15,21 +15,41 @@ import ( func TestExplainSnapshot(t *testing.T) { // load sample json - // TODO: make and commit minimal sample JSON - r, err := os.Open("./test-fixtures/keycloak-test.json") - require.NoError(t, err) + testCases := []struct { + name string + fixture string + vulnerabilityIDs []string + }{ + { + name: "keycloak-CVE-2020-12413", + fixture: "./test-fixtures/keycloak-test.json", + vulnerabilityIDs: []string{"CVE-2020-12413"}, + }, + { + name: "chainguard-ruby-CVE-2023-28755", + fixture: "test-fixtures/chainguard-ruby-test.json", + vulnerabilityIDs: []string{"CVE-2023-28755"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r, err := os.Open(tc.fixture) + require.NoError(t, err) + + // parse to models.Document + doc := models.Document{} + decoder := json.NewDecoder(r) + err = decoder.Decode(&doc) + require.NoError(t, err) + // create explain.VulnerabilityExplainer + w := bytes.NewBufferString("") + explainer := explain.NewVulnerabilityExplainer(w, &doc) + // call ExplainByID + err = explainer.ExplainByID(tc.vulnerabilityIDs) + require.NoError(t, err) + // assert output + snaps.MatchSnapshot(t, w.String()) + }) + } - // parse to models.Document - doc := models.Document{} - decoder := json.NewDecoder(r) - err = decoder.Decode(&doc) - require.NoError(t, err) - // create explain.VulnerabilityExplainer - w := bytes.NewBufferString("") - explainer := explain.NewVulnerabilityExplainer(w, &doc) - // call ExplainByID - err = explainer.ExplainByID([]string{"CVE-2020-12413"}) - require.NoError(t, err) - // assert output - snaps.MatchSnapshot(t, w.String()) } diff --git a/grype/presenter/explain/test-fixtures/chainguard-ruby-test.json b/grype/presenter/explain/test-fixtures/chainguard-ruby-test.json new file mode 100644 index 00000000000..22151b2e66e --- /dev/null +++ b/grype/presenter/explain/test-fixtures/chainguard-ruby-test.json @@ -0,0 +1,429 @@ +{ + "matches": [ + { + "vulnerability": { + "id": "CVE-2023-28755", + "dataSource": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755", + "namespace": "wolfi:distro:wolfi:rolling", + "severity": "High", + "urls": [ + "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755" + ], + "cvss": [], + "fix": { + "versions": [ + "3.0.6-r0" + ], + "state": "fixed" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2023-28755", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755", + "namespace": "nvd:cpe", + "severity": "High", + "urls": [ + "https://github.com/ruby/uri/releases/", + "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/", + "https://www.ruby-lang.org/en/downloads/releases/", + "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/", + "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/" + ], + "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "metrics": { + "baseScore": 7.5, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-indirect-match", + "matcher": "apk-matcher", + "searchedBy": { + "distro": { + "type": "wolfi", + "version": "20221118" + }, + "namespace": "wolfi:distro:wolfi:rolling", + "package": { + "name": "ruby-3.0", + "version": "3.0.4-r1" + } + }, + "found": { + "versionConstraint": "< 3.0.6-r0 (apk)", + "vulnerabilityID": "CVE-2023-28755" + } + }, + { + "type": "exact-direct-match", + "matcher": "apk-matcher", + "searchedBy": { + "distro": { + "type": "wolfi", + "version": "20221118" + }, + "namespace": "wolfi:distro:wolfi:rolling", + "package": { + "name": "ruby-3.0", + "version": "3.0.4-r1" + } + }, + "found": { + "versionConstraint": "< 3.0.6-r0 (apk)", + "vulnerabilityID": "CVE-2023-28755" + } + } + ], + "artifact": { + "name": "ruby-3.0", + "version": "3.0.4-r1", + "type": "apk", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880" + } + ], + "language": "", + "licenses": [ + "PSF-2.0" + ], + "cpes": [ + "cpe:2.3:a:ruby-lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:ruby:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:ruby:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:ruby:3.0.4-r1:*:*:*:*:*:*:*" + ], + "purl": "pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118", + "upstreams": [ + { + "name": "ruby-3.0" + } + ] + } + }, + { + "vulnerability": { + "id": "CVE-2023-28755", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755", + "namespace": "nvd:cpe", + "severity": "High", + "urls": [ + "https://github.com/ruby/uri/releases/", + "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/", + "https://www.ruby-lang.org/en/downloads/releases/", + "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/", + "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/" + ], + "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "metrics": { + "baseScore": 7.5, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ], + "fix": { + "versions": [], + "state": "unknown" + }, + "advisories": [] + }, + "relatedVulnerabilities": [], + "matchDetails": [ + { + "type": "cpe-match", + "matcher": "ruby-gem-matcher", + "searchedBy": { + "namespace": "nvd:cpe", + "cpes": [ + "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*" + ] + }, + "found": { + "vulnerabilityID": "CVE-2023-28755", + "versionConstraint": "<= 0.10.0 || = 0.10.1 || = 0.11.0 || = 0.12.0 (unknown)", + "cpes": [ + "cpe:2.3:a:ruby-lang:uri:*:*:*:*:*:ruby:*:*", + "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:ruby:*:*" + ] + } + } + ], + "artifact": { + "name": "uri", + "version": "0.10.1", + "type": "gem", + "locations": [ + { + "path": "/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec", + "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880" + } + ], + "language": "ruby", + "licenses": [ + "Ruby", + "BSD-2-Clause" + ], + "cpes": [ + "cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*" + ], + "purl": "pkg:gem/uri@0.10.1", + "upstreams": [] + } + }, + { + "vulnerability": { + "id": "GHSA-hv5j-3h9f-99c2", + "dataSource": "https://github.com/advisories/GHSA-hv5j-3h9f-99c2", + "namespace": "github:language:ruby", + "severity": "High", + "urls": [ + "https://github.com/advisories/GHSA-hv5j-3h9f-99c2" + ], + "description": "Ruby URI component ReDoS issue", + "cvss": [], + "fix": { + "versions": [ + "0.10.2" + ], + "state": "fixed" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2023-28755", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-28755", + "namespace": "nvd:cpe", + "severity": "High", + "urls": [ + "https://github.com/ruby/uri/releases/", + "https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/", + "https://www.ruby-lang.org/en/downloads/releases/", + "https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/", + "https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/" + ], + "description": "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "metrics": { + "baseScore": 7.5, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-direct-match", + "matcher": "ruby-gem-matcher", + "searchedBy": { + "language": "ruby", + "namespace": "github:language:ruby" + }, + "found": { + "versionConstraint": "=0.10.1 (unknown)", + "vulnerabilityID": "GHSA-hv5j-3h9f-99c2" + } + } + ], + "artifact": { + "name": "uri", + "version": "0.10.1", + "type": "gem", + "locations": [ + { + "path": "/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec", + "layerID": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880" + } + ], + "language": "ruby", + "licenses": [ + "Ruby", + "BSD-2-Clause" + ], + "cpes": [ + "cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*", + "cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*" + ], + "purl": "pkg:gem/uri@0.10.1", + "upstreams": [] + } + } + ], + "source": { + "type": "image", + "target": { + "userInput": "cgr.dev/chainguard/ruby:latest-3.0", + "imageID": "sha256:2f88265cfbc43ca35cd327347a9f59375b9f29ef998b8a54a882e31111266640", + "manifestDigest": "sha256:86abe662dfa3746038eea6b0db91092b0767d78b8b8938a343d614cd1579adc2", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [ + "cgr.dev/chainguard/ruby:latest-3.0" + ], + "imageSize": 38865264, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880", + "size": 38865264 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo1NTMsImRpZ2VzdCI6InNoYTI1NjoyZjg4MjY1Y2ZiYzQzY2EzNWNkMzI3MzQ3YTlmNTkzNzViOWYyOWVmOTk4YjhhNTRhODgyZTMxMTExMjY2NjQwIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQxMTYxNzI4LCJkaWdlc3QiOiJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJ9XX0=", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImF1dGhvciI6ImdpdGh1Yi5jb20vY2hhaW5ndWFyZC1kZXYvYXBrbyIsImNyZWF0ZWQiOiIyMDIzLTAxLTEzVDAwOjExOjI2WiIsImhpc3RvcnkiOlt7ImF1dGhvciI6ImFwa28iLCJjcmVhdGVkIjoiMjAyMy0wMS0xM1QwMDoxMToyNloiLCJjcmVhdGVkX2J5IjoiYXBrbyIsImNvbW1lbnQiOiJUaGlzIGlzIGFuIGFwa28gc2luZ2xlLWxheWVyIGltYWdlIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJdfSwiY29uZmlnIjp7IkNtZCI6WyIvdXNyL2Jpbi9pcmIiXSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIiwiU1NMX0NFUlRfRklMRT0vZXRjL3NzbC9jZXJ0cy9jYS1jZXJ0aWZpY2F0ZXMuY3J0Il0sIlVzZXIiOiI2NTUzMiIsIldvcmtpbmdEaXIiOiIvd29yayJ9fQ==", + "repoDigests": [ + "cgr.dev/chainguard/ruby@sha256:3c9afb4f188827ea1062ec3b8acea32893236a0d7df31e0498df93486cff0978" + ], + "architecture": "arm64", + "os": "linux" + } + }, + "distro": { + "name": "wolfi", + "version": "20221118", + "idLike": [] + }, + "descriptor": { + "name": "grype", + "version": "0.61.1", + "configuration": { + "configPath": "", + "verbosity": 0, + "output": "json", + "file": "", + "distro": "", + "add-cpes-if-none": false, + "output-template-file": "", + "quiet": false, + "check-for-app-update": true, + "only-fixed": false, + "only-notfixed": false, + "platform": "", + "search": { + "scope": "Squashed", + "unindexed-archives": false, + "indexed-archives": true + }, + "ignore": null, + "exclude": [], + "db": { + "cache-dir": "/Users/willmurphy/Library/Caches/grype/db", + "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json", + "ca-cert": "", + "auto-update": true, + "validate-by-hash-on-start": false, + "validate-age": true, + "max-allowed-built-age": 432000000000000 + }, + "externalSources": { + "enable": false, + "maven": { + "searchUpstreamBySha1": true, + "baseUrl": "https://search.maven.org/solrsearch/select" + } + }, + "match": { + "java": { + "using-cpes": true + }, + "dotnet": { + "using-cpes": true + }, + "golang": { + "using-cpes": true + }, + "javascript": { + "using-cpes": false + }, + "python": { + "using-cpes": true + }, + "ruby": { + "using-cpes": true + }, + "stock": { + "using-cpes": true + } + }, + "dev": { + "profile-cpu": false, + "profile-mem": false + }, + "fail-on-severity": "", + "registry": { + "insecure-skip-tls-verify": false, + "insecure-use-http": false, + "auth": [] + }, + "log": { + "structured": false, + "level": "warn", + "file": "" + }, + "show-suppressed": false, + "by-cve": false, + "name": "", + "default-image-pull-source": "" + }, + "db": { + "built": "2023-05-17T01:32:43Z", + "schemaVersion": 5, + "location": "/Users/willmurphy/Library/Caches/grype/db/5", + "checksum": "sha256:84ebb8325f426565e7a0cd00b2ea265a0ee0ec69db158a65541a42fddd1e15b0", + "error": null + }, + "timestamp": "2023-05-17T21:00:56.783213-04:00" + } +} From aef26663f0258abcd58eda4cf1927c880db73e26 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 14:40:15 -0400 Subject: [PATCH 29/49] more linting Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 21 ++++++++------------ grype/presenter/explain/explain_cve_new.tmpl | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 8399e0f9354..d5ed27ad48d 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -58,7 +58,7 @@ type ExplainedPackage struct { type ExplainedEvidence struct { Location string - ArtifactId string + ArtifactID string ViaVulnID string ViaNamespace string } @@ -79,14 +79,14 @@ var funcs = template.FuncMap{ "trim": strings.TrimSpace, } -func (e *vulnerabilityExplainer) ExplainByID(IDs []string) error { +func (e *vulnerabilityExplainer) ExplainByID(ids []string) error { // TODO: requested ID is always the primary match - findings, err := ExplainDoc(e.doc, IDs) + findings, err := Doc(e.doc, ids) if err != nil { return err } t := template.Must(template.New("explanation").Funcs(funcs).Parse(explainTemplate)) - for _, id := range IDs { + for _, id := range ids { finding, ok := findings[id] if !ok { continue @@ -103,7 +103,7 @@ func (e *vulnerabilityExplainer) ExplainBySeverity(severity string) error { } func (e *vulnerabilityExplainer) ExplainAll() error { - findings, err := ExplainDoc(e.doc, nil) + findings, err := Doc(e.doc, nil) if err != nil { return err } @@ -112,7 +112,7 @@ func (e *vulnerabilityExplainer) ExplainAll() error { return t.Execute(e.w, findings) } -func ExplainDoc(doc *models.Document, requestedIDs []string) (ExplainedFindings, error) { +func Doc(doc *models.Document, requestedIDs []string) (ExplainedFindings, error) { result := make(ExplainedFindings) builders := make(map[string]*ExplainViewModelBuilder) for _, m := range doc.Matches { @@ -185,16 +185,11 @@ func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate models.Match, userReque return false } // Either the user didn't ask for specific IDs, or the candidate has an ID the user asked for. - currentPrimaryIsChildOfCandidate := false for _, related := range b.PrimaryMatch.RelatedVulnerabilities { if related.ID == candidate.Vulnerability.ID { - currentPrimaryIsChildOfCandidate = true - break + return true } } - if currentPrimaryIsChildOfCandidate { - return true - } return false } @@ -233,7 +228,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { for _, l := range m.Artifact.Locations { newLocations = append(newLocations, ExplainedEvidence{ Location: l.RealPath, - ArtifactId: m.Artifact.ID, // TODO: this is sometimes blank. Why? + ArtifactID: m.Artifact.ID, // TODO: this is sometimes blank. Why? ViaVulnID: m.Vulnerability.ID, ViaNamespace: m.Vulnerability.Namespace, }) diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl index 21c078f183b..004e6d3f449 100644 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ b/grype/presenter/explain/explain_cve_new.tmpl @@ -8,6 +8,6 @@ Matched packages:{{ range .MatchedPackages }} {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} {{ .IndirectExplanation }}{{ end }} Evidenced by:{{ range .Locations }} - - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactId}}){{ end }}{{ end }} + - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactID}}){{ end }}{{ end }} URLs:{{ range .URLs }} - {{ . }}{{ end }} From d811906a6269f3266ef0045f9593accef9c2ec51 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 14:51:03 -0400 Subject: [PATCH 30/49] more linting Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 57 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index d5ed27ad48d..01badab3b8e 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -29,22 +29,22 @@ type VulnerabilityExplainer interface { // of artifacts we matched and why, and then // render it either as JSON or as the template. -type ExplainViewModel struct { +type ViewModel struct { PrimaryVulnerability models.VulnerabilityMetadata RelatedVulnerabilities []models.VulnerabilityMetadata - MatchedPackages []*ExplainedPackage + MatchedPackages []*explainedPackage URLs []string } -type ExplainViewModelBuilder struct { +type viewModelBuilder struct { PrimaryVulnerability models.Vulnerability // this is the vulnerability we're trying to explain PrimaryMatch models.Match RelatedMatches []models.Match } -type ExplainedFindings map[string]ExplainViewModel +type Findings map[string]ViewModel -type ExplainedPackage struct { +type explainedPackage struct { PURL string Name string Version string // TODO: is there only going to be one of these? @@ -52,11 +52,11 @@ type ExplainedPackage struct { MatchedOnNamespace string IndirectExplanation string CPEExplanation string - Locations []ExplainedEvidence + Locations []explainedEvidence displayRank int // how early in output should this appear? } -type ExplainedEvidence struct { +type explainedEvidence struct { Location string ArtifactID string ViaVulnID string @@ -98,7 +98,7 @@ func (e *vulnerabilityExplainer) ExplainByID(ids []string) error { return nil } -func (e *vulnerabilityExplainer) ExplainBySeverity(severity string) error { +func (e *vulnerabilityExplainer) ExplainBySeverity(_ string) error { return fmt.Errorf("not implemented") } @@ -112,14 +112,14 @@ func (e *vulnerabilityExplainer) ExplainAll() error { return t.Execute(e.w, findings) } -func Doc(doc *models.Document, requestedIDs []string) (ExplainedFindings, error) { - result := make(ExplainedFindings) - builders := make(map[string]*ExplainViewModelBuilder) +func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { + result := make(Findings) + builders := make(map[string]*viewModelBuilder) for _, m := range doc.Matches { key := m.Vulnerability.ID existing, ok := builders[key] if !ok { - existing = NewExplainedVulnerabilityBuilder() + existing = newBuilder() builders[m.Vulnerability.ID] = existing } existing.WithMatch(m, requestedIDs, false) @@ -129,7 +129,7 @@ func Doc(doc *models.Document, requestedIDs []string) (ExplainedFindings, error) key := related.ID existing, ok := builders[key] if !ok { - existing = NewExplainedVulnerabilityBuilder() + existing = newBuilder() builders[key] = existing } existing.WithMatch(m, requestedIDs, false) @@ -141,13 +141,13 @@ func Doc(doc *models.Document, requestedIDs []string) (ExplainedFindings, error) return result, nil } -func NewExplainedVulnerabilityBuilder() *ExplainViewModelBuilder { - return &ExplainViewModelBuilder{} +func newBuilder() *viewModelBuilder { + return &viewModelBuilder{} } // WithMatch adds a match to the builder // accepting enough information to determine whether the match is a primary match or a related match -func (b *ExplainViewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string, graphIsByCVE bool) { +func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string, graphIsByCVE bool) { // TODO: check if it's a primary vulnerability // (the below checks if it's a primary _match_, which is wrong) if b.isPrimaryAdd(m, userRequestedIDs, graphIsByCVE) { @@ -162,7 +162,7 @@ func (b *ExplainViewModelBuilder) WithMatch(m models.Match, userRequestedIDs []s } } -func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string, graphIsByCVE bool) bool { +func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string, graphIsByCVE bool) bool { // TODO: "primary" is a property of a vulnerability, not a match // if there's not currently any match, make this one primary since we don't know any better if b.PrimaryMatch.Vulnerability.ID == "" { @@ -193,17 +193,17 @@ func (b *ExplainViewModelBuilder) isPrimaryAdd(candidate models.Match, userReque return false } -func (b *ExplainViewModelBuilder) WithPrimaryMatch(m models.Match) *ExplainViewModelBuilder { +func (b *viewModelBuilder) WithPrimaryMatch(m models.Match) *viewModelBuilder { b.PrimaryMatch = m return b } -func (b *ExplainViewModelBuilder) WithRelatedMatch(m models.Match) *ExplainViewModelBuilder { +func (b *viewModelBuilder) WithRelatedMatch(m models.Match) *viewModelBuilder { b.RelatedMatches = append(b.RelatedMatches, m) return b } -func (b *ExplainViewModelBuilder) Build() ExplainViewModel { +func (b *viewModelBuilder) Build() ViewModel { URLs := b.PrimaryMatch.Vulnerability.URLs URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) for _, v := range b.PrimaryMatch.RelatedVulnerabilities { @@ -218,15 +218,14 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } } - // TODO: use package ID - idsToMatchDetails := make(map[string]*ExplainedPackage) + idsToMatchDetails := make(map[string]*explainedPackage) for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { // key := m.Artifact.PURL key := m.Artifact.ID // TODO: match details can match multiple packages - var newLocations []ExplainedEvidence + var newLocations []explainedEvidence for _, l := range m.Artifact.Locations { - newLocations = append(newLocations, ExplainedEvidence{ + newLocations = append(newLocations, explainedEvidence{ Location: l.RealPath, ArtifactID: m.Artifact.ID, // TODO: this is sometimes blank. Why? ViaVulnID: m.Vulnerability.ID, @@ -254,7 +253,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } e, ok := idsToMatchDetails[key] if !ok { - e = &ExplainedPackage{ + e = &explainedPackage{ PURL: m.Artifact.PURL, Name: m.Artifact.Name, Version: m.Artifact.Version, @@ -287,11 +286,11 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { var sortIDs []string for k, v := range idsToMatchDetails { sortIDs = append(sortIDs, k) - dedupeLocations := make(map[string]ExplainedEvidence) + dedupeLocations := make(map[string]explainedEvidence) for _, l := range v.Locations { dedupeLocations[l.Location] = l } - var uniqueLocations []ExplainedEvidence + var uniqueLocations []explainedEvidence for _, l := range dedupeLocations { uniqueLocations = append(uniqueLocations, l) } @@ -312,7 +311,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { } return iMatch.Name < jMatch.Name }) - var explainedPackages []*ExplainedPackage + var explainedPackages []*explainedPackage for _, k := range sortIDs { explainedPackages = append(explainedPackages, idsToMatchDetails[k]) } @@ -351,7 +350,7 @@ func (b *ExplainViewModelBuilder) Build() ExplainViewModel { relatedVulnerabilities = append(relatedVulnerabilities, dedupeRelatedVulnerabilities[k]) } - return ExplainViewModel{ + return ViewModel{ PrimaryVulnerability: primaryVulnerability, RelatedVulnerabilities: relatedVulnerabilities, MatchedPackages: explainedPackages, From 6c715463fa8aa7f8d8e716e5346358c40faa20d4 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 14:56:29 -0400 Subject: [PATCH 31/49] remove unused graphIsByCVE Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 01badab3b8e..1a7b9aa7fcd 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -122,7 +122,7 @@ func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { existing = newBuilder() builders[m.Vulnerability.ID] = existing } - existing.WithMatch(m, requestedIDs, false) + existing.WithMatch(m, requestedIDs) } for _, m := range doc.Matches { for _, related := range m.RelatedVulnerabilities { @@ -132,7 +132,7 @@ func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { existing = newBuilder() builders[key] = existing } - existing.WithMatch(m, requestedIDs, false) + existing.WithMatch(m, requestedIDs) } } for k, v := range builders { @@ -147,10 +147,10 @@ func newBuilder() *viewModelBuilder { // WithMatch adds a match to the builder // accepting enough information to determine whether the match is a primary match or a related match -func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string, graphIsByCVE bool) { +func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) { // TODO: check if it's a primary vulnerability // (the below checks if it's a primary _match_, which is wrong) - if b.isPrimaryAdd(m, userRequestedIDs, graphIsByCVE) { + if b.isPrimaryAdd(m, userRequestedIDs) { // Demote the current primary match to related match // if it exists if b.PrimaryMatch.Vulnerability.ID != "" { @@ -162,16 +162,13 @@ func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string, } } -func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string, graphIsByCVE bool) bool { +func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string) bool { // TODO: "primary" is a property of a vulnerability, not a match // if there's not currently any match, make this one primary since we don't know any better if b.PrimaryMatch.Vulnerability.ID == "" { return true } - // There is a primary match, so we need to determine if the candidate is "more primary" - if graphIsByCVE { - panic("not implemented") // by-cve graphs are upside down. - } + idWasRequested := false for _, id := range userRequestedIDs { if candidate.Vulnerability.ID == id { From 340305641ac1ea539e12b840393c7a79df0bc225 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 15:03:39 -0400 Subject: [PATCH 32/49] almost all the linters Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 1a7b9aa7fcd..af269e78cb7 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -237,13 +237,14 @@ func (b *viewModelBuilder) Build() ViewModel { for i, md := range m.MatchDetails { explanation := explainMatchDetail(m, i) if explanation != "" { - if md.Type == string(match.CPEMatch) { + switch md.Type { + case string(match.CPEMatch): cpeExplanation = explanation displayRank = 1 - } else if md.Type == string(match.ExactIndirectMatch) { + case string(match.ExactIndirectMatch): indirectExplanation = explanation displayRank = 0 // display indirect explanations explanations of main matched packages - } else { + default: displayRank = 2 } } From fb6e5fd91e9ef54c5d10284fd45c25122ba9d92a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 17:03:07 -0400 Subject: [PATCH 33/49] extract url logic; still a few TODOs Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 179 +++++++++++++++-------------- 1 file changed, 93 insertions(+), 86 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index af269e78cb7..ceb89919f72 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -201,86 +201,7 @@ func (b *viewModelBuilder) WithRelatedMatch(m models.Match) *viewModelBuilder { } func (b *viewModelBuilder) Build() ViewModel { - URLs := b.PrimaryMatch.Vulnerability.URLs - URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) - for _, v := range b.PrimaryMatch.RelatedVulnerabilities { - URLs = append(URLs, v.URLs...) - URLs = append(URLs, v.DataSource) - } - for _, m := range b.RelatedMatches { - URLs = append(URLs, m.Vulnerability.URLs...) - for _, v := range m.RelatedVulnerabilities { - URLs = append(URLs, v.URLs...) - URLs = append(URLs, v.DataSource) - } - } - - idsToMatchDetails := make(map[string]*explainedPackage) - for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { - // key := m.Artifact.PURL - key := m.Artifact.ID - // TODO: match details can match multiple packages - var newLocations []explainedEvidence - for _, l := range m.Artifact.Locations { - newLocations = append(newLocations, explainedEvidence{ - Location: l.RealPath, - ArtifactID: m.Artifact.ID, // TODO: this is sometimes blank. Why? - ViaVulnID: m.Vulnerability.ID, - ViaNamespace: m.Vulnerability.Namespace, - }) - } - // TODO: how can match details explain locations? - // Like, I have N matchDetails, and N locations, but I don't know which matchDetail explains which location - var indirectExplanation string - var cpeExplanation string - var displayRank int - for i, md := range m.MatchDetails { - explanation := explainMatchDetail(m, i) - if explanation != "" { - switch md.Type { - case string(match.CPEMatch): - cpeExplanation = explanation - displayRank = 1 - case string(match.ExactIndirectMatch): - indirectExplanation = explanation - displayRank = 0 // display indirect explanations explanations of main matched packages - default: - displayRank = 2 - } - } - } - e, ok := idsToMatchDetails[key] - if !ok { - e = &explainedPackage{ - PURL: m.Artifact.PURL, - Name: m.Artifact.Name, - Version: m.Artifact.Version, - MatchedOnID: m.Vulnerability.ID, - MatchedOnNamespace: m.Vulnerability.Namespace, - IndirectExplanation: indirectExplanation, - CPEExplanation: cpeExplanation, - Locations: newLocations, - displayRank: displayRank, - } - idsToMatchDetails[key] = e - } else { - // TODO: what if MatchedOnID and MatchedOnNamespace are different? - e.Locations = append(e.Locations, newLocations...) - // TODO: why are these checks needed? What if a package is matched by more than one way? - if e.CPEExplanation == "" { - e.CPEExplanation = cpeExplanation - } - if e.IndirectExplanation == "" { - e.IndirectExplanation = indirectExplanation - } - e.displayRank += displayRank - // e.Explanations = append(e.Explanations, newExplanations...) - // if e.MatchedOnID != m.Vulnerability.ID || e.MatchedOnNamespace != m.Vulnerability.Namespace { - // // TODO: do something smart. - // panic("matched on different vulnerabilities") - // } - } - } + idsToMatchDetails := groupEvidence(append(b.RelatedMatches, b.PrimaryMatch)) var sortIDs []string for k, v := range idsToMatchDetails { sortIDs = append(sortIDs, k) @@ -336,7 +257,6 @@ func (b *viewModelBuilder) Build() ViewModel { if primaryVulnerability.ID == "" { primaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata } - primaryURL := primaryVulnerability.DataSource // delete the primary vulnerability from the related vulnerabilities delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", primaryVulnerability.Namespace, primaryVulnerability.ID)) @@ -352,8 +272,78 @@ func (b *viewModelBuilder) Build() ViewModel { PrimaryVulnerability: primaryVulnerability, RelatedVulnerabilities: relatedVulnerabilities, MatchedPackages: explainedPackages, - URLs: dedupeURLs(primaryURL, URLs), + URLs: b.dedupeAndSortURLs(primaryVulnerability), + } +} + +func groupEvidence(matches []models.Match) map[string]*explainedPackage { + idsToMatchDetails := make(map[string]*explainedPackage) + for _, m := range matches { + // key := m.Artifact.PURL + key := m.Artifact.ID + // TODO: match details can match multiple packages + var newLocations []explainedEvidence + for _, l := range m.Artifact.Locations { + newLocations = append(newLocations, explainedEvidence{ + Location: l.RealPath, + ArtifactID: m.Artifact.ID, // TODO: this is sometimes blank. Why? + ViaVulnID: m.Vulnerability.ID, + ViaNamespace: m.Vulnerability.Namespace, + }) + } + // TODO: how can match details explain locations? + // Like, I have N matchDetails, and N locations, but I don't know which matchDetail explains which location + var indirectExplanation string + var cpeExplanation string + var displayRank int + for i, md := range m.MatchDetails { + explanation := explainMatchDetail(m, i) + if explanation != "" { + switch md.Type { + case string(match.CPEMatch): + cpeExplanation = explanation + displayRank = 1 + case string(match.ExactIndirectMatch): + indirectExplanation = explanation + displayRank = 0 // display indirect explanations explanations of main matched packages + default: + displayRank = 2 + } + } + } + e, ok := idsToMatchDetails[key] + if !ok { + e = &explainedPackage{ + PURL: m.Artifact.PURL, + Name: m.Artifact.Name, + Version: m.Artifact.Version, + MatchedOnID: m.Vulnerability.ID, + MatchedOnNamespace: m.Vulnerability.Namespace, + IndirectExplanation: indirectExplanation, + CPEExplanation: cpeExplanation, + Locations: newLocations, + displayRank: displayRank, + } + idsToMatchDetails[key] = e + } else { + // TODO: what if MatchedOnID and MatchedOnNamespace are different? + e.Locations = append(e.Locations, newLocations...) + // TODO: why are these checks needed? What if a package is matched by more than one way? + if e.CPEExplanation == "" { + e.CPEExplanation = cpeExplanation + } + if e.IndirectExplanation == "" { + e.IndirectExplanation = indirectExplanation + } + e.displayRank += displayRank + // e.Explanations = append(e.Explanations, newExplanations...) + // if e.MatchedOnID != m.Vulnerability.ID || e.MatchedOnNamespace != m.Vulnerability.Namespace { + // // TODO: do something smart. + // panic("matched on different vulnerabilities") + // } + } } + return idsToMatchDetails } func explainMatchDetail(m models.Match, index int) string { @@ -372,12 +362,29 @@ func explainMatchDetail(m models.Match, index int) string { return explanation } -func dedupeURLs(showFirst string, rest []string) []string { +// dedupeAndSortURLs returns a list of URLs with the datasource of the primary vu8lnerability first, +// followed by data source for related vulnerabilities, followed by other URLs, but with no duplicates. +func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string { + showFirst := primaryVulnerability.DataSource + URLs := b.PrimaryMatch.Vulnerability.URLs + URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) + for _, v := range b.PrimaryMatch.RelatedVulnerabilities { + URLs = append(URLs, v.URLs...) + URLs = append(URLs, v.DataSource) + } + for _, m := range b.RelatedMatches { + URLs = append(URLs, m.Vulnerability.URLs...) + for _, v := range m.RelatedVulnerabilities { + URLs = append(URLs, v.URLs...) + URLs = append(URLs, v.DataSource) + } + } var result []string - result = append(result, showFirst) deduplicate := make(map[string]bool) - for _, u := range rest { - if _, ok := deduplicate[u]; !ok && u != showFirst { + result = append(result, showFirst) + deduplicate[showFirst] = true + for _, u := range URLs { + if _, ok := deduplicate[u]; !ok { result = append(result, u) deduplicate[u] = true } From d2acd519187572e5cde3ee0c141d0cacd9ea282a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Sep 2023 17:08:18 -0400 Subject: [PATCH 34/49] move sorting logic into evidence function Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 70 +++++++++++++++--------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index ceb89919f72..ebaf0270a78 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -201,39 +201,7 @@ func (b *viewModelBuilder) WithRelatedMatch(m models.Match) *viewModelBuilder { } func (b *viewModelBuilder) Build() ViewModel { - idsToMatchDetails := groupEvidence(append(b.RelatedMatches, b.PrimaryMatch)) - var sortIDs []string - for k, v := range idsToMatchDetails { - sortIDs = append(sortIDs, k) - dedupeLocations := make(map[string]explainedEvidence) - for _, l := range v.Locations { - dedupeLocations[l.Location] = l - } - var uniqueLocations []explainedEvidence - for _, l := range dedupeLocations { - uniqueLocations = append(uniqueLocations, l) - } - sort.Slice(uniqueLocations, func(i, j int) bool { - return uniqueLocations[i].Location < uniqueLocations[j].Location - }) - v.Locations = uniqueLocations - } - - sort.Slice(sortIDs, func(i, j int) bool { - iKey := sortIDs[i] - jKey := sortIDs[j] - iMatch := idsToMatchDetails[iKey] - jMatch := idsToMatchDetails[jKey] - // reverse by display rank - if iMatch.displayRank != jMatch.displayRank { - return jMatch.displayRank < iMatch.displayRank - } - return iMatch.Name < jMatch.Name - }) - var explainedPackages []*explainedPackage - for _, k := range sortIDs { - explainedPackages = append(explainedPackages, idsToMatchDetails[k]) - } + explainedPackages := groupAndSortEvidence(append(b.RelatedMatches, b.PrimaryMatch)) // TODO: this isn't right at all. // We need to be able to add related vulnerabilities @@ -276,7 +244,7 @@ func (b *viewModelBuilder) Build() ViewModel { } } -func groupEvidence(matches []models.Match) map[string]*explainedPackage { +func groupAndSortEvidence(matches []models.Match) []*explainedPackage { idsToMatchDetails := make(map[string]*explainedPackage) for _, m := range matches { // key := m.Artifact.PURL @@ -343,7 +311,39 @@ func groupEvidence(matches []models.Match) map[string]*explainedPackage { // } } } - return idsToMatchDetails + var sortIDs []string + for k, v := range idsToMatchDetails { + sortIDs = append(sortIDs, k) + dedupeLocations := make(map[string]explainedEvidence) + for _, l := range v.Locations { + dedupeLocations[l.Location] = l + } + var uniqueLocations []explainedEvidence + for _, l := range dedupeLocations { + uniqueLocations = append(uniqueLocations, l) + } + sort.Slice(uniqueLocations, func(i, j int) bool { + return uniqueLocations[i].Location < uniqueLocations[j].Location + }) + v.Locations = uniqueLocations + } + + sort.Slice(sortIDs, func(i, j int) bool { + iKey := sortIDs[i] + jKey := sortIDs[j] + iMatch := idsToMatchDetails[iKey] + jMatch := idsToMatchDetails[jKey] + // reverse by display rank + if iMatch.displayRank != jMatch.displayRank { + return jMatch.displayRank < iMatch.displayRank + } + return iMatch.Name < jMatch.Name + }) + var explainedPackages []*explainedPackage + for _, k := range sortIDs { + explainedPackages = append(explainedPackages, idsToMatchDetails[k]) + } + return explainedPackages } func explainMatchDetail(m models.Match, index int) string { From 7b2daef4b8ea5096c49c6a610b6e2330f4c6ec41 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 5 Sep 2023 07:00:28 -0400 Subject: [PATCH 35/49] little refactor Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index ebaf0270a78..9e86e6dcd92 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -329,15 +329,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { } sort.Slice(sortIDs, func(i, j int) bool { - iKey := sortIDs[i] - jKey := sortIDs[j] - iMatch := idsToMatchDetails[iKey] - jMatch := idsToMatchDetails[jKey] - // reverse by display rank - if iMatch.displayRank != jMatch.displayRank { - return jMatch.displayRank < iMatch.displayRank - } - return iMatch.Name < jMatch.Name + return explainedPackageIsLess(idsToMatchDetails[sortIDs[i]], idsToMatchDetails[sortIDs[j]]) }) var explainedPackages []*explainedPackage for _, k := range sortIDs { @@ -346,6 +338,13 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { return explainedPackages } +func explainedPackageIsLess(i, j *explainedPackage) bool { + if i.displayRank != j.displayRank { + return i.displayRank < j.displayRank + } + return i.Name < j.Name +} + func explainMatchDetail(m models.Match, index int) string { if len(m.MatchDetails) <= index { return "" From 31154511d2e429aa9ba99db142de846fcc9d219d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 6 Sep 2023 17:28:02 -0400 Subject: [PATCH 36/49] WIP - mostly works, but lots of todos and comments to read Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 60 ++++++++++ grype/presenter/explain/explain.go | 105 ++++++++++++++---- 2 files changed, 146 insertions(+), 19 deletions(-) diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index 656d18bdc8b..ed4b736e20f 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -69,3 +69,63 @@ URLs: - https://github.com/advisories/GHSA-hv5j-3h9f-99c2 --- + +[TestExplainSnapshot/test_a_GHSA - 1] +GHSA-cfh5-3ghh-wfjx from github:language:java (Medium) +Moderate severity vulnerability that affects org.apache.httpcomponents:httpclient +Related vulnerabilities: + - nvd:cpe CVE-2014-3577 (Medium) +Matched packages: + - Package: httpclient, version: 4.1.1 + PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 + Evidenced by: + - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) +URLs: + - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx + - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html + - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html + - http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html + - http://rhn.redhat.com/errata/RHSA-2014-1146.html + - http://rhn.redhat.com/errata/RHSA-2014-1166.html + - http://rhn.redhat.com/errata/RHSA-2014-1833.html + - http://rhn.redhat.com/errata/RHSA-2014-1834.html + - http://rhn.redhat.com/errata/RHSA-2014-1835.html + - http://rhn.redhat.com/errata/RHSA-2014-1836.html + - http://rhn.redhat.com/errata/RHSA-2014-1891.html + - http://rhn.redhat.com/errata/RHSA-2014-1892.html + - http://rhn.redhat.com/errata/RHSA-2015-0125.html + - http://rhn.redhat.com/errata/RHSA-2015-0158.html + - http://rhn.redhat.com/errata/RHSA-2015-0675.html + - http://rhn.redhat.com/errata/RHSA-2015-0720.html + - http://rhn.redhat.com/errata/RHSA-2015-0765.html + - http://rhn.redhat.com/errata/RHSA-2015-0850.html + - http://rhn.redhat.com/errata/RHSA-2015-0851.html + - http://rhn.redhat.com/errata/RHSA-2015-1176.html + - http://rhn.redhat.com/errata/RHSA-2015-1177.html + - http://rhn.redhat.com/errata/RHSA-2015-1888.html + - http://rhn.redhat.com/errata/RHSA-2016-1773.html + - http://rhn.redhat.com/errata/RHSA-2016-1931.html + - http://seclists.org/fulldisclosure/2014/Aug/48 + - http://secunia.com/advisories/60466 + - http://www.openwall.com/lists/oss-security/2021/10/06/1 + - http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html + - http://www.osvdb.org/110143 + - http://www.securityfocus.com/bid/69258 + - http://www.securitytracker.com/id/1030812 + - http://www.ubuntu.com/usn/USN-2769-1 + - https://access.redhat.com/solutions/1165533 + - https://exchange.xforce.ibmcloud.com/vulnerabilities/95327 + - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564 + - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782 + - https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E + - https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E + - https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E + - https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E + - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 + +--- diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 9e86e6dcd92..f4ca9500fc0 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -37,9 +37,11 @@ type ViewModel struct { } type viewModelBuilder struct { + // TODO: this field is unused. Need think a bit more here. PrimaryVulnerability models.Vulnerability // this is the vulnerability we're trying to explain - PrimaryMatch models.Match + PrimaryMatch models.Match // The primary vulnerability RelatedMatches []models.Match + requestedIDs []string // the vulnerability IDs the user requested explanations of } type Findings map[string]ViewModel @@ -119,7 +121,7 @@ func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { key := m.Vulnerability.ID existing, ok := builders[key] if !ok { - existing = newBuilder() + existing = newBuilder(requestedIDs) builders[m.Vulnerability.ID] = existing } existing.WithMatch(m, requestedIDs) @@ -129,9 +131,10 @@ func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { key := related.ID existing, ok := builders[key] if !ok { - existing = newBuilder() + existing = newBuilder(requestedIDs) builders[key] = existing } + // TODO: need to pass in info about the related vulnerability existing.WithMatch(m, requestedIDs) } } @@ -141,8 +144,10 @@ func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { return result, nil } -func newBuilder() *viewModelBuilder { - return &viewModelBuilder{} +func newBuilder(requestedIDs []string) *viewModelBuilder { + return &viewModelBuilder{ + requestedIDs: requestedIDs, + } } // WithMatch adds a match to the builder @@ -162,6 +167,7 @@ func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) } } +// TODO: is this still needed? func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string) bool { // TODO: "primary" is a property of a vulnerability, not a match // if there's not currently any match, make this one primary since we don't know any better @@ -181,7 +187,13 @@ func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs if !idWasRequested && len(userRequestedIDs) > 0 { return false } + // NVD CPEs are somewhat canonical IDs for vulnerabilities, so if the user asked about CVE-YYYY-ID + // type number, and we have a record from NVD, consider that the primary record. + if candidate.Vulnerability.Namespace == "nvd:cpe" { + return true + } // Either the user didn't ask for specific IDs, or the candidate has an ID the user asked for. + // TODO: this is the property for _, related := range b.PrimaryMatch.RelatedVulnerabilities { if related.ID == candidate.Vulnerability.ID { return true @@ -216,18 +228,10 @@ func (b *viewModelBuilder) Build() ViewModel { dedupeRelatedVulnerabilities[key] = r } } - var primaryVulnerability models.VulnerabilityMetadata - for _, r := range dedupeRelatedVulnerabilities { - if r.ID == b.PrimaryMatch.Vulnerability.ID && r.Namespace == "nvd:cpe" { - primaryVulnerability = r - } - } - if primaryVulnerability.ID == "" { - primaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata - } - // delete the primary vulnerability from the related vulnerabilities - delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", primaryVulnerability.Namespace, primaryVulnerability.ID)) + // delete the primary vulnerability from the related vulnerabilities so it isn't listed twice + primary := b.primaryVulnerability() + delete(dedupeRelatedVulnerabilities, fmt.Sprintf("%s:%s", primary.Namespace, primary.ID)) for k := range dedupeRelatedVulnerabilities { sortDedupedRelatedVulnerabilities = append(sortDedupedRelatedVulnerabilities, k) } @@ -237,14 +241,34 @@ func (b *viewModelBuilder) Build() ViewModel { } return ViewModel{ - PrimaryVulnerability: primaryVulnerability, + PrimaryVulnerability: primary, RelatedVulnerabilities: relatedVulnerabilities, MatchedPackages: explainedPackages, - URLs: b.dedupeAndSortURLs(primaryVulnerability), + URLs: b.dedupeAndSortURLs(primary), + } +} + +func (b *viewModelBuilder) primaryVulnerability() models.VulnerabilityMetadata { + var primaryVulnerability models.VulnerabilityMetadata + for _, m := range append(b.RelatedMatches, b.PrimaryMatch) { + for _, r := range append(m.RelatedVulnerabilities, m.Vulnerability.VulnerabilityMetadata) { + if r.ID == b.PrimaryMatch.Vulnerability.ID && r.Namespace == "nvd:cpe" { + primaryVulnerability = r + } + } + } + if primaryVulnerability.ID == "" { + primaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata } + return primaryVulnerability } func groupAndSortEvidence(matches []models.Match) []*explainedPackage { + // These are artifact IDs. + // I think grouping by artifact ID is wrong, but I'm not sure what do to instead. + // Specifically, repeat artifact IDs could be something like the RPM DB + // which is the evidence that a package has been installd pretty often. + // So what should I group on besides artifact ID? idsToMatchDetails := make(map[string]*explainedPackage) for _, m := range matches { // key := m.Artifact.PURL @@ -312,11 +336,42 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { } } var sortIDs []string + // TODO: this loses evidence if the same location is matched in multiple ways + // Extract a method to rotate match details. + // Should be a map of evidence to match details. + // concrete example: + // `cat willtmp/ghsa-check-json.json| go run cmd/grype/main.go explain --id GHSA-cfh5-3ghh-wfjx` + // reports + // - Package: httpclient, version: 4.1.1 + // PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 + // Evidenced by: + // - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) + // but `cat willtmp/ghsa-check-json.json| go run cmd/grype/main.go explain --id CVE-2014-3577` + // reports + /* + Matched packages: + - Package: httpclient, version: 4.1.1 + PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 + CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*` + Evidenced by: + - nvd:cpe:CVE-2014-3577 evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) + */ + // but these are both true; both should be in explain? + /* + ❯ grype anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da | rg -e GHSA-cfh5 -e CVE-2014-3577 + httpclient 4.1.1 4.3.5 java-archive GHSA-cfh5-3ghh-wfjx Medium + httpclient 4.1.1 java-archive CVE-2014-3577 Medium + */ for k, v := range idsToMatchDetails { + // If I deduplicate these, than + // some additional evidence doesn't get reported, + // for example if the same package matched via CPE and via direct match. + // If I _don't_ deduplicate them, they get weird extra stuff in them. sortIDs = append(sortIDs, k) dedupeLocations := make(map[string]explainedEvidence) for _, l := range v.Locations { - dedupeLocations[l.Location] = l + key := fmt.Sprintf("%s:%s:%s:%s:%s", l.ArtifactID, l.Location, l.ViaNamespace, l.ViaVulnID, v.MatchedOnID) + dedupeLocations[key] = l } var uniqueLocations []explainedEvidence for _, l := range dedupeLocations { @@ -365,6 +420,8 @@ func explainMatchDetail(m models.Match, index int) string { // followed by data source for related vulnerabilities, followed by other URLs, but with no duplicates. func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string { showFirst := primaryVulnerability.DataSource + nvdURL := "" + // TODO: totally remove primary match? URLs := b.PrimaryMatch.Vulnerability.URLs URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) for _, v := range b.PrimaryMatch.RelatedVulnerabilities { @@ -382,6 +439,16 @@ func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.Vulnera deduplicate := make(map[string]bool) result = append(result, showFirst) deduplicate[showFirst] = true + for _, u := range URLs { + if strings.HasPrefix(u, "https://nvd.nist.gov/vuln/detail") { + nvdURL = u + } + } + if nvdURL != "" && nvdURL != showFirst { + result = append(result, nvdURL) + deduplicate[nvdURL] = true + } + for _, u := range URLs { if _, ok := deduplicate[u]; !ok { result = append(result, u) From c842674028d9134277bf5421d41ad77a9ad89c56 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 6 Sep 2023 17:29:05 -0400 Subject: [PATCH 37/49] snaps for new behavior Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index ed4b736e20f..977b3c3076a 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -1,3 +1,4 @@ + [TestExplainSnapshot/keycloak-CVE-2020-12413 - 1] CVE-2020-12413 from nvd:cpe (Medium) The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites. @@ -55,6 +56,7 @@ Matched packages: Evidenced by: - wolfi:distro:wolfi:rolling:CVE-2023-28755 evidence at /lib/apk/db/installed (artifact ID: ) - nvd:cpe:CVE-2023-28755 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec (artifact ID: ) + - github:language:ruby:GHSA-hv5j-3h9f-99c2 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec (artifact ID: ) URLs: - https://nvd.nist.gov/vuln/detail/CVE-2023-28755 - https://github.com/ruby/uri/releases/ @@ -82,6 +84,7 @@ Matched packages: - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) URLs: - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx + - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html - http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html @@ -126,6 +129,67 @@ URLs: - https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E - https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E - https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E + +--- + +[TestExplainSnapshot/test_a_CVE_alias_of_a_GHSA - 1] +CVE-2014-3577 from nvd:cpe (Medium) +org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a "CN=" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the "foo,CN=www.apache.org" string in the O field. +Related vulnerabilities: + - github:language:java GHSA-cfh5-3ghh-wfjx (Medium) +Matched packages: + - Package: httpclient, version: 4.1.1 + PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 + CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*` + Evidenced by: + - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) + - nvd:cpe:CVE-2014-3577 evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) +URLs: - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 + - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html + - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html + - http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html + - http://rhn.redhat.com/errata/RHSA-2014-1146.html + - http://rhn.redhat.com/errata/RHSA-2014-1166.html + - http://rhn.redhat.com/errata/RHSA-2014-1833.html + - http://rhn.redhat.com/errata/RHSA-2014-1834.html + - http://rhn.redhat.com/errata/RHSA-2014-1835.html + - http://rhn.redhat.com/errata/RHSA-2014-1836.html + - http://rhn.redhat.com/errata/RHSA-2014-1891.html + - http://rhn.redhat.com/errata/RHSA-2014-1892.html + - http://rhn.redhat.com/errata/RHSA-2015-0125.html + - http://rhn.redhat.com/errata/RHSA-2015-0158.html + - http://rhn.redhat.com/errata/RHSA-2015-0675.html + - http://rhn.redhat.com/errata/RHSA-2015-0720.html + - http://rhn.redhat.com/errata/RHSA-2015-0765.html + - http://rhn.redhat.com/errata/RHSA-2015-0850.html + - http://rhn.redhat.com/errata/RHSA-2015-0851.html + - http://rhn.redhat.com/errata/RHSA-2015-1176.html + - http://rhn.redhat.com/errata/RHSA-2015-1177.html + - http://rhn.redhat.com/errata/RHSA-2015-1888.html + - http://rhn.redhat.com/errata/RHSA-2016-1773.html + - http://rhn.redhat.com/errata/RHSA-2016-1931.html + - http://seclists.org/fulldisclosure/2014/Aug/48 + - http://secunia.com/advisories/60466 + - http://www.openwall.com/lists/oss-security/2021/10/06/1 + - http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html + - http://www.osvdb.org/110143 + - http://www.securityfocus.com/bid/69258 + - http://www.securitytracker.com/id/1030812 + - http://www.ubuntu.com/usn/USN-2769-1 + - https://access.redhat.com/solutions/1165533 + - https://exchange.xforce.ibmcloud.com/vulnerabilities/95327 + - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564 + - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782 + - https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E + - https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E + - https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E + - https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E + - https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E + - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx --- From 715985530304f471c9f96681397299ddda399c7d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 6 Sep 2023 17:58:09 -0400 Subject: [PATCH 38/49] clean up some old comments and todos Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 42 ------------------------------ 1 file changed, 42 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index f4ca9500fc0..35bf615e939 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -264,14 +264,8 @@ func (b *viewModelBuilder) primaryVulnerability() models.VulnerabilityMetadata { } func groupAndSortEvidence(matches []models.Match) []*explainedPackage { - // These are artifact IDs. - // I think grouping by artifact ID is wrong, but I'm not sure what do to instead. - // Specifically, repeat artifact IDs could be something like the RPM DB - // which is the evidence that a package has been installd pretty often. - // So what should I group on besides artifact ID? idsToMatchDetails := make(map[string]*explainedPackage) for _, m := range matches { - // key := m.Artifact.PURL key := m.Artifact.ID // TODO: match details can match multiple packages var newLocations []explainedEvidence @@ -328,45 +322,10 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { e.IndirectExplanation = indirectExplanation } e.displayRank += displayRank - // e.Explanations = append(e.Explanations, newExplanations...) - // if e.MatchedOnID != m.Vulnerability.ID || e.MatchedOnNamespace != m.Vulnerability.Namespace { - // // TODO: do something smart. - // panic("matched on different vulnerabilities") - // } } } var sortIDs []string - // TODO: this loses evidence if the same location is matched in multiple ways - // Extract a method to rotate match details. - // Should be a map of evidence to match details. - // concrete example: - // `cat willtmp/ghsa-check-json.json| go run cmd/grype/main.go explain --id GHSA-cfh5-3ghh-wfjx` - // reports - // - Package: httpclient, version: 4.1.1 - // PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 - // Evidenced by: - // - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) - // but `cat willtmp/ghsa-check-json.json| go run cmd/grype/main.go explain --id CVE-2014-3577` - // reports - /* - Matched packages: - - Package: httpclient, version: 4.1.1 - PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 - CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*` - Evidenced by: - - nvd:cpe:CVE-2014-3577 evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) - */ - // but these are both true; both should be in explain? - /* - ❯ grype anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da | rg -e GHSA-cfh5 -e CVE-2014-3577 - httpclient 4.1.1 4.3.5 java-archive GHSA-cfh5-3ghh-wfjx Medium - httpclient 4.1.1 java-archive CVE-2014-3577 Medium - */ for k, v := range idsToMatchDetails { - // If I deduplicate these, than - // some additional evidence doesn't get reported, - // for example if the same package matched via CPE and via direct match. - // If I _don't_ deduplicate them, they get weird extra stuff in them. sortIDs = append(sortIDs, k) dedupeLocations := make(map[string]explainedEvidence) for _, l := range v.Locations { @@ -421,7 +380,6 @@ func explainMatchDetail(m models.Match, index int) string { func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string { showFirst := primaryVulnerability.DataSource nvdURL := "" - // TODO: totally remove primary match? URLs := b.PrimaryMatch.Vulnerability.URLs URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) for _, v := range b.PrimaryMatch.RelatedVulnerabilities { From 4c4663a01f677483a3f456e24d902a69449127e5 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 6 Sep 2023 18:08:47 -0400 Subject: [PATCH 39/49] stable sort of evidence Signed-off-by: Will Murphy --- .../explain/__snapshots__/explain_snapshot_test.snap | 6 +++--- grype/presenter/explain/explain.go | 6 +++++- grype/presenter/explain/explain_cve_new.tmpl | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index 977b3c3076a..37509fb3483 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -54,9 +54,9 @@ Matched packages: CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*` Note: This CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package. Evidenced by: - - wolfi:distro:wolfi:rolling:CVE-2023-28755 evidence at /lib/apk/db/installed (artifact ID: ) - - nvd:cpe:CVE-2023-28755 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec (artifact ID: ) - - github:language:ruby:GHSA-hv5j-3h9f-99c2 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec (artifact ID: ) + - github:language:ruby:GHSA-hv5j-3h9f-99c2 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec + - nvd:cpe:CVE-2023-28755 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec + - wolfi:distro:wolfi:rolling:CVE-2023-28755 evidence at /lib/apk/db/installed URLs: - https://nvd.nist.gov/vuln/detail/CVE-2023-28755 - https://github.com/ruby/uri/releases/ diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 35bf615e939..206377603f6 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -325,6 +325,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { } } var sortIDs []string + // TODO: why are artifact IDs sometimes blank? for k, v := range idsToMatchDetails { sortIDs = append(sortIDs, k) dedupeLocations := make(map[string]explainedEvidence) @@ -337,7 +338,10 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { uniqueLocations = append(uniqueLocations, l) } sort.Slice(uniqueLocations, func(i, j int) bool { - return uniqueLocations[i].Location < uniqueLocations[j].Location + if uniqueLocations[i].ViaNamespace == uniqueLocations[j].ViaNamespace { + return uniqueLocations[i].Location < uniqueLocations[j].Location + } + return uniqueLocations[i].ViaNamespace < uniqueLocations[j].ViaNamespace }) v.Locations = uniqueLocations } diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl index 004e6d3f449..7b77324748d 100644 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ b/grype/presenter/explain/explain_cve_new.tmpl @@ -8,6 +8,6 @@ Matched packages:{{ range .MatchedPackages }} {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} {{ .IndirectExplanation }}{{ end }} Evidenced by:{{ range .Locations }} - - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }} (artifact ID: {{.ArtifactID}}){{ end }}{{ end }} + - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }}{{ if .ArtifactID }} (artifact ID: {{.ArtifactID}}){{ end }}{{ end }}{{ end }} URLs:{{ range .URLs }} - {{ . }}{{ end }} From 0949a6d321546c30ad6ac3945ee6eb9257109f4d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 04:36:52 -0400 Subject: [PATCH 40/49] add snap tests for GHSAs Signed-off-by: Will Murphy --- grype/presenter/explain/explain_snapshot_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/grype/presenter/explain/explain_snapshot_test.go b/grype/presenter/explain/explain_snapshot_test.go index 7834c3588f5..7f164d2bebe 100644 --- a/grype/presenter/explain/explain_snapshot_test.go +++ b/grype/presenter/explain/explain_snapshot_test.go @@ -30,6 +30,16 @@ func TestExplainSnapshot(t *testing.T) { fixture: "test-fixtures/chainguard-ruby-test.json", vulnerabilityIDs: []string{"CVE-2023-28755"}, }, + { + name: "test a GHSA", + fixture: "test-fixtures/ghsa-test.json", + vulnerabilityIDs: []string{"GHSA-cfh5-3ghh-wfjx"}, + }, + { + name: "test a CVE alias of a GHSA", + fixture: "test-fixtures/ghsa-test.json", + vulnerabilityIDs: []string{"CVE-2014-3577"}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { From 4bd3106976cfb6fdb520a17dd3fe58e03f430b6c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 11:22:30 -0400 Subject: [PATCH 41/49] WIP: start re-arranging Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 20 ++++---- grype/presenter/explain/explain.go | 51 +++++++++++++++---- grype/presenter/explain/explain_cve_new.tmpl | 3 +- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index 37509fb3483..641e66cc3a1 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -134,18 +134,19 @@ URLs: [TestExplainSnapshot/test_a_CVE_alias_of_a_GHSA - 1] CVE-2014-3577 from nvd:cpe (Medium) -org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a "CN=" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the "foo,CN=www.apache.org" string in the O field. -Related vulnerabilities: - - github:language:java GHSA-cfh5-3ghh-wfjx (Medium) +Aliases: + - GHSA-cfh5-3ghh-wfjx from github:language:java (Medium) Matched packages: - - Package: httpclient, version: 4.1.1 - PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 - CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*` - Evidenced by: - - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) - - nvd:cpe:CVE-2014-3577 evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) + - httpclient 4.1.1 (PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1, artifact ID: f09cdae46b001bc5) + Locations and evidence: + - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:org.apache.httpcomponents:httpclient + - github:language:java:GHSA-cfh5-3ghh-wfjx matched directly by package name, version, and ecosystem + - nvd:cpe:CVE-2014-3577 matched on CPE `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*` +Description: + org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a "CN=" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the "foo,CN=www.apache.org" string in the O field. URLs: - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 + - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html - http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html @@ -190,6 +191,5 @@ URLs: - https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E - https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E - https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E - - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx --- diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 206377603f6..cae844a2fb5 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -9,7 +9,9 @@ import ( "text/template" "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/presenter/models" + "github.com/anchore/syft/syft/file" ) //go:embed explain_cve_new.tmpl @@ -32,7 +34,7 @@ type VulnerabilityExplainer interface { type ViewModel struct { PrimaryVulnerability models.VulnerabilityMetadata RelatedVulnerabilities []models.VulnerabilityMetadata - MatchedPackages []*explainedPackage + MatchedPackages []*explainedPackage // I think this needs a map of artifacts to explained evidence URLs []string } @@ -46,6 +48,8 @@ type viewModelBuilder struct { type Findings map[string]ViewModel +// It looks like an explained package +// should really be a single location and a slice of evidence? type explainedPackage struct { PURL string Name string @@ -53,6 +57,7 @@ type explainedPackage struct { MatchedOnID string MatchedOnNamespace string IndirectExplanation string + DirectExplanation string CPEExplanation string Locations []explainedEvidence displayRank int // how early in output should this appear? @@ -264,21 +269,26 @@ func (b *viewModelBuilder) primaryVulnerability() models.VulnerabilityMetadata { } func groupAndSortEvidence(matches []models.Match) []*explainedPackage { + // TODO: group by PURL, then by artifact ID, then explain each match type on the + // artifact ID. + // question: does the type have the right shape for this? + // question: does the proposed explanation help folks remediate? + /* + Proposed return type would be + map[PURL]map[location][]evidence + except we want deterministict traversal, so we'll use sorted maps. + */ idsToMatchDetails := make(map[string]*explainedPackage) for _, m := range matches { key := m.Artifact.ID // TODO: match details can match multiple packages var newLocations []explainedEvidence for _, l := range m.Artifact.Locations { - newLocations = append(newLocations, explainedEvidence{ - Location: l.RealPath, - ArtifactID: m.Artifact.ID, // TODO: this is sometimes blank. Why? - ViaVulnID: m.Vulnerability.ID, - ViaNamespace: m.Vulnerability.Namespace, - }) + newLocations = append(newLocations, explainLocation(m, l)) } // TODO: how can match details explain locations? // Like, I have N matchDetails, and N locations, but I don't know which matchDetail explains which location + var directExplanation string var indirectExplanation string var cpeExplanation string var displayRank int @@ -292,7 +302,8 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { case string(match.ExactIndirectMatch): indirectExplanation = explanation displayRank = 0 // display indirect explanations explanations of main matched packages - default: + case string(match.ExactDirectMatch): + directExplanation = explanation displayRank = 2 } } @@ -305,6 +316,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { Version: m.Artifact.Version, MatchedOnID: m.Vulnerability.ID, MatchedOnNamespace: m.Vulnerability.Namespace, + DirectExplanation: directExplanation, IndirectExplanation: indirectExplanation, CPEExplanation: cpeExplanation, Locations: newLocations, @@ -375,6 +387,8 @@ func explainMatchDetail(m models.Match, index int) string { case string(match.ExactIndirectMatch): sourceName, sourceVersion := sourcePackageNameAndVersion(md) explanation = fmt.Sprintf("Note: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) + case string(match.ExactDirectMatch): + explanation = fmt.Sprintf("Direct match against %s (version %s).", m.Artifact.Name, m.Artifact.Version) } return explanation } @@ -420,13 +434,32 @@ func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.Vulnera return result } +func explainLocation(match models.Match, location file.Coordinates) explainedEvidence { + path := location.RealPath + // TODO: try casting metadata as java metadata + switch match.Artifact.MetadataType { + case pkg.JavaMetadataType: + if javaMeta, ok := match.Artifact.Metadata.(map[string]any); ok { + if virtPath, ok := javaMeta["virtualPath"].(string); ok { + path = virtPath + } + } + } + return explainedEvidence{ + Location: path, + ArtifactID: match.Artifact.ID, + ViaVulnID: match.Vulnerability.ID, + ViaNamespace: match.Vulnerability.Namespace, + } +} + func formatCPEExplanation(m models.Match) string { searchedBy := m.MatchDetails[0].SearchedBy if mapResult, ok := searchedBy.(map[string]interface{}); ok { if cpes, ok := mapResult["cpes"]; ok { if cpeSlice, ok := cpes.([]interface{}); ok { if len(cpeSlice) > 0 { - return fmt.Sprintf("CPE match on `%s`", cpeSlice[0]) + return fmt.Sprintf("CPE match on `%s`.", cpeSlice[0]) } } } diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl index 7b77324748d..2ab47bdd8d3 100644 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ b/grype/presenter/explain/explain_cve_new.tmpl @@ -4,7 +4,8 @@ Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} Matched packages:{{ range .MatchedPackages }} - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} - PURL: {{ .PURL }}{{ end }}{{ if .CPEExplanation }} + PURL: {{ .PURL }}{{ end }}{{ if .DirectExplanation }} + {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }} {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} {{ .IndirectExplanation }}{{ end }} Evidenced by:{{ range .Locations }} From a9778c9dbd5d50135bc9a195c67e7318727462f5 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 12:55:31 -0400 Subject: [PATCH 42/49] fix sorting: GHSA URLs earlier, exact direct matches earlier Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 36 ++++++++++--------- grype/presenter/explain/explain.go | 12 +++++-- .../explain/explain_snapshot_test.go | 10 ++++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index 641e66cc3a1..2ad365a7c7a 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -5,15 +5,16 @@ The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS spe Related vulnerabilities: - redhat:distro:redhat:9 CVE-2020-12413 (Low) Matched packages: + - Package: nss, version: 3.79.0-17.el9_1 + PURL: pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 + Direct match against nss (version 3.79.0-17.el9_1). + Evidenced by: + - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 840f8a931c86688f) - Package: nspr, version: 4.34.0-17.el9_1 PURL: pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. Evidenced by: - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: ff2aefb138ebd4bf) - - Package: nss, version: 3.79.0-17.el9_1 - PURL: pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 - Evidenced by: - - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 840f8a931c86688f) - Package: nss-softokn, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. @@ -51,7 +52,8 @@ Related vulnerabilities: Matched packages: - Package: ruby-3.0, version: 3.0.4-r1 PURL: pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118 - CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*` + Direct match against ruby-3.0 (version 3.0.4-r1). + CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*`. Note: This CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package. Evidenced by: - github:language:ruby:GHSA-hv5j-3h9f-99c2 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec @@ -59,6 +61,7 @@ Matched packages: - wolfi:distro:wolfi:rolling:CVE-2023-28755 evidence at /lib/apk/db/installed URLs: - https://nvd.nist.gov/vuln/detail/CVE-2023-28755 + - https://github.com/advisories/GHSA-hv5j-3h9f-99c2 - https://github.com/ruby/uri/releases/ - https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/ @@ -68,7 +71,6 @@ URLs: - https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/ - https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/ - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755 - - https://github.com/advisories/GHSA-hv5j-3h9f-99c2 --- @@ -80,8 +82,9 @@ Related vulnerabilities: Matched packages: - Package: httpclient, version: 4.1.1 PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 + Direct match against httpclient (version 4.1.1). Evidenced by: - - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi (artifact ID: f09cdae46b001bc5) + - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient (artifact ID: f09cdae46b001bc5) URLs: - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 @@ -134,16 +137,17 @@ URLs: [TestExplainSnapshot/test_a_CVE_alias_of_a_GHSA - 1] CVE-2014-3577 from nvd:cpe (Medium) -Aliases: - - GHSA-cfh5-3ghh-wfjx from github:language:java (Medium) +org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a "CN=" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the "foo,CN=www.apache.org" string in the O field. +Related vulnerabilities: + - github:language:java GHSA-cfh5-3ghh-wfjx (Medium) Matched packages: - - httpclient 4.1.1 (PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1, artifact ID: f09cdae46b001bc5) - Locations and evidence: - - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:org.apache.httpcomponents:httpclient - - github:language:java:GHSA-cfh5-3ghh-wfjx matched directly by package name, version, and ecosystem - - nvd:cpe:CVE-2014-3577 matched on CPE `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*` -Description: - org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a "CN=" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the "foo,CN=www.apache.org" string in the O field. + - Package: httpclient, version: 4.1.1 + PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 + Direct match against httpclient (version 4.1.1). + CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*`. + Evidenced by: + - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient (artifact ID: f09cdae46b001bc5) + - nvd:cpe:CVE-2014-3577 evidence at /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient (artifact ID: f09cdae46b001bc5) URLs: - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index cae844a2fb5..6536c4f261e 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -370,7 +370,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { func explainedPackageIsLess(i, j *explainedPackage) bool { if i.displayRank != j.displayRank { - return i.displayRank < j.displayRank + return i.displayRank > j.displayRank } return i.Name < j.Name } @@ -397,7 +397,6 @@ func explainMatchDetail(m models.Match, index int) string { // followed by data source for related vulnerabilities, followed by other URLs, but with no duplicates. func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string { showFirst := primaryVulnerability.DataSource - nvdURL := "" URLs := b.PrimaryMatch.Vulnerability.URLs URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) for _, v := range b.PrimaryMatch.RelatedVulnerabilities { @@ -415,15 +414,24 @@ func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.Vulnera deduplicate := make(map[string]bool) result = append(result, showFirst) deduplicate[showFirst] = true + nvdURL := "" + ghsaURL := "" for _, u := range URLs { if strings.HasPrefix(u, "https://nvd.nist.gov/vuln/detail") { nvdURL = u } + if strings.HasPrefix(u, "https://github.com/advisories") { + ghsaURL = u + } } if nvdURL != "" && nvdURL != showFirst { result = append(result, nvdURL) deduplicate[nvdURL] = true } + if ghsaURL != "" && ghsaURL != showFirst { + result = append(result, ghsaURL) + deduplicate[ghsaURL] = true + } for _, u := range URLs { if _, ok := deduplicate[u]; !ok { diff --git a/grype/presenter/explain/explain_snapshot_test.go b/grype/presenter/explain/explain_snapshot_test.go index 7f164d2bebe..4bbdd4cb211 100644 --- a/grype/presenter/explain/explain_snapshot_test.go +++ b/grype/presenter/explain/explain_snapshot_test.go @@ -31,7 +31,14 @@ func TestExplainSnapshot(t *testing.T) { vulnerabilityIDs: []string{"CVE-2023-28755"}, }, { - name: "test a GHSA", + name: "test a GHSA", + /* + fixture created by: + Saving output of + grype anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da -o json + Then filtering matches to relevant ones: + jq -c '.matches[]' | rg -e GHSA-cfh5-3ghh-wfjx -e CVE-2014-3577 | jq -s . + */ fixture: "test-fixtures/ghsa-test.json", vulnerabilityIDs: []string{"GHSA-cfh5-3ghh-wfjx"}, }, @@ -61,5 +68,4 @@ func TestExplainSnapshot(t *testing.T) { snaps.MatchSnapshot(t, w.String()) }) } - } From feb6776d7ed4c9efe2f83cb5742254f59a5c74ab Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 14:41:01 -0400 Subject: [PATCH 43/49] looking nice Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 41 +++++++++----------- grype/presenter/explain/explain_cve_new.tmpl | 13 ++++--- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 6536c4f261e..08ed5e38f85 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -60,7 +60,7 @@ type explainedPackage struct { DirectExplanation string CPEExplanation string Locations []explainedEvidence - displayRank int // how early in output should this appear? + displayPriority int // how early in output should this appear? } type explainedEvidence struct { @@ -291,20 +291,20 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { var directExplanation string var indirectExplanation string var cpeExplanation string - var displayRank int + var matchTypePriority int for i, md := range m.MatchDetails { explanation := explainMatchDetail(m, i) if explanation != "" { switch md.Type { case string(match.CPEMatch): - cpeExplanation = explanation - displayRank = 1 + cpeExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) + matchTypePriority = 1 case string(match.ExactIndirectMatch): - indirectExplanation = explanation - displayRank = 0 // display indirect explanations explanations of main matched packages + indirectExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) + matchTypePriority = 0 // display indirect explanations explanations of main matched packages case string(match.ExactDirectMatch): - directExplanation = explanation - displayRank = 2 + directExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) + matchTypePriority = 2 } } } @@ -320,7 +320,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { IndirectExplanation: indirectExplanation, CPEExplanation: cpeExplanation, Locations: newLocations, - displayRank: displayRank, + displayPriority: matchTypePriority, } idsToMatchDetails[key] = e } else { @@ -333,7 +333,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { if e.IndirectExplanation == "" { e.IndirectExplanation = indirectExplanation } - e.displayRank += displayRank + e.displayPriority += matchTypePriority } } var sortIDs []string @@ -342,8 +342,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { sortIDs = append(sortIDs, k) dedupeLocations := make(map[string]explainedEvidence) for _, l := range v.Locations { - key := fmt.Sprintf("%s:%s:%s:%s:%s", l.ArtifactID, l.Location, l.ViaNamespace, l.ViaVulnID, v.MatchedOnID) - dedupeLocations[key] = l + dedupeLocations[l.Location] = l } var uniqueLocations []explainedEvidence for _, l := range dedupeLocations { @@ -369,8 +368,8 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { } func explainedPackageIsLess(i, j *explainedPackage) bool { - if i.displayRank != j.displayRank { - return i.displayRank > j.displayRank + if i.displayPriority != j.displayPriority { + return i.displayPriority > j.displayPriority } return i.Name < j.Name } @@ -386,9 +385,9 @@ func explainMatchDetail(m models.Match, index int) string { explanation = formatCPEExplanation(m) case string(match.ExactIndirectMatch): sourceName, sourceVersion := sourcePackageNameAndVersion(md) - explanation = fmt.Sprintf("Note: This CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) + explanation = fmt.Sprintf("Indirect match; this CVE is reported against %s (version %s), the %s of this %s package.", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type) case string(match.ExactDirectMatch): - explanation = fmt.Sprintf("Direct match against %s (version %s).", m.Artifact.Name, m.Artifact.Version) + explanation = fmt.Sprintf("Direct match (package name, version, and ecosystem) against %s (version %s).", m.Artifact.Name, m.Artifact.Version) } return explanation } @@ -397,16 +396,14 @@ func explainMatchDetail(m models.Match, index int) string { // followed by data source for related vulnerabilities, followed by other URLs, but with no duplicates. func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string { showFirst := primaryVulnerability.DataSource - URLs := b.PrimaryMatch.Vulnerability.URLs + var URLs []string URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) for _, v := range b.PrimaryMatch.RelatedVulnerabilities { - URLs = append(URLs, v.URLs...) URLs = append(URLs, v.DataSource) } for _, m := range b.RelatedMatches { - URLs = append(URLs, m.Vulnerability.URLs...) + URLs = append(URLs, m.Vulnerability.DataSource) for _, v := range m.RelatedVulnerabilities { - URLs = append(URLs, v.URLs...) URLs = append(URLs, v.DataSource) } } @@ -444,9 +441,7 @@ func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.Vulnera func explainLocation(match models.Match, location file.Coordinates) explainedEvidence { path := location.RealPath - // TODO: try casting metadata as java metadata - switch match.Artifact.MetadataType { - case pkg.JavaMetadataType: + if match.Artifact.MetadataType == pkg.JavaMetadataType { if javaMeta, ok := match.Artifact.Metadata.(map[string]any); ok { if virtPath, ok := javaMeta["virtualPath"].(string); ok { path = virtPath diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl index 2ab47bdd8d3..952b315350c 100644 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ b/grype/presenter/explain/explain_cve_new.tmpl @@ -4,11 +4,12 @@ Related vulnerabilities:{{ range .RelatedVulnerabilities }} - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} Matched packages:{{ range .MatchedPackages }} - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} - PURL: {{ .PURL }}{{ end }}{{ if .DirectExplanation }} - {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }} - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} - {{ .IndirectExplanation }}{{ end }} - Evidenced by:{{ range .Locations }} - - {{ .ViaNamespace }}:{{ .ViaVulnID }} evidence at {{ .Location }}{{ if .ArtifactID }} (artifact ID: {{.ArtifactID}}){{ end }}{{ end }}{{ end }} + PURL: {{ .PURL }}{{ end }} + Match explanation(s):{{ if .DirectExplanation }} + - {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }} + - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} + - {{ .IndirectExplanation }}{{ end }} + Locations:{{ range .Locations }} + - {{ .Location }}{{ end }}{{ end }} URLs:{{ range .URLs }} - {{ . }}{{ end }} From 978ab05699440aa160989cc0a07ee60697ff65e4 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 14:43:18 -0400 Subject: [PATCH 44/49] update snaps Signed-off-by: Will Murphy --- .../__snapshots__/explain_snapshot_test.snap | 171 ++++-------------- 1 file changed, 40 insertions(+), 131 deletions(-) diff --git a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap index 2ad365a7c7a..0d151ca8f8c 100755 --- a/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap +++ b/grype/presenter/explain/__snapshots__/explain_snapshot_test.snap @@ -7,39 +7,43 @@ Related vulnerabilities: Matched packages: - Package: nss, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 - Direct match against nss (version 3.79.0-17.el9_1). - Evidenced by: - - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 840f8a931c86688f) + Match explanation(s): + - redhat:distro:redhat:9:CVE-2020-12413 Direct match (package name, version, and ecosystem) against nss (version 3.79.0-17.el9_1). + Locations: + - /var/lib/rpm/rpmdb.sqlite - Package: nspr, version: 4.34.0-17.el9_1 PURL: pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 - Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. - Evidenced by: - - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: ff2aefb138ebd4bf) + Match explanation(s): + - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Locations: + - /var/lib/rpm/rpmdb.sqlite - Package: nss-softokn, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 - Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. - Evidenced by: - - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 7d1c659d9eb00024) + Match explanation(s): + - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Locations: + - /var/lib/rpm/rpmdb.sqlite - Package: nss-softokn-freebl, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 - Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. - Evidenced by: - - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: cb1f96627e29924e) + Match explanation(s): + - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Locations: + - /var/lib/rpm/rpmdb.sqlite - Package: nss-sysinit, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 - Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. - Evidenced by: - - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: d096d490e4fccf36) + Match explanation(s): + - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Locations: + - /var/lib/rpm/rpmdb.sqlite - Package: nss-util, version: 3.79.0-17.el9_1 PURL: pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1 - Note: This CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. - Evidenced by: - - redhat:distro:redhat:9:CVE-2020-12413 evidence at /var/lib/rpm/rpmdb.sqlite (artifact ID: 641950c22b3f5035) + Match explanation(s): + - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package. + Locations: + - /var/lib/rpm/rpmdb.sqlite URLs: - https://nvd.nist.gov/vuln/detail/CVE-2020-12413 - https://access.redhat.com/security/cve/CVE-2020-12413 - - https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413 - - https://raccoon-attack.com/ --- @@ -52,24 +56,16 @@ Related vulnerabilities: Matched packages: - Package: ruby-3.0, version: 3.0.4-r1 PURL: pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118 - Direct match against ruby-3.0 (version 3.0.4-r1). - CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*`. - Note: This CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package. - Evidenced by: - - github:language:ruby:GHSA-hv5j-3h9f-99c2 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec - - nvd:cpe:CVE-2023-28755 evidence at /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec - - wolfi:distro:wolfi:rolling:CVE-2023-28755 evidence at /lib/apk/db/installed + Match explanation(s): + - wolfi:distro:wolfi:rolling:CVE-2023-28755 Direct match (package name, version, and ecosystem) against ruby-3.0 (version 3.0.4-r1). + - nvd:cpe:CVE-2023-28755 CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*`. + - wolfi:distro:wolfi:rolling:CVE-2023-28755 Indirect match; this CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package. + Locations: + - /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec + - /lib/apk/db/installed URLs: - https://nvd.nist.gov/vuln/detail/CVE-2023-28755 - https://github.com/advisories/GHSA-hv5j-3h9f-99c2 - - https://github.com/ruby/uri/releases/ - - https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html - - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/ - - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/ - - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/ - - https://www.ruby-lang.org/en/downloads/releases/ - - https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/ - - https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/ - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755 --- @@ -82,56 +78,13 @@ Related vulnerabilities: Matched packages: - Package: httpclient, version: 4.1.1 PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 - Direct match against httpclient (version 4.1.1). - Evidenced by: - - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient (artifact ID: f09cdae46b001bc5) + Match explanation(s): + - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1). + Locations: + - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient URLs: - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 - - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html - - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html - - http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html - - http://rhn.redhat.com/errata/RHSA-2014-1146.html - - http://rhn.redhat.com/errata/RHSA-2014-1166.html - - http://rhn.redhat.com/errata/RHSA-2014-1833.html - - http://rhn.redhat.com/errata/RHSA-2014-1834.html - - http://rhn.redhat.com/errata/RHSA-2014-1835.html - - http://rhn.redhat.com/errata/RHSA-2014-1836.html - - http://rhn.redhat.com/errata/RHSA-2014-1891.html - - http://rhn.redhat.com/errata/RHSA-2014-1892.html - - http://rhn.redhat.com/errata/RHSA-2015-0125.html - - http://rhn.redhat.com/errata/RHSA-2015-0158.html - - http://rhn.redhat.com/errata/RHSA-2015-0675.html - - http://rhn.redhat.com/errata/RHSA-2015-0720.html - - http://rhn.redhat.com/errata/RHSA-2015-0765.html - - http://rhn.redhat.com/errata/RHSA-2015-0850.html - - http://rhn.redhat.com/errata/RHSA-2015-0851.html - - http://rhn.redhat.com/errata/RHSA-2015-1176.html - - http://rhn.redhat.com/errata/RHSA-2015-1177.html - - http://rhn.redhat.com/errata/RHSA-2015-1888.html - - http://rhn.redhat.com/errata/RHSA-2016-1773.html - - http://rhn.redhat.com/errata/RHSA-2016-1931.html - - http://seclists.org/fulldisclosure/2014/Aug/48 - - http://secunia.com/advisories/60466 - - http://www.openwall.com/lists/oss-security/2021/10/06/1 - - http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html - - http://www.osvdb.org/110143 - - http://www.securityfocus.com/bid/69258 - - http://www.securitytracker.com/id/1030812 - - http://www.ubuntu.com/usn/USN-2769-1 - - https://access.redhat.com/solutions/1165533 - - https://exchange.xforce.ibmcloud.com/vulnerabilities/95327 - - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564 - - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782 - - https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E - - https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E - - https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E - - https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E --- @@ -143,57 +96,13 @@ Related vulnerabilities: Matched packages: - Package: httpclient, version: 4.1.1 PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1 - Direct match against httpclient (version 4.1.1). - CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*`. - Evidenced by: - - github:language:java:GHSA-cfh5-3ghh-wfjx evidence at /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient (artifact ID: f09cdae46b001bc5) - - nvd:cpe:CVE-2014-3577 evidence at /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient (artifact ID: f09cdae46b001bc5) + Match explanation(s): + - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1). + - nvd:cpe:CVE-2014-3577 CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*`. + Locations: + - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient URLs: - https://nvd.nist.gov/vuln/detail/CVE-2014-3577 - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx - - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html - - http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html - - http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html - - http://rhn.redhat.com/errata/RHSA-2014-1146.html - - http://rhn.redhat.com/errata/RHSA-2014-1166.html - - http://rhn.redhat.com/errata/RHSA-2014-1833.html - - http://rhn.redhat.com/errata/RHSA-2014-1834.html - - http://rhn.redhat.com/errata/RHSA-2014-1835.html - - http://rhn.redhat.com/errata/RHSA-2014-1836.html - - http://rhn.redhat.com/errata/RHSA-2014-1891.html - - http://rhn.redhat.com/errata/RHSA-2014-1892.html - - http://rhn.redhat.com/errata/RHSA-2015-0125.html - - http://rhn.redhat.com/errata/RHSA-2015-0158.html - - http://rhn.redhat.com/errata/RHSA-2015-0675.html - - http://rhn.redhat.com/errata/RHSA-2015-0720.html - - http://rhn.redhat.com/errata/RHSA-2015-0765.html - - http://rhn.redhat.com/errata/RHSA-2015-0850.html - - http://rhn.redhat.com/errata/RHSA-2015-0851.html - - http://rhn.redhat.com/errata/RHSA-2015-1176.html - - http://rhn.redhat.com/errata/RHSA-2015-1177.html - - http://rhn.redhat.com/errata/RHSA-2015-1888.html - - http://rhn.redhat.com/errata/RHSA-2016-1773.html - - http://rhn.redhat.com/errata/RHSA-2016-1931.html - - http://seclists.org/fulldisclosure/2014/Aug/48 - - http://secunia.com/advisories/60466 - - http://www.openwall.com/lists/oss-security/2021/10/06/1 - - http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html - - http://www.osvdb.org/110143 - - http://www.securityfocus.com/bid/69258 - - http://www.securitytracker.com/id/1030812 - - http://www.ubuntu.com/usn/USN-2769-1 - - https://access.redhat.com/solutions/1165533 - - https://exchange.xforce.ibmcloud.com/vulnerabilities/95327 - - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564 - - https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782 - - https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E - - https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E - - https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E - - https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E - - https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E --- From d944b377db776743df1ff8fa7e776b333733df79 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 14:49:02 -0400 Subject: [PATCH 45/49] clean up some comments; suppress one lint Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 31 +++++++++--------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 08ed5e38f85..c227b75c306 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -268,26 +268,15 @@ func (b *viewModelBuilder) primaryVulnerability() models.VulnerabilityMetadata { return primaryVulnerability } +// nolint:funlen func groupAndSortEvidence(matches []models.Match) []*explainedPackage { - // TODO: group by PURL, then by artifact ID, then explain each match type on the - // artifact ID. - // question: does the type have the right shape for this? - // question: does the proposed explanation help folks remediate? - /* - Proposed return type would be - map[PURL]map[location][]evidence - except we want deterministict traversal, so we'll use sorted maps. - */ idsToMatchDetails := make(map[string]*explainedPackage) for _, m := range matches { key := m.Artifact.ID - // TODO: match details can match multiple packages var newLocations []explainedEvidence for _, l := range m.Artifact.Locations { newLocations = append(newLocations, explainLocation(m, l)) } - // TODO: how can match details explain locations? - // Like, I have N matchDetails, and N locations, but I don't know which matchDetail explains which location var directExplanation string var indirectExplanation string var cpeExplanation string @@ -297,14 +286,14 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { if explanation != "" { switch md.Type { case string(match.CPEMatch): - cpeExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) - matchTypePriority = 1 + cpeExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) + matchTypePriority = 1 // cpes are a type of direct match case string(match.ExactIndirectMatch): indirectExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) - matchTypePriority = 0 // display indirect explanations explanations of main matched packages + matchTypePriority = 0 // display indirect matches after direct matches case string(match.ExactDirectMatch): directExplanation = fmt.Sprintf("%s:%s %s", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation) - matchTypePriority = 2 + matchTypePriority = 2 // exact-direct-matches are high confidence, direct matches; display them first. } } } @@ -324,9 +313,7 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { } idsToMatchDetails[key] = e } else { - // TODO: what if MatchedOnID and MatchedOnNamespace are different? e.Locations = append(e.Locations, newLocations...) - // TODO: why are these checks needed? What if a package is matched by more than one way? if e.CPEExplanation == "" { e.CPEExplanation = cpeExplanation } @@ -337,7 +324,6 @@ func groupAndSortEvidence(matches []models.Match) []*explainedPackage { } } var sortIDs []string - // TODO: why are artifact IDs sometimes blank? for k, v := range idsToMatchDetails { sortIDs = append(sortIDs, k) dedupeLocations := make(map[string]explainedEvidence) @@ -392,11 +378,12 @@ func explainMatchDetail(m models.Match, index int) string { return explanation } -// dedupeAndSortURLs returns a list of URLs with the datasource of the primary vu8lnerability first, -// followed by data source for related vulnerabilities, followed by other URLs, but with no duplicates. +// dedupeAndSortURLs returns a slice of the DataSource fields, deduplicated and sorted +// the NVD and GHSA URL are given special treatment; they return first and second if present +// and the rest are sorted by string sort. func (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string { showFirst := primaryVulnerability.DataSource - var URLs []string + var URLs []string URLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource) for _, v := range b.PrimaryMatch.RelatedVulnerabilities { URLs = append(URLs, v.DataSource) From d2f0fbd77df1e92a03431a5b0e6399cd9add8a1b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 14:52:02 -0400 Subject: [PATCH 46/49] remove unused template Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 2 +- grype/presenter/explain/explain_cve.tmpl | 27 +++++++++++--------- grype/presenter/explain/explain_cve_new.tmpl | 15 ----------- 3 files changed, 16 insertions(+), 28 deletions(-) delete mode 100644 grype/presenter/explain/explain_cve_new.tmpl diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index c227b75c306..9d4ad28f220 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -14,7 +14,7 @@ import ( "github.com/anchore/syft/syft/file" ) -//go:embed explain_cve_new.tmpl +//go:embed explain_cve.tmpl var explainTemplate string type VulnerabilityExplainer interface { diff --git a/grype/presenter/explain/explain_cve.tmpl b/grype/presenter/explain/explain_cve.tmpl index 1dcf00c862e..952b315350c 100644 --- a/grype/presenter/explain/explain_cve.tmpl +++ b/grype/presenter/explain/explain_cve.tmpl @@ -1,12 +1,15 @@ -{{ .VulnerabilityID }} ({{ .Severity }}) for namespace "{{ .Namespace }}": -{{ .Description }} - -URLs: {{ range .URLs }} - - {{ . }}{{ end }}{{ if .VersionConstraint }} -Versions affected: {{ .VersionConstraint }}{{ end }} - Matched packages: {{ range .MatchedPackages }} - Package: {{ .Name }}, version: {{ .Version }} - PURL: {{ .PURL }}{{if .Explanation }} - {{ .Explanation }}{{end}} - Evidenced by: {{ range .Locations }} - - (artifact ID: {{.ArtifactId}}): {{ .Location }}{{ if .ViaVulnID }}(via {{ .ViaVulnID }}){{ end }}{{ end }}{{ end }} +{{ .PrimaryVulnerability.ID }} from {{ .PrimaryVulnerability.Namespace }} ({{ .PrimaryVulnerability.Severity }}) +{{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} +Related vulnerabilities:{{ range .RelatedVulnerabilities }} + - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} +Matched packages:{{ range .MatchedPackages }} + - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} + PURL: {{ .PURL }}{{ end }} + Match explanation(s):{{ if .DirectExplanation }} + - {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }} + - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} + - {{ .IndirectExplanation }}{{ end }} + Locations:{{ range .Locations }} + - {{ .Location }}{{ end }}{{ end }} +URLs:{{ range .URLs }} + - {{ . }}{{ end }} diff --git a/grype/presenter/explain/explain_cve_new.tmpl b/grype/presenter/explain/explain_cve_new.tmpl deleted file mode 100644 index 952b315350c..00000000000 --- a/grype/presenter/explain/explain_cve_new.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -{{ .PrimaryVulnerability.ID }} from {{ .PrimaryVulnerability.Namespace }} ({{ .PrimaryVulnerability.Severity }}) -{{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }} -Related vulnerabilities:{{ range .RelatedVulnerabilities }} - - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}} -Matched packages:{{ range .MatchedPackages }} - - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }} - PURL: {{ .PURL }}{{ end }} - Match explanation(s):{{ if .DirectExplanation }} - - {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }} - - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }} - - {{ .IndirectExplanation }}{{ end }} - Locations:{{ range .Locations }} - - {{ .Location }}{{ end }}{{ end }} -URLs:{{ range .URLs }} - - {{ . }}{{ end }} From 8d07aa06101d3c001091fc8cf9635cd51c705680 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 7 Sep 2023 15:11:06 -0400 Subject: [PATCH 47/49] track test fixture Signed-off-by: Will Murphy --- .../explain/test-fixtures/ghsa-test.json | 396 ++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 grype/presenter/explain/test-fixtures/ghsa-test.json diff --git a/grype/presenter/explain/test-fixtures/ghsa-test.json b/grype/presenter/explain/test-fixtures/ghsa-test.json new file mode 100644 index 00000000000..661b78b0bab --- /dev/null +++ b/grype/presenter/explain/test-fixtures/ghsa-test.json @@ -0,0 +1,396 @@ +{ + "matches": [ + { + "vulnerability": { + "id": "GHSA-cfh5-3ghh-wfjx", + "dataSource": "https://github.com/advisories/GHSA-cfh5-3ghh-wfjx", + "namespace": "github:language:java", + "severity": "Medium", + "urls": [ + "https://github.com/advisories/GHSA-cfh5-3ghh-wfjx" + ], + "description": "Moderate severity vulnerability that affects org.apache.httpcomponents:httpclient", + "cvss": [], + "fix": { + "versions": [ + "4.3.5" + ], + "state": "fixed" + }, + "advisories": [] + }, + "relatedVulnerabilities": [ + { + "id": "CVE-2014-3577", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2014-3577", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html", + "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html", + "http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html", + "http://rhn.redhat.com/errata/RHSA-2014-1146.html", + "http://rhn.redhat.com/errata/RHSA-2014-1166.html", + "http://rhn.redhat.com/errata/RHSA-2014-1833.html", + "http://rhn.redhat.com/errata/RHSA-2014-1834.html", + "http://rhn.redhat.com/errata/RHSA-2014-1835.html", + "http://rhn.redhat.com/errata/RHSA-2014-1836.html", + "http://rhn.redhat.com/errata/RHSA-2014-1891.html", + "http://rhn.redhat.com/errata/RHSA-2014-1892.html", + "http://rhn.redhat.com/errata/RHSA-2015-0125.html", + "http://rhn.redhat.com/errata/RHSA-2015-0158.html", + "http://rhn.redhat.com/errata/RHSA-2015-0675.html", + "http://rhn.redhat.com/errata/RHSA-2015-0720.html", + "http://rhn.redhat.com/errata/RHSA-2015-0765.html", + "http://rhn.redhat.com/errata/RHSA-2015-0850.html", + "http://rhn.redhat.com/errata/RHSA-2015-0851.html", + "http://rhn.redhat.com/errata/RHSA-2015-1176.html", + "http://rhn.redhat.com/errata/RHSA-2015-1177.html", + "http://rhn.redhat.com/errata/RHSA-2015-1888.html", + "http://rhn.redhat.com/errata/RHSA-2016-1773.html", + "http://rhn.redhat.com/errata/RHSA-2016-1931.html", + "http://seclists.org/fulldisclosure/2014/Aug/48", + "http://secunia.com/advisories/60466", + "http://www.openwall.com/lists/oss-security/2021/10/06/1", + "http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html", + "http://www.osvdb.org/110143", + "http://www.securityfocus.com/bid/69258", + "http://www.securitytracker.com/id/1030812", + "http://www.ubuntu.com/usn/USN-2769-1", + "https://access.redhat.com/solutions/1165533", + "https://exchange.xforce.ibmcloud.com/vulnerabilities/95327", + "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564", + "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E" + ], + "description": "org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \"CN=\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \"foo,CN=www.apache.org\" string in the O field.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "2.0", + "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N", + "metrics": { + "baseScore": 5.8, + "exploitabilityScore": 8.6, + "impactScore": 4.9 + }, + "vendorMetadata": {} + } + ] + } + ], + "matchDetails": [ + { + "type": "exact-direct-match", + "matcher": "java-matcher", + "searchedBy": { + "language": "java", + "namespace": "github:language:java", + "package": { + "name": "httpclient", + "version": "4.1.1" + } + }, + "found": { + "versionConstraint": "<4.3.5 (unknown)", + "vulnerabilityID": "GHSA-cfh5-3ghh-wfjx" + } + } + ], + "artifact": { + "id": "f09cdae46b001bc5", + "name": "httpclient", + "version": "4.1.1", + "type": "java-archive", + "locations": [ + { + "path": "/TwilioNotifier.hpi", + "layerID": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9" + } + ], + "language": "java", + "licenses": [], + "cpes": [ + "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*" + ], + "purl": "pkg:maven/org.apache.httpcomponents/httpclient@4.1.1", + "upstreams": [], + "metadataType": "JavaMetadata", + "metadata": { + "virtualPath": "/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient", + "pomArtifactID": "httpclient", + "pomGroupID": "org.apache.httpcomponents", + "manifestName": "", + "archiveDigests": null + } + } + }, + { + "vulnerability": { + "id": "CVE-2014-3577", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2014-3577", + "namespace": "nvd:cpe", + "severity": "Medium", + "urls": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html", + "http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html", + "http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html", + "http://rhn.redhat.com/errata/RHSA-2014-1146.html", + "http://rhn.redhat.com/errata/RHSA-2014-1166.html", + "http://rhn.redhat.com/errata/RHSA-2014-1833.html", + "http://rhn.redhat.com/errata/RHSA-2014-1834.html", + "http://rhn.redhat.com/errata/RHSA-2014-1835.html", + "http://rhn.redhat.com/errata/RHSA-2014-1836.html", + "http://rhn.redhat.com/errata/RHSA-2014-1891.html", + "http://rhn.redhat.com/errata/RHSA-2014-1892.html", + "http://rhn.redhat.com/errata/RHSA-2015-0125.html", + "http://rhn.redhat.com/errata/RHSA-2015-0158.html", + "http://rhn.redhat.com/errata/RHSA-2015-0675.html", + "http://rhn.redhat.com/errata/RHSA-2015-0720.html", + "http://rhn.redhat.com/errata/RHSA-2015-0765.html", + "http://rhn.redhat.com/errata/RHSA-2015-0850.html", + "http://rhn.redhat.com/errata/RHSA-2015-0851.html", + "http://rhn.redhat.com/errata/RHSA-2015-1176.html", + "http://rhn.redhat.com/errata/RHSA-2015-1177.html", + "http://rhn.redhat.com/errata/RHSA-2015-1888.html", + "http://rhn.redhat.com/errata/RHSA-2016-1773.html", + "http://rhn.redhat.com/errata/RHSA-2016-1931.html", + "http://seclists.org/fulldisclosure/2014/Aug/48", + "http://secunia.com/advisories/60466", + "http://www.openwall.com/lists/oss-security/2021/10/06/1", + "http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html", + "http://www.osvdb.org/110143", + "http://www.securityfocus.com/bid/69258", + "http://www.securitytracker.com/id/1030812", + "http://www.ubuntu.com/usn/USN-2769-1", + "https://access.redhat.com/solutions/1165533", + "https://exchange.xforce.ibmcloud.com/vulnerabilities/95327", + "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564", + "https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E", + "https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E" + ], + "description": "org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \"CN=\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \"foo,CN=www.apache.org\" string in the O field.", + "cvss": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "version": "2.0", + "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N", + "metrics": { + "baseScore": 5.8, + "exploitabilityScore": 8.6, + "impactScore": 4.9 + }, + "vendorMetadata": {} + } + ], + "fix": { + "versions": [], + "state": "unknown" + }, + "advisories": [] + }, + "relatedVulnerabilities": [], + "matchDetails": [ + { + "type": "cpe-match", + "matcher": "java-matcher", + "searchedBy": { + "namespace": "nvd:cpe", + "cpes": [ + "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*" + ], + "Package": { + "name": "httpclient", + "version": "4.1.1" + } + }, + "found": { + "vulnerabilityID": "CVE-2014-3577", + "versionConstraint": ">= 4.0, <= 4.3.4 (unknown)", + "cpes": [ + "cpe:2.3:a:apache:httpclient:*:*:*:*:*:*:*:*" + ] + } + } + ], + "artifact": { + "id": "f09cdae46b001bc5", + "name": "httpclient", + "version": "4.1.1", + "type": "java-archive", + "locations": [ + { + "path": "/TwilioNotifier.hpi", + "layerID": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9" + } + ], + "language": "java", + "licenses": [], + "cpes": [ + "cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*" + ], + "purl": "pkg:maven/org.apache.httpcomponents/httpclient@4.1.1", + "upstreams": [], + "metadataType": "JavaMetadata", + "metadata": { + "virtualPath": "/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient", + "pomArtifactID": "httpclient", + "pomGroupID": "org.apache.httpcomponents", + "manifestName": "", + "archiveDigests": null + } + } + } + ], + "source": { + "type": "image", + "target": { + "userInput": "anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da", + "imageID": "sha256:e1a0913e5e6eb346f15791e9627842ae80b14564f9c7a4f2e0910a9433673d8b", + "manifestDigest": "sha256:1212e7636ec0b1a7b90eb354e761e67163c2256de127036f086876e190631b43", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [], + "imageSize": 42104079, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68", + "size": 5590942 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9", + "size": 36511427 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:5d5007f009bb615228db4046d5cae910563859d1e3a37cadb2d691ea783ad8a7", + "size": 1710 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMTU5LCJkaWdlc3QiOiJzaGEyNTY6ZTFhMDkxM2U1ZTZlYjM0NmYxNTc5MWU5NjI3ODQyYWU4MGIxNDU2NGY5YzdhNGYyZTA5MTBhOTQzMzY3M2Q4YiJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODY1NDcyLCJkaWdlc3QiOiJzaGEyNTY6ZTJlYjA2ZDhhZjgyMThjZmVjODIxMDE0NzM1N2E2OGI3ZTEzZjdjNDg1Yjk5MWMyODhjMmQwMWRjMjI4YmI2OCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM2NTE1MzI4LCJkaWdlc3QiOiJzaGEyNTY6NmNjNmRiMTc2NDQwZTNkYzMyMThkMmUzMjU3MTZjMTkyMmVhOWQ5MDBiNjFkN2FkNmYzODhmZDBlZDJiNGVmOSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1Njo1ZDUwMDdmMDA5YmI2MTUyMjhkYjQwNDZkNWNhZTkxMDU2Mzg1OWQxZTNhMzdjYWRiMmQ2OTFlYTc4M2FkOGE3In1dfQ==", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6YTUyNzg0NzAxODkzMmE0ZWZlMWYxM2U4MzY3NTE4YzQ0MmI2MzE1OTA3YTE2MDRiZWJhYTJhZjg1NjgwMTc1MSIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsiL2Jpbi9zaCIsIi1jIiwiIyhub3ApIENPUFkgZmlsZTo4ZTY2YzA3MmFjYjU4ZTVjN2ViZWU5MGI0ZGVhNjc1YjdjM2VmOTA5MTQ0Yjk3MzA4MzYwMGU3N2NkNDIyNzY5IGluIC8gIl0sIkltYWdlIjoic2hhMjU2OmE1Mjc4NDcwMTg5MzJhNGVmZTFmMTNlODM2NzUxOGM0NDJiNjMxNTkwN2ExNjA0YmViYWEyYWY4NTY4MDE3NTEiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMjJUMTc6MDY6MzIuOTIxOTkxNjI5WiIsImRvY2tlcl92ZXJzaW9uIjoiMjAuMTAuNyIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA4LTI3VDE3OjE5OjQ1LjU1MzA5MjM2M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YWFkNDI5MGQyNzU4MGNjMWEwOTRmZmFmOThjM2NhMmZjNWQ2OTlmZTY5NWRmYjhlNmU5ZmFjMjBmMTEyOTQ1MCBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjEtMDgtMjdUMTc6MTk6NDUuNzU4NjExNTIzWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMS0xMC0yMlQxNzowNjozMi43MTI3NTA5MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgd2dldCAtbnYgaHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL2p1bml0L2p1bml0LzQuMTMuMS9qdW5pdC00LjEzLjEuamFyIFx1MDAyNlx1MDAyNiAgICAgd2dldCAtbnYgaHR0cHM6Ly9nZXQuamVua2lucy5pby9wbHVnaW5zL1R3aWxpb05vdGlmaWVyLzAuMi4xL1R3aWxpb05vdGlmaWVyLmhwaSBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vdXBkYXRlcy5qZW5raW5zLWNpLm9yZy9kb3dubG9hZC93YXIvMS4zOTAvaHVkc29uLndhciBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vZ2V0LmplbmtpbnMuaW8vcGx1Z2lucy9ub21hZC8wLjcuNC9ub21hZC5ocGkifSx7ImNyZWF0ZWQiOiIyMDIxLTEwLTIyVDE3OjA2OjMyLjkyMTk5MTYyOVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOjhlNjZjMDcyYWNiNThlNWM3ZWJlZTkwYjRkZWE2NzViN2MzZWY5MDkxNDRiOTczMDgzNjAwZTc3Y2Q0MjI3NjkgaW4gLyAifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjplMmViMDZkOGFmODIxOGNmZWM4MjEwMTQ3MzU3YTY4YjdlMTNmN2M0ODViOTkxYzI4OGMyZDAxZGMyMjhiYjY4Iiwic2hhMjU2OjZjYzZkYjE3NjQ0MGUzZGMzMjE4ZDJlMzI1NzE2YzE5MjJlYTlkOTAwYjYxZDdhZDZmMzg4ZmQwZWQyYjRlZjkiLCJzaGEyNTY6NWQ1MDA3ZjAwOWJiNjE1MjI4ZGI0MDQ2ZDVjYWU5MTA1NjM4NTlkMWUzYTM3Y2FkYjJkNjkxZWE3ODNhZDhhNyJdfX0=", + "repoDigests": [ + "anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da" + ], + "architecture": "amd64", + "os": "linux" + } + }, + "distro": { + "name": "alpine", + "version": "3.14.2", + "idLike": [] + }, + "descriptor": { + "name": "grype", + "version": "0.65.1", + "configuration": { + "configPath": "", + "verbosity": 0, + "output": [ + "json" + ], + "file": "", + "distro": "", + "add-cpes-if-none": false, + "output-template-file": "", + "quiet": true, + "check-for-app-update": true, + "only-fixed": false, + "only-notfixed": false, + "platform": "", + "search": { + "scope": "Squashed", + "unindexed-archives": false, + "indexed-archives": true + }, + "ignore": null, + "exclude": [], + "db": { + "cache-dir": "/Users/willmurphy/Library/Caches/grype/db", + "update-url": "https://toolbox-data.anchore.io/grype/databases/listing.json", + "ca-cert": "", + "auto-update": true, + "validate-by-hash-on-start": false, + "validate-age": true, + "max-allowed-built-age": 432000000000000 + }, + "externalSources": { + "enable": false, + "maven": { + "searchUpstreamBySha1": true, + "baseUrl": "https://search.maven.org/solrsearch/select" + } + }, + "match": { + "java": { + "using-cpes": true + }, + "dotnet": { + "using-cpes": true + }, + "golang": { + "using-cpes": true + }, + "javascript": { + "using-cpes": false + }, + "python": { + "using-cpes": true + }, + "ruby": { + "using-cpes": true + }, + "stock": { + "using-cpes": true + } + }, + "dev": { + "profile-cpu": false, + "profile-mem": false + }, + "fail-on-severity": "", + "registry": { + "insecure-skip-tls-verify": false, + "insecure-use-http": false, + "auth": [] + }, + "log": { + "structured": false, + "level": "", + "file": "" + }, + "show-suppressed": false, + "by-cve": false, + "name": "", + "default-image-pull-source": "" + }, + "db": { + "built": "2023-08-31T01:24:19Z", + "schemaVersion": 5, + "location": "/Users/willmurphy/Library/Caches/grype/db/5", + "checksum": "sha256:911c05ea7c2a5f993758e5428c614914384c2a8265d7e2b0edb843799d62626c", + "error": null + }, + "timestamp": "2023-08-31T15:13:32.377177-04:00" + } +} From 03dc5dbf61ff0916193fe53b86ef7cd9cd619e0c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 8 Sep 2023 12:08:56 -0400 Subject: [PATCH 48/49] clean up comments Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index 9d4ad28f220..c55c637b77c 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -23,14 +23,6 @@ type VulnerabilityExplainer interface { ExplainAll() error } -// TODO: basically re-write a lot of this -// Build a structure where an ExplainedVulnerability -// is basically the nvd:cpe record, plus a list of -// records that relate up to it. The convert that -// record to the ExplainViewModel building a list -// of artifacts we matched and why, and then -// render it either as JSON or as the template. - type ViewModel struct { PrimaryVulnerability models.VulnerabilityMetadata RelatedVulnerabilities []models.VulnerabilityMetadata @@ -48,8 +40,6 @@ type viewModelBuilder struct { type Findings map[string]ViewModel -// It looks like an explained package -// should really be a single location and a slice of evidence? type explainedPackage struct { PURL string Name string @@ -60,7 +50,7 @@ type explainedPackage struct { DirectExplanation string CPEExplanation string Locations []explainedEvidence - displayPriority int // how early in output should this appear? + displayPriority int // shows how early it should be displayed; direct matches first } type explainedEvidence struct { @@ -87,7 +77,6 @@ var funcs = template.FuncMap{ } func (e *vulnerabilityExplainer) ExplainByID(ids []string) error { - // TODO: requested ID is always the primary match findings, err := Doc(e.doc, ids) if err != nil { return err @@ -158,8 +147,6 @@ func newBuilder(requestedIDs []string) *viewModelBuilder { // WithMatch adds a match to the builder // accepting enough information to determine whether the match is a primary match or a related match func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) { - // TODO: check if it's a primary vulnerability - // (the below checks if it's a primary _match_, which is wrong) if b.isPrimaryAdd(m, userRequestedIDs) { // Demote the current primary match to related match // if it exists @@ -174,8 +161,6 @@ func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) // TODO: is this still needed? func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string) bool { - // TODO: "primary" is a property of a vulnerability, not a match - // if there's not currently any match, make this one primary since we don't know any better if b.PrimaryMatch.Vulnerability.ID == "" { return true } @@ -187,8 +172,7 @@ func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs break } } - // We're making graphs of specifically requested IDs, and the user didn't ask about - // this ID, so it can't be primary + // the user didn't ask about this ID, so it's not the primary one if !idWasRequested && len(userRequestedIDs) > 0 { return false } @@ -198,7 +182,6 @@ func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs return true } // Either the user didn't ask for specific IDs, or the candidate has an ID the user asked for. - // TODO: this is the property for _, related := range b.PrimaryMatch.RelatedVulnerabilities { if related.ID == candidate.Vulnerability.ID { return true @@ -220,8 +203,6 @@ func (b *viewModelBuilder) WithRelatedMatch(m models.Match) *viewModelBuilder { func (b *viewModelBuilder) Build() ViewModel { explainedPackages := groupAndSortEvidence(append(b.RelatedMatches, b.PrimaryMatch)) - // TODO: this isn't right at all. - // We need to be able to add related vulnerabilities var relatedVulnerabilities []models.VulnerabilityMetadata dedupeRelatedVulnerabilities := make(map[string]models.VulnerabilityMetadata) var sortDedupedRelatedVulnerabilities []string From b6ccb2e025eba979897301c90c97466a99f11e0d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 11 Sep 2023 12:59:57 -0400 Subject: [PATCH 49/49] more comment cleanup Signed-off-by: Will Murphy --- grype/presenter/explain/explain.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/grype/presenter/explain/explain.go b/grype/presenter/explain/explain.go index c55c637b77c..92038be7574 100644 --- a/grype/presenter/explain/explain.go +++ b/grype/presenter/explain/explain.go @@ -31,11 +31,9 @@ type ViewModel struct { } type viewModelBuilder struct { - // TODO: this field is unused. Need think a bit more here. - PrimaryVulnerability models.Vulnerability // this is the vulnerability we're trying to explain - PrimaryMatch models.Match // The primary vulnerability - RelatedMatches []models.Match - requestedIDs []string // the vulnerability IDs the user requested explanations of + PrimaryMatch models.Match // The match that seems to be the one we're trying to explain + RelatedMatches []models.Match + requestedIDs []string // the vulnerability IDs the user requested explanations of } type Findings map[string]ViewModel @@ -43,7 +41,7 @@ type Findings map[string]ViewModel type explainedPackage struct { PURL string Name string - Version string // TODO: is there only going to be one of these? + Version string MatchedOnID string MatchedOnNamespace string IndirectExplanation string @@ -128,7 +126,6 @@ func Doc(doc *models.Document, requestedIDs []string) (Findings, error) { existing = newBuilder(requestedIDs) builders[key] = existing } - // TODO: need to pass in info about the related vulnerability existing.WithMatch(m, requestedIDs) } } @@ -159,7 +156,6 @@ func (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) } } -// TODO: is this still needed? func (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string) bool { if b.PrimaryMatch.Vulnerability.ID == "" { return true