internal/report: add a lint check that summary contains module/package path

A new lint check ensures that summaries mention at least one of the affected modules or package paths.

As a special case, the check also allows the
summary to mention a prefix of a module/package path, in case the report
affects many paths or a path is very long.

Change-Id: Iff0c9405343b385a8ee471b09df0686ad39360dc
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/543798
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/internal/genericosv/testdata/yaml/GHSA-28r2-q6m8-9hpx.yaml b/internal/genericosv/testdata/yaml/GHSA-28r2-q6m8-9hpx.yaml
index ed924b9..eed2254 100644
--- a/internal/genericosv/testdata/yaml/GHSA-28r2-q6m8-9hpx.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-28r2-q6m8-9hpx.yaml
@@ -38,3 +38,5 @@
     - web: https://discuss.hashicorp.com/t/hcsec-2022-13-multiple-vulnerabilities-in-go-getter-library/
     - web: https://discuss.hashicorp.com/t/hcsec-2022-13-multiple-vulnerabilities-in-go-getter-library/39930
     - web: https://github.com/hashicorp/go-getter/releases
+notes:
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/hashicorp/go-getter")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-3hwm-922r-47hw.yaml b/internal/genericosv/testdata/yaml/GHSA-3hwm-922r-47hw.yaml
index c5289f1..8d97ded 100644
--- a/internal/genericosv/testdata/yaml/GHSA-3hwm-922r-47hw.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-3hwm-922r-47hw.yaml
@@ -22,3 +22,4 @@
     - web: https://github.com/42Atomys/stud42/commit/a70bfc72fba721917bf681d72a58093fb9deee17
 notes:
     - lint: 'modules[0] "atomys.codes/stud42": version 0.23.0 does not exist'
+    - lint: 'summary: must contain an affected module or package path (e.g. "atomys.codes/stud42")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-3wq5-3f56-v5xc.yaml b/internal/genericosv/testdata/yaml/GHSA-3wq5-3f56-v5xc.yaml
index a5fbd0b..c8c1d90 100644
--- a/internal/genericosv/testdata/yaml/GHSA-3wq5-3f56-v5xc.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-3wq5-3f56-v5xc.yaml
@@ -27,3 +27,4 @@
 notes:
     - lint: 'modules[0] "github.com/mattermost/mattermost-server": 6 versions do not exist: 7.1.0, 7.1.6, 7.7.0, 7.7.2, 7.8.0, 7.8.1'
     - lint: 'modules[1] "github.com/mattermost/mattermost-server/v6": version 7.1.6 does not exist'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/mattermost/mattermost-server")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-54q4-74p3-mgcw.yaml b/internal/genericosv/testdata/yaml/GHSA-54q4-74p3-mgcw.yaml
index f9586f7..de356fe 100644
--- a/internal/genericosv/testdata/yaml/GHSA-54q4-74p3-mgcw.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-54q4-74p3-mgcw.yaml
@@ -21,3 +21,4 @@
     - lint: 'modules[0] "github.com/zhaojh329/rttys": unsupported_versions: found 1 (want none)'
     - lint: 'modules[0] "github.com/zhaojh329/rttys": version 4.0.0 does not exist'
     - lint: 'summary: must begin with a capital letter'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/zhaojh329/rttys")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-5m6c-jp6f-2vcv.yaml b/internal/genericosv/testdata/yaml/GHSA-5m6c-jp6f-2vcv.yaml
index 632e7b4..28b211c 100644
--- a/internal/genericosv/testdata/yaml/GHSA-5m6c-jp6f-2vcv.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-5m6c-jp6f-2vcv.yaml
@@ -23,3 +23,4 @@
 notes:
     - lint: 'modules[0] "github.com/oauth2-proxy/oauth2-proxy": 2 versions do not exist: 5.1.1, 6.0.0'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/oauth2-proxy/oauth2-proxy")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-627p-rr78-99rj.yaml b/internal/genericosv/testdata/yaml/GHSA-627p-rr78-99rj.yaml
index 288fb61..671a9e9 100644
--- a/internal/genericosv/testdata/yaml/GHSA-627p-rr78-99rj.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-627p-rr78-99rj.yaml
@@ -69,3 +69,4 @@
     - lint: 'modules[0] "github.com/concourse/concourse": 4 versions do not exist: 6.3.0, 6.3.1, 6.4.0, 6.4.1'
     - lint: 'modules[1] "github.com/concourse/dex": 4 versions do not exist: 6.3.0, 6.3.1, 6.4.0, 6.4.1'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/concourse/concourse")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-66p8-j459-rq63.yaml b/internal/genericosv/testdata/yaml/GHSA-66p8-j459-rq63.yaml
index ade0fca..5791009 100644
--- a/internal/genericosv/testdata/yaml/GHSA-66p8-j459-rq63.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-66p8-j459-rq63.yaml
@@ -44,4 +44,5 @@
     - fix: https://github.com/pterodactyl/wings/commit/429ac62dba22997a278bc709df5ac00a5a25d83d
 notes:
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/pterodactyl/wings")'
     - lint: 'summary: too long (found 131 characters, want <=100)'
diff --git a/internal/genericosv/testdata/yaml/GHSA-69v6-xc2j-r2jf.yaml b/internal/genericosv/testdata/yaml/GHSA-69v6-xc2j-r2jf.yaml
index 2af66be..9df2fb2 100644
--- a/internal/genericosv/testdata/yaml/GHSA-69v6-xc2j-r2jf.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-69v6-xc2j-r2jf.yaml
@@ -43,3 +43,4 @@
     - lint: 'modules[1] "github.com/ethereum/go-ethereum": packages[0] "github.com/ethereum/go-ethereum/core/vm": at least one of vulnerable_at and skip_fix must be set'
     - lint: 'modules[1] "github.com/ethereum/go-ethereum": version 1.19.7 does not exist'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/ethereum/go-ethereum")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-6qfg-8799-r575.yaml b/internal/genericosv/testdata/yaml/GHSA-6qfg-8799-r575.yaml
index f8a73c4..bd4b88e 100644
--- a/internal/genericosv/testdata/yaml/GHSA-6qfg-8799-r575.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-6qfg-8799-r575.yaml
@@ -33,3 +33,5 @@
     - report: https://github.com/kubernetes/kubernetes/issues/87773
     - fix: https://github.com/kubernetes/kubernetes/pull/82143
     - web: https://groups.google.com/d/msg/kubernetes-announce/YYtEFdFimZ4/nZnOezZuBgAJ
+notes:
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/kubernetes/kubernetes")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-6rg3-8h8x-5xfv.yaml b/internal/genericosv/testdata/yaml/GHSA-6rg3-8h8x-5xfv.yaml
index f7e83fd..f3b2f7b 100644
--- a/internal/genericosv/testdata/yaml/GHSA-6rg3-8h8x-5xfv.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-6rg3-8h8x-5xfv.yaml
@@ -28,4 +28,5 @@
 references:
     - advisory: https://github.com/pterodactyl/wings/security/advisories/GHSA-6rg3-8h8x-5xfv
 notes:
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/pterodactyl/wings")'
     - lint: 'summary: too long (found 110 characters, want <=100)'
diff --git a/internal/genericosv/testdata/yaml/GHSA-7943-82jg-wmw5.yaml b/internal/genericosv/testdata/yaml/GHSA-7943-82jg-wmw5.yaml
index bbbbac0..3596a12 100644
--- a/internal/genericosv/testdata/yaml/GHSA-7943-82jg-wmw5.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-7943-82jg-wmw5.yaml
@@ -130,3 +130,4 @@
 notes:
     - lint: 'modules[0] "github.com/argoproj/argo-cd": version 2.2.11 does not exist'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/argoproj/argo-cd")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-7fxj-fr3v-r9gj.yaml b/internal/genericosv/testdata/yaml/GHSA-7fxj-fr3v-r9gj.yaml
index 5d4c177..9145d7b 100644
--- a/internal/genericosv/testdata/yaml/GHSA-7fxj-fr3v-r9gj.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-7fxj-fr3v-r9gj.yaml
@@ -26,3 +26,4 @@
 notes:
     - lint: 'modules[0] "github.com/pingcap/tidb": unsupported_versions: found 2 (want none)'
     - lint: 'modules[0] "github.com/pingcap/tidb": version 6.2.0 does not exist'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/pingcap/tidb")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-9689-rx4v-cqgc.yaml b/internal/genericosv/testdata/yaml/GHSA-9689-rx4v-cqgc.yaml
index 7fd6e28..6980554 100644
--- a/internal/genericosv/testdata/yaml/GHSA-9689-rx4v-cqgc.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-9689-rx4v-cqgc.yaml
@@ -27,3 +27,4 @@
 notes:
     - lint: 'modules[0] "github.com/concourse/concourse": 5 versions do not exist: 5.2.8, 5.3.0, 5.5.10, 5.6.0, 5.8.1'
     - lint: 'modules[0] "github.com/concourse/concourse": packages[0] "github.com/concourse/concourse/skymarshal/skyserver": at least one of vulnerable_at and skip_fix must be set'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/concourse/concourse")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-cf7g-cm7q-rq7f.yaml b/internal/genericosv/testdata/yaml/GHSA-cf7g-cm7q-rq7f.yaml
index fc78087..d3e9818 100644
--- a/internal/genericosv/testdata/yaml/GHSA-cf7g-cm7q-rq7f.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-cf7g-cm7q-rq7f.yaml
@@ -21,3 +21,4 @@
 notes:
     - lint: 'modules[0] "github.com/drakkan/sftpgo": version 2.3.5 does not exist'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/drakkan/sftpgo")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-g5gj-9ggf-9vmq.yaml b/internal/genericosv/testdata/yaml/GHSA-g5gj-9ggf-9vmq.yaml
index c09fecb..a7d87d3 100644
--- a/internal/genericosv/testdata/yaml/GHSA-g5gj-9ggf-9vmq.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-g5gj-9ggf-9vmq.yaml
@@ -26,3 +26,4 @@
     - web: https://www.debian.org/security/2022/dsa-5041
 notes:
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/cloudflare/cfrpki")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-g9wh-3vrx-r7hg.yaml b/internal/genericosv/testdata/yaml/GHSA-g9wh-3vrx-r7hg.yaml
index 01d8c9b..c5bb973 100644
--- a/internal/genericosv/testdata/yaml/GHSA-g9wh-3vrx-r7hg.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-g9wh-3vrx-r7hg.yaml
@@ -25,3 +25,4 @@
     - web: https://www.debian.org/security/2022/dsa-5041
 notes:
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/cloudflare/cfrpki")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-hjv9-hm2f-rpcj.yaml b/internal/genericosv/testdata/yaml/GHSA-hjv9-hm2f-rpcj.yaml
index 3e6728c..7b9c46e 100644
--- a/internal/genericosv/testdata/yaml/GHSA-hjv9-hm2f-rpcj.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-hjv9-hm2f-rpcj.yaml
@@ -31,3 +31,4 @@
     - web: https://security.netapp.com/advisory/ntap-20230413-0001/
 notes:
     - lint: 'modules[0] "github.com/grafana/grafana": 6 versions do not exist: 8.1.0, 8.5.21, 9.0.0, 9.2.13, 9.3.0, 9.3.8'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/grafana/grafana")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-hv53-vf5m-8q94.yaml b/internal/genericosv/testdata/yaml/GHSA-hv53-vf5m-8q94.yaml
index 6fbf2aa..79d9767 100644
--- a/internal/genericosv/testdata/yaml/GHSA-hv53-vf5m-8q94.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-hv53-vf5m-8q94.yaml
@@ -55,3 +55,4 @@
 notes:
     - lint: 'modules[0] "github.com/personnummer/go": version 3.0.1 does not exist'
     - lint: 'summary: must begin with a capital letter'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/personnummer/go")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-jh36-q97c-9928.yaml b/internal/genericosv/testdata/yaml/GHSA-jh36-q97c-9928.yaml
index 098acc2..186d7c7 100644
--- a/internal/genericosv/testdata/yaml/GHSA-jh36-q97c-9928.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-jh36-q97c-9928.yaml
@@ -31,3 +31,5 @@
     - web: https://github.com/kubernetes/kubernetes/issues/113757
     - web: https://groups.google.com/g/kubernetes-security-announce/c/VyPOxF7CIbA
     - web: https://security.netapp.com/advisory/ntap-20230505-0007/
+notes:
+    - lint: 'summary: must contain an affected module or package path (e.g. "k8s.io/kubernetes")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-jmp2-wc4p-wfh2.yaml b/internal/genericosv/testdata/yaml/GHSA-jmp2-wc4p-wfh2.yaml
index 595b468..397c72b 100644
--- a/internal/genericosv/testdata/yaml/GHSA-jmp2-wc4p-wfh2.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-jmp2-wc4p-wfh2.yaml
@@ -60,4 +60,5 @@
     - web: https://github.com/mutagen-io/mutagen/releases/tag/v0.17.1
 notes:
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/mutagen-io/mutagen")'
     - lint: 'summary: too long (found 111 characters, want <=100)'
diff --git a/internal/genericosv/testdata/yaml/GHSA-pg5p-wwp8-97g8.yaml b/internal/genericosv/testdata/yaml/GHSA-pg5p-wwp8-97g8.yaml
index 13a018a..f221579 100644
--- a/internal/genericosv/testdata/yaml/GHSA-pg5p-wwp8-97g8.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-pg5p-wwp8-97g8.yaml
@@ -64,3 +64,4 @@
     - lint: 'modules[0] "github.com/cilium/cilium": unsupported_versions: found 1 (want none)'
     - lint: 'modules[0] "github.com/cilium/cilium": versions: introduced and fixed versions must alternate'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/cilium/cilium")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-pmfr-63c2-jr5c.yaml b/internal/genericosv/testdata/yaml/GHSA-pmfr-63c2-jr5c.yaml
index ab3ef0a..0cac627 100644
--- a/internal/genericosv/testdata/yaml/GHSA-pmfr-63c2-jr5c.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-pmfr-63c2-jr5c.yaml
@@ -70,3 +70,4 @@
 notes:
     - lint: 'modules[0] "github.com/sylabs/singularity": version 3.6.0 does not exist'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/sylabs/singularity")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-vp35-85q5-9f25.yaml b/internal/genericosv/testdata/yaml/GHSA-vp35-85q5-9f25.yaml
index 2773ad9..c493d91 100644
--- a/internal/genericosv/testdata/yaml/GHSA-vp35-85q5-9f25.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-vp35-85q5-9f25.yaml
@@ -108,3 +108,5 @@
     - web: https://github.blog/2022-10-17-git-security-vulnerabilities-announced/
     - web: https://github.com/moby/moby/releases/tag/v20.10.20
     - web: https://lore.kernel.org/git/xmqq4jw1uku5.fsf@gitster.g/T/#u
+notes:
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/moby/moby")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-w4xh-w33p-4v29.yaml b/internal/genericosv/testdata/yaml/GHSA-w4xh-w33p-4v29.yaml
index 9383164..95901af 100644
--- a/internal/genericosv/testdata/yaml/GHSA-w4xh-w33p-4v29.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-w4xh-w33p-4v29.yaml
@@ -31,3 +31,4 @@
     - lint: 'modules[0] "github.com/git-lfs/git-lfs": version 2.1.1-0.20170519163204-f913f5f9c7c6 does not exist'
     - lint: 'modules[1] "github.com/git-lfs/git-lfs": packages[0] "github.com/git-lfs/git-lfs/lfsapi": at least one of vulnerable_at and skip_fix must be set'
     - lint: 'modules[1] "github.com/git-lfs/git-lfs": version 2.1.1-0.20170519163204-f913f5f9c7c6 does not exist'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/git-lfs/git-lfs")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-xmg8-99r8-jc2j.yaml b/internal/genericosv/testdata/yaml/GHSA-xmg8-99r8-jc2j.yaml
index ef88a19..aa31e84 100644
--- a/internal/genericosv/testdata/yaml/GHSA-xmg8-99r8-jc2j.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-xmg8-99r8-jc2j.yaml
@@ -74,3 +74,4 @@
 notes:
     - lint: 'modules[0] "github.com/argoproj/argo-cd": version 2.1.15 does not exist'
     - lint: 'references: too many advisories (found 2, want <=1)'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/argoproj/argo-cd")'
diff --git a/internal/genericosv/testdata/yaml/GHSA-xx9w-464f-7h6f.yaml b/internal/genericosv/testdata/yaml/GHSA-xx9w-464f-7h6f.yaml
index 60e154f..d1fd09b 100644
--- a/internal/genericosv/testdata/yaml/GHSA-xx9w-464f-7h6f.yaml
+++ b/internal/genericosv/testdata/yaml/GHSA-xx9w-464f-7h6f.yaml
@@ -43,3 +43,4 @@
     - advisory: https://github.com/goharbor/harbor/security/advisories/GHSA-xx9w-464f-7h6f
 notes:
     - lint: 'modules[0] "github.com/goharbor/harbor": version 1.0.0 does not exist'
+    - lint: 'summary: must contain an affected module or package path (e.g. "github.com/goharbor/harbor")'
diff --git a/internal/report/lint.go b/internal/report/lint.go
index da80704..c585b5f 100644
--- a/internal/report/lint.go
+++ b/internal/report/lint.go
@@ -286,6 +286,8 @@
 	}
 	if hasTODO(summary) {
 		l.Error(hasTODOErr)
+		// No need to keep linting, as this is likely a placeholder value.
+		return
 	}
 	if ln := len(summary); ln > 100 {
 		l.Errorf("too long (found %d characters, want <=100)", ln)
@@ -301,6 +303,60 @@
 			l.Error("must begin with a capital letter")
 		}
 	}
+
+	// Summary must contain one of the listed module or package
+	// paths. (Except in the "std" module, where a specific package
+	// must be mentioned).
+	// If there are no such paths listed in the report at all,
+	// another lint will complain, so reduce noise by not erroring here.
+	if paths := r.nonStdPaths(); len(paths) > 0 {
+		if ok := containsPath(l, summary, paths); !ok {
+			l.Errorf("must contain an affected module or package path (e.g. %q)", paths[0])
+		}
+	}
+}
+
+// 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 "/"
+// and is a prefix of a path, the function returns true. This gives us a
+// workaround for reports that affect a lot of modules and/or have very long
+// module paths.
+func containsPath(l *linter, summary string, paths []string) bool {
+	if len(paths) == 0 {
+		return false
+	}
+
+	for _, possiblePath := range strings.Fields(summary) {
+		possiblePath := strings.TrimRight(possiblePath, ":,.")
+		for _, path := range paths {
+			if possiblePath == path {
+				return true
+			}
+			if strings.Contains(possiblePath, "/") &&
+				strings.HasPrefix(path, possiblePath) {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+// nonStdPaths returns all module and package paths (except "std")
+// mentioned in the report.
+func (r *Report) nonStdPaths() (paths []string) {
+	for _, m := range r.Modules {
+		if m.Module != "" && m.Module != stdlib.ModulePath {
+			paths = append(paths, m.Module)
+		}
+		for _, p := range m.Packages {
+			if p.Package != "" {
+				paths = append(paths, p.Package)
+			}
+		}
+	}
+	return paths
 }
 
 func (r *Report) IsExcluded() bool {
diff --git a/internal/report/lint_test.go b/internal/report/lint_test.go
index 4045f09..c6aee64 100644
--- a/internal/report/lint_test.go
+++ b/internal/report/lint_test.go
@@ -287,6 +287,30 @@
 			wantNumLints: 1,
 		},
 		{
+			name: "summary_no_path",
+			desc: "The summary must contain a module or package path listed in the report.",
+			report: validReport(func(r *Report) {
+				r.Summary = "This summary doesn't have a path"
+			}),
+			wantNumLints: 1,
+		},
+		{
+			name: "summary_path_prefix",
+			desc: "Summary may contain a prefix of a module or package path that is mentioned at least twice in a report. This is a workaround for reports that affect many modules.",
+			report: validReport(func(r *Report) {
+				r.Modules = append(r.Modules, &Module{
+					Module:       "example.com/module/example/v2",
+					VulnerableAt: "1.0.0",
+				})
+				r.Modules = append(r.Modules, &Module{
+					Module:       "example.com/module/example/v3",
+					VulnerableAt: "1.0.0",
+				})
+				r.Summary = "This summary is about example.com/module/example"
+			}),
+			// No lints.
+		},
+		{
 			name: "no_package_path",
 			desc: "All packages must have a path.",
 			report: validReport(func(r *Report) {
diff --git a/internal/report/testdata/lint/TestLintOffline/summary_no_path.txtar b/internal/report/testdata/lint/TestLintOffline/summary_no_path.txtar
new file mode 100644
index 0000000..934d273
--- /dev/null
+++ b/internal/report/testdata/lint/TestLintOffline/summary_no_path.txtar
@@ -0,0 +1,21 @@
+Copyright 2023 The Go Authors. All rights reserved.
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file.
+
+Test: TestLintOffline/summary_no_path
+Description: The summary must contain a module or package path listed in the report.
+
+-- data/reports/GO-0000-0000.yaml --
+id: GO-0000-0000
+modules:
+    - module: golang.org/x/net
+      vulnerable_at: 1.2.3
+      packages:
+        - package: golang.org/x/net/http2
+summary: This summary doesn't have a path
+description: description
+cves:
+    - CVE-1234-0000
+
+-- golden --
+summary: must contain an affected module or package path (e.g. "golang.org/x/net")
diff --git a/internal/report/testdata/lint/TestLintOffline/summary_path_prefix.txtar b/internal/report/testdata/lint/TestLintOffline/summary_path_prefix.txtar
new file mode 100644
index 0000000..9d894cd
--- /dev/null
+++ b/internal/report/testdata/lint/TestLintOffline/summary_path_prefix.txtar
@@ -0,0 +1,25 @@
+Copyright 2023 The Go Authors. All rights reserved.
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file.
+
+Test: TestLintOffline/summary_path_prefix
+Description: Summary may contain a prefix of a module or package path that is mentioned at least twice in a report. This is a workaround for reports that affect many modules.
+
+-- data/reports/GO-0000-0000.yaml --
+id: GO-0000-0000
+modules:
+    - module: golang.org/x/net
+      vulnerable_at: 1.2.3
+      packages:
+        - package: golang.org/x/net/http2
+    - module: example.com/module/example/v2
+      vulnerable_at: 1.0.0
+    - module: example.com/module/example/v3
+      vulnerable_at: 1.0.0
+summary: This summary is about example.com/module/example
+description: description
+cves:
+    - CVE-1234-0000
+
+-- golden --
+