frontend: display unexported vulnerable symbols

If all vulnerable symbols in a package are unexported, display
the unexported symbols instead of displaying "all symbols".

Change-Id: I509ac158670ed2529bc040abbd80e96bdde676a4
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/571320
TryBot-Bypass: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/vuln/vulns.go b/internal/vuln/vulns.go
index 2ced671..445b2e6 100644
--- a/internal/vuln/vulns.go
+++ b/internal/vuln/vulns.go
@@ -78,9 +78,10 @@
 type AffectedPackage struct {
 	PackagePath string
 	Versions    string
-	// List of exported affected symbols. Empty list
-	// implies all symbols in the package are affected.
-	Symbols []string
+	// Lists of affected symbols.
+	// If both of these lists are empty, all symbols in the package are affected.
+	ExportedSymbols   []string
+	UnexportedSymbols []string
 }
 
 // A pair is like an osv.Range, but each pair is a self-contained 2-tuple
@@ -152,10 +153,12 @@
 			vs = append(vs, s)
 		}
 		for _, p := range a.EcosystemSpecific.Packages {
+			exported, unexported := affectedSymbols(p.Symbols)
 			affs = append(affs, &AffectedPackage{
-				PackagePath: p.Path,
-				Versions:    strings.Join(vs, ", "),
-				Symbols:     exportedSymbols(p.Symbols),
+				PackagePath:       p.Path,
+				Versions:          strings.Join(vs, ", "),
+				ExportedSymbols:   exported,
+				UnexportedSymbols: unexported,
 				// TODO(hyangah): where to place GOOS/GOARCH info
 			})
 		}
@@ -163,18 +166,19 @@
 	return affs
 }
 
-func exportedSymbols(in []string) []string {
-	var out []string
+func affectedSymbols(in []string) (e, u []string) {
 	for _, s := range in {
 		exported := true
 		for _, part := range strings.Split(s, ".") {
 			if !token.IsExported(part) {
-				exported = false // exported only all parts in the symbol name are exported.
+				exported = false // exported only if all parts of the symbol name are exported.
 			}
 		}
 		if exported {
-			out = append(out, s)
+			e = append(e, s)
+		} else {
+			u = append(u, s)
 		}
 	}
-	return out
+	return e, u
 }
diff --git a/internal/vuln/vulns_test.go b/internal/vuln/vulns_test.go
index 3fd99fa..167a8cc 100644
--- a/internal/vuln/vulns_test.go
+++ b/internal/vuln/vulns_test.go
@@ -261,8 +261,8 @@
 				}},
 			},
 			want: []*AffectedPackage{{
-				PackagePath: "example.com/mod/pkg",
-				Symbols:     []string{"F"},
+				PackagePath:     "example.com/mod/pkg",
+				ExportedSymbols: []string{"F"},
 			}},
 		},
 		{
@@ -280,8 +280,9 @@
 				}},
 			},
 			want: []*AffectedPackage{{
-				PackagePath: "example.com/mod/pkg",
-				Symbols:     []string{"F", "S.F"}, // unexported symbols are excluded.
+				PackagePath:       "example.com/mod/pkg",
+				ExportedSymbols:   []string{"F", "S.F"},
+				UnexportedSymbols: []string{"g", "S.f", "s.F", "s.f"},
 			}},
 		},
 		{
@@ -328,11 +329,12 @@
 			want: []*AffectedPackage{{
 				PackagePath: "example.com/mod1/pkg1",
 			}, {
-				PackagePath: "example.com/mod1/pkg2",
-				Symbols:     []string{"F"},
+				PackagePath:     "example.com/mod1/pkg2",
+				ExportedSymbols: []string{"F"},
 			}, {
-				PackagePath: "example.com/mod2/pkg3",
-				Symbols:     []string{"H"},
+				PackagePath:       "example.com/mod2/pkg3",
+				ExportedSymbols:   []string{"H"},
+				UnexportedSymbols: []string{"g"},
 			}},
 		},
 	}
diff --git a/static/frontend/vuln/entry/entry.tmpl b/static/frontend/vuln/entry/entry.tmpl
index 44b935d..620aa8a 100644
--- a/static/frontend/vuln/entry/entry.tmpl
+++ b/static/frontend/vuln/entry/entry.tmpl
@@ -56,12 +56,20 @@
       <div class="VulnEntryPackages-attr" data-name="Path"><a href="/{{.PackagePath}}">{{.PackagePath}}</a></div>
       <div class="VulnEntryPackages-attr" data-name="Versions">{{if .Versions}}{{.Versions}}{{else}}all versions, no known fixed{{end}}</div>
       <div class="VulnEntryPackages-attr VulnEntryPackages-symbols" data-name="Symbols">
-      {{if .Symbols}}{{ $length := len .Symbols}}
-         {{ $vuln := . }}
-         {{if lt $length 5}}<ul>{{range .Symbols}}<li><a href="/{{$vuln.PackagePath}}#{{.}}">{{.}}</a></li>{{end}}</ul>
-         {{else}}<details><summary>{{len .Symbols}} affected symbols</summary>
-         <ul class="VulnEntryPackages-detailsContent">{{range .Symbols}}<li><a href="/{{$vuln.PackagePath}}#{{.}}">{{.}}</a></li>{{end}}</ul></details>
+      {{ $vuln := . }}
+      {{if .ExportedSymbols}}{{ $length := len .ExportedSymbols}}
+         {{if lt $length 5}}<ul>{{range .ExportedSymbols}}<li><a href="/{{$vuln.PackagePath}}#{{.}}">{{.}}</a></li>{{end}}</ul>
+         {{else}}<details><summary>{{len .ExportedSymbols}} affected symbols</summary>
+         <ul class="VulnEntryPackages-detailsContent">{{range .ExportedSymbols}}<li><a href="/{{$vuln.PackagePath}}#{{.}}">{{.}}</a></li>{{end}}</ul></details>
          {{end}}
+      {{else if .UnexportedSymbols}}
+        <details><summary>{{len .UnexportedSymbols}} unexported affected symbols</summary>
+          <ul class="VulnEntryPackages-detailsContent">
+            {{range .UnexportedSymbols}}
+              <li>{{.}}</li>
+            {{end}}
+          </ul>
+        </details>
       {{else}}all symbols{{end}}
       </div>
     </li>
@@ -109,4 +117,4 @@
       Suggest an edit to this report.
     </a>
   </div>
-{{end}}
\ No newline at end of file
+{{end}}