diff --git a/internal/frontend/vulns.go b/internal/frontend/vulns.go index fa2b75f2..adda808e 100644 --- a/internal/frontend/vulns.go +++ b/internal/frontend/vulns.go @@ -35,10 +35,11 @@ type VulnListPage struct { // vuln entry. type VulnEntryPage struct { page.BasePage - Entry *osv.Entry - AffectedPackages []*vuln.AffectedPackage - AliasLinks []link - AdvisoryLinks []link + Entry *osv.Entry + AffectedPackages []*vuln.AffectedComponent + ModulesWithNoPackages []*vuln.AffectedComponent + AliasLinks []link + AdvisoryLinks []link } func (s *Server) serveVuln(w http.ResponseWriter, r *http.Request, _ internal.DataSource) error { @@ -120,11 +121,13 @@ func newVulnEntryPage(ctx context.Context, client *vuln.Client, id string) (*Vul if entry == nil { return nil, derrors.NotFound } + pkgs, mods := vuln.AffectedComponents(entry) return &VulnEntryPage{ - Entry: entry, - AffectedPackages: vuln.AffectedPackages(entry), - AliasLinks: aliasLinks(entry), - AdvisoryLinks: advisoryLinks(entry), + Entry: entry, + AffectedPackages: pkgs, + ModulesWithNoPackages: mods, + AliasLinks: aliasLinks(entry), + AdvisoryLinks: advisoryLinks(entry), }, nil } diff --git a/internal/vuln/vulns.go b/internal/vuln/vulns.go index 445b2e6d..cff9a33e 100644 --- a/internal/vuln/vulns.go +++ b/internal/vuln/vulns.go @@ -74,11 +74,11 @@ func toVulns(entries []*osv.Entry) []Vuln { return vulns } -// AffectedPackage holds information about a package affected by a certain vulnerability. -type AffectedPackage struct { - PackagePath string - Versions string - // Lists of affected symbols. +// AffectedComponent holds information about a module/package affected by a certain vulnerability. +type AffectedComponent struct { + Path string + Versions string + // Lists of affected symbols (for packages). // If both of these lists are empty, all symbols in the package are affected. ExportedSymbols []string UnexportedSymbols []string @@ -131,9 +131,8 @@ func collectRangePairs(a osv.Affected) []pair { return ps } -// AffectedPackages extracts information about affected packages from the given osv.Entry. -func AffectedPackages(e *osv.Entry) []*AffectedPackage { - var affs []*AffectedPackage +// AffectedComponents extracts information about affected packages (and // modules, if there are any with no package information) from the given osv.Entry. +func AffectedComponents(e *osv.Entry) (pkgs, modsNoPkgs []*AffectedComponent) { for _, a := range e.Affected { pairs := collectRangePairs(a) var vs []string @@ -152,10 +151,16 @@ func AffectedPackages(e *osv.Entry) []*AffectedPackage { } vs = append(vs, s) } + if len(a.EcosystemSpecific.Packages) == 0 { + modsNoPkgs = append(modsNoPkgs, &AffectedComponent{ + Path: a.Module.Path, + Versions: strings.Join(vs, ", "), + }) + } for _, p := range a.EcosystemSpecific.Packages { exported, unexported := affectedSymbols(p.Symbols) - affs = append(affs, &AffectedPackage{ - PackagePath: p.Path, + pkgs = append(pkgs, &AffectedComponent{ + Path: p.Path, Versions: strings.Join(vs, ", "), ExportedSymbols: exported, UnexportedSymbols: unexported, @@ -163,7 +168,7 @@ func AffectedPackages(e *osv.Entry) []*AffectedPackage { }) } } - return affs + return pkgs, modsNoPkgs } func affectedSymbols(in []string) (e, u []string) { diff --git a/internal/vuln/vulns_test.go b/internal/vuln/vulns_test.go index 167a8cc1..375b886f 100644 --- a/internal/vuln/vulns_test.go +++ b/internal/vuln/vulns_test.go @@ -186,7 +186,7 @@ func TestCollectRangePairs(t *testing.T) { } -func TestAffectedPackages_Versions(t *testing.T) { +func TestAffectedComponents_Versions(t *testing.T) { for _, test := range []struct { name string in []osv.RangeEvent @@ -231,7 +231,7 @@ func TestAffectedPackages_Versions(t *testing.T) { }}, }}, } - out := AffectedPackages(entry) + out, _ := AffectedComponents(entry) got := out[0].Versions if got != test.want { t.Errorf("got %q, want %q\n", got, test.want) @@ -240,11 +240,12 @@ func TestAffectedPackages_Versions(t *testing.T) { } } -func TestAffectedPackagesPackagesSymbols(t *testing.T) { +func TestAffectedComponents(t *testing.T) { tests := []struct { - name string - in *osv.Entry - want []*AffectedPackage + name string + in *osv.Entry + wantPkgs []*AffectedComponent + wantMods []*AffectedComponent }{ { name: "one symbol", @@ -260,10 +261,11 @@ func TestAffectedPackagesPackagesSymbols(t *testing.T) { }, }}, }, - want: []*AffectedPackage{{ - PackagePath: "example.com/mod/pkg", + wantPkgs: []*AffectedComponent{{ + Path: "example.com/mod/pkg", ExportedSymbols: []string{"F"}, }}, + wantMods: nil, }, { name: "multiple symbols", @@ -279,11 +281,12 @@ func TestAffectedPackagesPackagesSymbols(t *testing.T) { }, }}, }, - want: []*AffectedPackage{{ - PackagePath: "example.com/mod/pkg", + wantPkgs: []*AffectedComponent{{ + Path: "example.com/mod/pkg", ExportedSymbols: []string{"F", "S.F"}, UnexportedSymbols: []string{"g", "S.f", "s.F", "s.f"}, }}, + wantMods: nil, }, { name: "no symbol", @@ -298,51 +301,68 @@ func TestAffectedPackagesPackagesSymbols(t *testing.T) { }, }}, }, - want: []*AffectedPackage{{ - PackagePath: "example.com/mod/pkg", + wantPkgs: []*AffectedComponent{{ + Path: "example.com/mod/pkg", }}, + wantMods: nil, }, { name: "multiple pkgs and modules", in: &osv.Entry{ ID: "GO-2022-0004", - Affected: []osv.Affected{{ - Module: osv.Module{Path: "example.com/mod1"}, - EcosystemSpecific: osv.EcosystemSpecific{ - Packages: []osv.Package{{ - Path: "example.com/mod1/pkg1", - }, { - Path: "example.com/mod1/pkg2", - Symbols: []string{"F"}, + Affected: []osv.Affected{ + { + Module: osv.Module{Path: "example.com/mod"}, + Ranges: []osv.Range{{ + Type: osv.RangeTypeSemver, + Events: []osv.RangeEvent{{Fixed: "1.5"}}, }}, + // no packages }, - }, { - Module: osv.Module{Path: "example.com/mod2"}, - EcosystemSpecific: osv.EcosystemSpecific{ - Packages: []osv.Package{{ - Path: "example.com/mod2/pkg3", - Symbols: []string{"g", "H"}, - }}, - }, - }}, + { + Module: osv.Module{Path: "example.com/mod1"}, + EcosystemSpecific: osv.EcosystemSpecific{ + Packages: []osv.Package{{ + Path: "example.com/mod1/pkg1", + }, { + Path: "example.com/mod1/pkg2", + Symbols: []string{"F"}, + }}, + }, + }, { + Module: osv.Module{Path: "example.com/mod2"}, + EcosystemSpecific: osv.EcosystemSpecific{ + Packages: []osv.Package{{ + Path: "example.com/mod2/pkg3", + Symbols: []string{"g", "H"}, + }}, + }, + }}, }, - want: []*AffectedPackage{{ - PackagePath: "example.com/mod1/pkg1", + wantPkgs: []*AffectedComponent{{ + Path: "example.com/mod1/pkg1", }, { - PackagePath: "example.com/mod1/pkg2", + Path: "example.com/mod1/pkg2", ExportedSymbols: []string{"F"}, }, { - PackagePath: "example.com/mod2/pkg3", + Path: "example.com/mod2/pkg3", ExportedSymbols: []string{"H"}, UnexportedSymbols: []string{"g"}, }}, + wantMods: []*AffectedComponent{{ + Path: "example.com/mod", + Versions: "before v1.5", + }}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := AffectedPackages(tt.in) - if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(AffectedPackage{})); diff != "" { - t.Errorf("mismatch (-want, +got):\n%s", diff) + gotPkgs, gotMods := AffectedComponents(tt.in) + if diff := cmp.Diff(tt.wantPkgs, gotPkgs, cmpopts.IgnoreUnexported(AffectedComponent{})); diff != "" { + t.Errorf("pkgs mismatch (-want, +got):\n%s", diff) + } + if diff := cmp.Diff(tt.wantMods, gotMods, cmpopts.IgnoreUnexported(AffectedComponent{})); diff != "" { + t.Errorf("mods mismatch (-want, +got):\n%s", diff) } }) } diff --git a/static/frontend/vuln/entry/entry.css b/static/frontend/vuln/entry/entry.css index e9e9523b..dafa36c5 100644 --- a/static/frontend/vuln/entry/entry.css +++ b/static/frontend/vuln/entry/entry.css @@ -81,6 +81,10 @@ padding: 0.5rem; } + .VulnEntryModules { + grid-template-columns: minmax(10em, 50%) 1fr; + } + /* Header */ .VulnEntryPackages-item-container:first-child { background-color: var(--color-background-accented); diff --git a/static/frontend/vuln/entry/entry.min.css b/static/frontend/vuln/entry/entry.min.css index d8bac6fb..aa859aff 100644 --- a/static/frontend/vuln/entry/entry.min.css +++ b/static/frontend/vuln/entry/entry.min.css @@ -3,5 +3,5 @@ * Use of this source code is governed by a BSD-style * license that can be found in the LICENSE file. */ -.Vuln-alias{display:none}.VulnEntry{display:flex;flex-direction:column;gap:1rem;margin-top:.5rem}.VulnEntry h2{font-size:1.25rem}.VulnEntryPackages-detailsContent{margin-left:.2rem}.VulnEntryPackages-container{display:grid;grid-gap:.5rem;grid-template-columns:1fr}.VulnEntryPackages-container>li:first-child{display:none}.VulnEntryPackages-attr:before{color:var(--color-text-subtle);content:attr(data-name)}.VulnEntryPackages-attr{display:grid;grid-template-columns:minmax(5em,10%) 1fr;list-style:disc;list-style-position:inside;overflow-wrap:anywhere;padding:.2rem}@media screen and (min-width: 46rem){.VulnEntryPackages-container{grid-gap:0}.VulnEntryPackages-item{padding:inherit}.VulnEntryPackages-container>li:first-child{display:grid}.VulnEntryPackages-attr:before{content:none}.VulnEntryPackages-attr{grid-template-columns:1fr}.VulnEntryPackages-item-container{display:grid;grid-template-columns:minmax(10em,50%) minmax(5em,20%) 1fr;padding:.5rem}.VulnEntryPackages-item-container:first-child{background-color:var(--color-background-accented)}.VulnEntryPackages-item-container:first-child .VulnEntryPackages-attr{display:flex;font-weight:700;overflow:auto;text-overflow:initial;white-space:normal}}.VulnEntry-referenceList,.VulnEntry-aliases{line-height:1.75rem;word-break:break-all} +.Vuln-alias{display:none}.VulnEntry{display:flex;flex-direction:column;gap:1rem;margin-top:.5rem}.VulnEntry h2{font-size:1.25rem}.VulnEntryPackages-detailsContent{margin-left:.2rem}.VulnEntryPackages-container{display:grid;grid-gap:.5rem;grid-template-columns:1fr}.VulnEntryPackages-container>li:first-child{display:none}.VulnEntryPackages-attr:before{color:var(--color-text-subtle);content:attr(data-name)}.VulnEntryPackages-attr{display:grid;grid-template-columns:minmax(5em,10%) 1fr;list-style:disc;list-style-position:inside;overflow-wrap:anywhere;padding:.2rem}@media screen and (min-width: 46rem){.VulnEntryPackages-container{grid-gap:0}.VulnEntryPackages-item{padding:inherit}.VulnEntryPackages-container>li:first-child{display:grid}.VulnEntryPackages-attr:before{content:none}.VulnEntryPackages-attr{grid-template-columns:1fr}.VulnEntryPackages-item-container{display:grid;grid-template-columns:minmax(10em,50%) minmax(5em,20%) 1fr;padding:.5rem}.VulnEntryModules{grid-template-columns:minmax(10em,50%) 1fr}.VulnEntryPackages-item-container:first-child{background-color:var(--color-background-accented)}.VulnEntryPackages-item-container:first-child .VulnEntryPackages-attr{display:flex;font-weight:700;overflow:auto;text-overflow:initial;white-space:normal}}.VulnEntry-referenceList,.VulnEntry-aliases{line-height:1.75rem;word-break:break-all} /*# sourceMappingURL=entry.min.css.map */ diff --git a/static/frontend/vuln/entry/entry.min.css.map b/static/frontend/vuln/entry/entry.min.css.map index f0ceeee7..1140bdce 100644 --- a/static/frontend/vuln/entry/entry.min.css.map +++ b/static/frontend/vuln/entry/entry.min.css.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["entry.css"], - "sourcesContent": ["/*\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.Vuln-alias {\n display: none;\n}\n\n.VulnEntry {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n margin-top: 0.5rem;\n}\n\n.VulnEntry h2 {\n font-size: 1.25rem;\n}\n\n.VulnEntryPackages-detailsContent {\n margin-left: 0.2rem;\n}\n\n/* One column by default */\n.VulnEntryPackages-container {\n display: grid;\n grid-gap: 0.5rem;\n grid-template-columns: 1fr;\n}\n\n/* Don't display the first item - the headers for multi-col layout */\n.VulnEntryPackages-container > li:first-child {\n display: none;\n}\n\n.VulnEntryPackages-attr::before {\n color: var(--color-text-subtle);\n content: attr(data-name);\n}\n\n/* Attribute name for first column, and attribute value for second column. */\n.VulnEntryPackages-attr {\n display: grid;\n grid-template-columns: minmax(5em, 10%) 1fr;\n list-style: disc;\n list-style-position: inside;\n\n /* package and symbol names can be pretty long */\n overflow-wrap: anywhere;\n padding: 0.2rem;\n}\n\n/* Three columns for wider screen */\n@media screen and (min-width: 46rem) {\n /* Undo what's done by default */\n .VulnEntryPackages-container {\n grid-gap: 0;\n }\n\n .VulnEntryPackages-item {\n padding: inherit;\n }\n\n .VulnEntryPackages-container > li:first-child {\n display: grid; /* undo display: none setfor default */\n }\n\n .VulnEntryPackages-attr::before {\n content: none;\n }\n\n .VulnEntryPackages-attr {\n grid-template-columns: 1fr;\n }\n\n .VulnEntryPackages-item-container {\n display: grid;\n grid-template-columns: minmax(10em, 50%) minmax(5em, 20%) 1fr;\n padding: 0.5rem;\n }\n\n /* Header */\n .VulnEntryPackages-item-container:first-child {\n background-color: var(--color-background-accented);\n }\n\n /* Header text */\n .VulnEntryPackages-item-container:first-child .VulnEntryPackages-attr {\n display: flex;\n font-weight: bold;\n overflow: auto;\n text-overflow: initial;\n white-space: normal;\n }\n}\n\n.VulnEntry-referenceList,\n.VulnEntry-aliases {\n line-height: 1.75rem;\n word-break: break-all;\n}\n"], - "mappings": ";;;;;AAMA,YACE,aAGF,WACE,aACA,sBACA,SACA,iBAGF,cACE,kBAGF,kCACE,kBAIF,6BACE,aACA,eACA,0BAIF,4CACE,aAGF,+BACE,+BACA,wBAIF,wBACE,aACA,0CACA,gBACA,2BAGA,uBAlDF,cAuDA,qCAEE,6BACE,WAGF,wBACE,gBAGF,4CACE,aAGF,+BACE,aAGF,wBACE,0BAGF,kCACE,aACA,2DA/EJ,cAoFE,8CACE,kDAIF,sEACE,aACA,gBACA,cACA,sBACA,oBAIJ,4CAEE,oBACA", + "sourcesContent": ["/*\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.Vuln-alias {\n display: none;\n}\n\n.VulnEntry {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n margin-top: 0.5rem;\n}\n\n.VulnEntry h2 {\n font-size: 1.25rem;\n}\n\n.VulnEntryPackages-detailsContent {\n margin-left: 0.2rem;\n}\n\n/* One column by default */\n.VulnEntryPackages-container {\n display: grid;\n grid-gap: 0.5rem;\n grid-template-columns: 1fr;\n}\n\n/* Don't display the first item - the headers for multi-col layout */\n.VulnEntryPackages-container > li:first-child {\n display: none;\n}\n\n.VulnEntryPackages-attr::before {\n color: var(--color-text-subtle);\n content: attr(data-name);\n}\n\n/* Attribute name for first column, and attribute value for second column. */\n.VulnEntryPackages-attr {\n display: grid;\n grid-template-columns: minmax(5em, 10%) 1fr;\n list-style: disc;\n list-style-position: inside;\n\n /* package and symbol names can be pretty long */\n overflow-wrap: anywhere;\n padding: 0.2rem;\n}\n\n/* Three columns for wider screen */\n@media screen and (min-width: 46rem) {\n /* Undo what's done by default */\n .VulnEntryPackages-container {\n grid-gap: 0;\n }\n\n .VulnEntryPackages-item {\n padding: inherit;\n }\n\n .VulnEntryPackages-container > li:first-child {\n display: grid; /* undo display: none setfor default */\n }\n\n .VulnEntryPackages-attr::before {\n content: none;\n }\n\n .VulnEntryPackages-attr {\n grid-template-columns: 1fr;\n }\n\n .VulnEntryPackages-item-container {\n display: grid;\n grid-template-columns: minmax(10em, 50%) minmax(5em, 20%) 1fr;\n padding: 0.5rem;\n }\n\n .VulnEntryModules {\n grid-template-columns: minmax(10em, 50%) 1fr;\n }\n\n /* Header */\n .VulnEntryPackages-item-container:first-child {\n background-color: var(--color-background-accented);\n }\n\n /* Header text */\n .VulnEntryPackages-item-container:first-child .VulnEntryPackages-attr {\n display: flex;\n font-weight: bold;\n overflow: auto;\n text-overflow: initial;\n white-space: normal;\n }\n}\n\n.VulnEntry-referenceList,\n.VulnEntry-aliases {\n line-height: 1.75rem;\n word-break: break-all;\n}\n"], + "mappings": ";;;;;AAMA,YACE,aAGF,WACE,aACA,sBACA,SACA,iBAGF,cACE,kBAGF,kCACE,kBAIF,6BACE,aACA,eACA,0BAIF,4CACE,aAGF,+BACE,+BACA,wBAIF,wBACE,aACA,0CACA,gBACA,2BAGA,uBAlDF,cAuDA,qCAEE,6BACE,WAGF,wBACE,gBAGF,4CACE,aAGF,+BACE,aAGF,wBACE,0BAGF,kCACE,aACA,2DA/EJ,cAmFE,kBACE,2CAIF,8CACE,kDAIF,sEACE,aACA,gBACA,cACA,sBACA,oBAIJ,4CAEE,oBACA", "names": [] } diff --git a/static/frontend/vuln/entry/entry.tmpl b/static/frontend/vuln/entry/entry.tmpl index 0ccb356b..0bbe0e7b 100644 --- a/static/frontend/vuln/entry/entry.tmpl +++ b/static/frontend/vuln/entry/entry.tmpl @@ -41,7 +41,8 @@

{{end}}
- {{template "affected" .AffectedPackages}} + {{with .ModulesWithNoPackages}}{{template "affected-modules" .}}{{end}} + {{with .AffectedPackages}}{{template "affected-packages" .}}{{end}} {{template "entry" .}}
{{end}} @@ -53,7 +54,7 @@ {{end}} -{{define "affected"}} +{{define "affected-packages"}}

Affected Packages

{{end}} +{{define "affected-modules"}} +

Affected Modules

+ +{{end}} + {{define "entry"}} {{$e := .Entry}} {{if .AliasLinks}} diff --git a/tests/screentest/testdata/ci/vuln-entry-no-packages-540x1080.a.png b/tests/screentest/testdata/ci/vuln-entry-no-packages-540x1080.a.png index 814d2253..c7efb6d8 100644 Binary files a/tests/screentest/testdata/ci/vuln-entry-no-packages-540x1080.a.png and b/tests/screentest/testdata/ci/vuln-entry-no-packages-540x1080.a.png differ diff --git a/tests/screentest/testdata/ci/vuln-entry-no-packages.a.png b/tests/screentest/testdata/ci/vuln-entry-no-packages.a.png index caf1ce8f..a6a5cc1c 100644 Binary files a/tests/screentest/testdata/ci/vuln-entry-no-packages.a.png and b/tests/screentest/testdata/ci/vuln-entry-no-packages.a.png differ