internal/report: add a lint check for module existence

Add a basic lint check that the proxy knows about each module
listed in a report. This allows us to do basic checking on
low-information reports where we don't know versions, packages or symbols.

Change-Id: I92d06cbd27d89ead7725d131e69087085c4cdba5
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/576996
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/report/lint.go b/internal/report/lint.go
index d66d05c..47ce631 100644
--- a/internal/report/lint.go
+++ b/internal/report/lint.go
@@ -25,6 +25,10 @@
 )
 
 func (m *Module) checkModVersions(pc *proxy.Client) error {
+	if ok := pc.ModuleExists(m.Module); !ok {
+		return fmt.Errorf("module %s not known to proxy", m.Module)
+	}
+
 	var notFound []string
 	var nonCanonical []string
 	for _, vr := range m.Versions {
@@ -272,8 +276,8 @@
 }
 
 func (r *Report) missingAdvisory(advisoryCount int) bool {
-	return !r.IsExcluded() && !r.IsFirstParty() &&
-		advisoryCount == 0 && r.Description == "" && r.CVEMetadata == nil
+	return !r.IsExcluded() && r.Description == "" && !r.IsFirstParty() &&
+		advisoryCount == 0 && r.CVEMetadata == nil
 }
 
 func (d *Description) lint(l *linter, r *Report) {
@@ -309,13 +313,8 @@
 	if strings.HasSuffix(summary, ".") {
 		l.Error("must not end in a period (should be a phrase, not a sentence)")
 	}
-	for i, r := range summary {
-		if i != 0 {
-			break
-		}
-		if !unicode.IsUpper(r) {
-			l.Error("must begin with a capital letter")
-		}
+	if !startsWithUpper(summary) {
+		l.Error("must begin with a capital letter")
 	}
 
 	// Summary must contain one of the listed module or package
@@ -330,6 +329,18 @@
 	}
 }
 
+func startsWithUpper(s string) bool {
+	for i, r := range s {
+		if i != 0 {
+			return true
+		}
+		if !unicode.IsUpper(r) {
+			return false
+		}
+	}
+	return false
+}
+
 // containsPath returns whether the summary contains one of
 // the paths in paths.
 // As a special case, if the summary contains a word that contains a "/"
@@ -482,7 +493,7 @@
 		l.Error("no module name")
 	}
 
-	if !m.IsFirstParty() && pc != nil {
+	if !r.IsExcluded() && !m.IsFirstParty() && pc != nil {
 		if err := m.checkModVersions(pc); err != nil {
 			l.Error(err)
 		}
diff --git a/internal/report/testdata/proxy/TestGHSAToReport.json b/internal/report/testdata/proxy/TestGHSAToReport.json
index c7e57d1..da6abb5 100644
--- a/internal/report/testdata/proxy/TestGHSAToReport.json
+++ b/internal/report/testdata/proxy/TestGHSAToReport.json
@@ -1,13 +1,13 @@
 {
 	"golang.org/x/tools/@v/list": {
-		"body": "v0.1.4\nv0.9.3\nv0.7.0\nv0.1.12\nv0.1.3\nv0.9.2\nv0.3.0\nv0.8.0\nv0.6.0\nv0.10.0\nv0.1.9\nv0.5.0\nv0.1.7\nv0.1.10\nv0.12.0\nv0.1.6\nv0.1.2\nv0.9.0\nv0.1.11\nv0.4.0\nv0.1.8\nv0.11.0\nv0.1.0\nv0.1.5\nv0.9.1\nv0.1.1\nv0.11.1\nv0.2.0\n",
+		"body": "v0.15.0\nv0.1.4\nv0.9.3\nv0.7.0\nv0.1.12\nv0.1.3\nv0.9.2\nv0.3.0\nv0.8.0\nv0.6.0\nv0.10.0\nv0.20.0\nv0.1.9\nv0.5.0\nv0.18.0\nv0.1.7\nv0.1.10\nv0.12.0\nv0.1.6\nv0.19.0\nv0.1.2\nv0.9.0\nv0.1.11\nv0.4.0\nv0.1.8\nv0.14.0\nv0.11.0\nv0.1.0\nv0.1.5\nv0.9.1\nv0.1.1\nv0.11.1\nv0.16.1\nv0.13.0\nv0.17.0\nv0.2.0\nv0.16.0\n",
 		"status_code": 200
 	},
 	"golang.org/x/tools/@v/v0.9.0.mod": {
 		"body": "module golang.org/x/tools\n\ngo 1.18 // tagx:compat 1.16\n\nrequire (\n\tgithub.com/yuin/goldmark v1.4.13\n\tgolang.org/x/mod v0.10.0\n\tgolang.org/x/net v0.10.0\n\tgolang.org/x/sys v0.8.0\n)\n\nrequire golang.org/x/sync v0.2.0\n",
 		"status_code": 200
 	},
-	"golang.org/x/tools/go/packages/@v/v0.9.0.mod": {
+	"golang.org/x/tools/go/packages/@v/list": {
 		"status_code": 404
 	}
 }
\ No newline at end of file
diff --git a/internal/report/testdata/proxy/TestLint.json b/internal/report/testdata/proxy/TestLint.json
index f4783e7..a2ed531 100644
--- a/internal/report/testdata/proxy/TestLint.json
+++ b/internal/report/testdata/proxy/TestLint.json
@@ -1,4 +1,8 @@
 {
+	"github.com/golang/vuln/@v/list": {
+		"body": "v1.0.2\nv1.0.0\nv1.0.3\nv1.0.4\nv0.1.0\nv1.0.1\nv0.2.0\n",
+		"status_code": 200
+	},
 	"github.com/golang/vuln/@v/v0.1.0.mod": {
 		"body": "module golang.org/x/vuln\n\ngo 1.18\n\nrequire (\n\tgithub.com/client9/misspell v0.3.4\n\tgithub.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786\n\tgithub.com/google/go-cmp v0.5.8\n\tgolang.org/x/mod v0.10.0\n\tgolang.org/x/sync v0.1.0\n\tgolang.org/x/tools v0.8.1-0.20230421161920-b9619ee54b47\n\thonnef.co/go/tools v0.4.3\n\tmvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8\n)\n\nrequire (\n\tgithub.com/BurntSushi/toml v1.2.1 // indirect\n\tgithub.com/google/renameio v0.1.0 // indirect\n\tgolang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect\n\tgolang.org/x/sys v0.7.0 // indirect\n)\n",
 		"status_code": 200
@@ -9,6 +13,10 @@
 	"github.com/golang/vuln/@v/v0.2.6.mod": {
 		"status_code": 404
 	},
+	"golang.org/x/net/@v/list": {
+		"body": "v0.15.0\nv0.7.0\nv0.3.0\nv0.8.0\nv0.6.0\nv0.10.0\nv0.20.0\nv0.24.0\nv0.23.0\nv0.5.0\nv0.22.0\nv0.18.0\nv0.21.0\nv0.12.0\nv0.19.0\nv0.9.0\nv0.4.0\nv0.14.0\nv0.11.0\nv0.1.0\nv0.13.0\nv0.17.0\nv0.2.0\nv0.16.0\n",
+		"status_code": 200
+	},
 	"golang.org/x/net/@v/v0.2.0.mod": {
 		"body": "module golang.org/x/net\n\ngo 1.17\n\nrequire (\n\tgolang.org/x/sys v0.2.0\n\tgolang.org/x/term v0.2.0\n\tgolang.org/x/text v0.4.0\n)\n",
 		"status_code": 200