govulncheck is the Go team's vulnerability scanner, and its selling point is reachability: it reports a vulnerability as affecting your code only if your code calls a vulnerable function. We measured what it has to work with in 2026. The Go vulnerability database picks up 99.1% of GitHub's Go advisories, but a median 10.7 days later, and a third take more than a month. And 84.7% of the entries published this year have no package or symbol data at all. For those entries, govulncheck treats every function in the module as vulnerable, so calling any of them is enough to be flagged.
This post is for Go teams that rely on govulncheck in CI, and for anyone deciding how much weight to put on "your code doesn't call this". It covers how the database is built, coverage and delay across 1,982 advisories, what missing symbol data does to reachability, with a real govulncheck run, and the script we used, so you can rerun the numbers.
The short answer
Between 1 January and 30 September 2026, GitHub published 1,982 reviewed advisories for Go modules. By 16 October, 1,965 (99.1%) were in the Go vulnerability database, 11 had been deliberately excluded, and 6 were missing. The median delay from GitHub's advisory to the Go entry was 10.7 days, 33.2% took over 30 days, and severity made no difference: critical advisories had a median of 10.9 days. Of 2,127 Go database entries published in 2026, 1,802 (84.7%) carry no package or symbol information. govulncheck treats those as affecting every symbol in every package of the module, so its reachability filtering doesn't narrow them at all.
How the Go vulnerability database is built
The Go vulnerability database is what govulncheck downloads from vuln.go.dev. A worker files new GitHub advisories and CVEs as issues, and the Go security team triages them. The database's format documentation defines two states for a published entry:
- UNREVIEWED entries are "generally auto-generated or nearly so". Their details haven't been verified, and "no attempt has been made to determine packages or symbols".
- REVIEWED entries are ones where the team "made a good faith effort to determine correct affected versions, packages, and symbols".
Some advisories are excluded on purpose, for example when the affected code isn't Go or isn't importable. Entries are published in batches. In 2026 the longest gaps between batches ran 12 to 16 days. The most recent batch when we ran our analysis was published on 8 October at 22:31 UTC.
Coverage is close to complete
We matched every GitHub-reviewed Go advisory published from January to September 2026 against the Go database's aliases, using OSV's export of both, and checked the excluded list in the vulndb repository:
| Status on 16 Oct 2026 | Advisories | Share |
|---|---|---|
| In the Go database | 1,965 | 99.1% |
| Excluded by the Go team | 11 | 0.6% |
| Missing | 6 | 0.3% |
Seven exclusions were marked NOT_GO_CODE and four EFFECTIVELY_PRIVATE. The six missing advisories, four high and two moderate, cover bytebase, the AWS EFS CSI driver, Talos, Pomerium and two Apache Thrift advisories. None of them appear anywhere in the vulndb repository. That's a short list, and it's the least interesting finding. If GitHub publishes an advisory for a Go module, govulncheck will almost certainly get it.
Delay is the real gap
Getting it is one thing. When is another:
| Delay from GitHub advisory to Go entry | Value |
|---|---|
| Median | 10.7 days |
| 25th percentile | 5.0 days |
| 75th percentile | 44.1 days |
| 90th percentile | 72.0 days |
| Within 1 day | 6.9% |
| Over 7 days | 61.9% |
| Over 30 days | 33.2% |
Severity doesn't change it. The median was 10.9 days for critical advisories, 10.7 for high, 10.1 for moderate and 13.0 for low. There's no fast lane.
You can see it this week. GitHub added eight kin-openapi advisories on 15 October. On 16 October govulncheck didn't know about any of them, because the database hadn't published a batch since 8 October.
What missing symbols do to reachability
The bigger finding is about symbols. Of the 2,127 entries the database published in 2026, 356 were REVIEWED and 1,771 UNREVIEWED. 1,802 (84.7%) list no packages or symbols at all.
govulncheck's source code is explicit about how it handles that. In internal/vulncheck/vulncheck.go, an entry with no packages means "all symbols of all packages are vulnerable". So for 85% of this year's entries, reachability comes down to whether your code calls anything at all in the affected module.
To see what that looks like, we built a short program that imports only the logger package from nats-server and writes one log line. We required nats-server v2.12.2 and ran govulncheck v1.9.0 on 16 October:
package main
import "github.com/nats-io/nats-server/v2/logger"
func main() {
l := logger.NewStdLogger(true, false, false, false, false)
l.Noticef("hello")
}
$ govulncheck ./...
=== Symbol Results ===
Vulnerability #1: GO-2026-4841
NATS: Pre-auth remote server crash via WebSocket frame length overflow in
wsRead in github.com/nats-io/nats-server
More info: https://pkg.go.dev/vuln/GO-2026-4841
Module: github.com/nats-io/nats-server/v2
Found in: github.com/nats-io/nats-server/v2@v2.12.2
Fixed in: github.com/nats-io/nats-server/v2@v2.12.5
Example traces found:
#1: main.go:7:11: demo.main calls logger.Logger.Noticef
#2: main.go:6:26: demo.main calls logger.NewStdLogger
#3: main.go:3:8: demo.init calls logger.init
[13 more entries with the same three traces: GO-2026-4837, GO-2026-4836, GO-2026-4835, GO-2026-4834, GO-2026-4833, GO-2026-4832, GO-2026-4831, GO-2026-4830, GO-2026-4829, GO-2026-4828, GO-2026-4827, GO-2026-4826, GO-2026-4533]
Your code is affected by 14 vulnerabilities from 1 module.
This scan also found 1 vulnerability in packages you import and 12
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
All 14 are UNREVIEWED entries with no symbol data. They concern WebSocket frame handling, MQTT access control, leafnode panics and similar server code, none of which a log call touches. Each comes with the same evidence: demo.main calls logger.NewStdLogger. The scan also listed one standard library vulnerability as imported but not called. That entry has symbol data, so govulncheck could tell.
This is conservative. It errs towards flagging rather than missing. But if your process treats "called" as "urgent", the label means less than it looks for most third-party entries.
Who it affects most
The modules with the most entries lacking symbol data are applications: Mattermost (117 entries on its oldest module path alone), SiYuan's kernel (100), Gitea, Gogs, Traefik and Grafana. Few teams import those as libraries, so for most codebases the effect is smaller than 84.7% suggests.
A large share of reviewed entries cover code the Go team owns. Of the 356 reviewed entries published in 2026, 101 affect the standard library, the toolchain or golang.org/x modules. If you embed a server as a library, such as nats-server in tests, Traefik's packages, or an SDK that lives inside an application module, expect most of its advisories to show up as "called" whether or not they are.
Rerun the numbers
This script downloads OSV's Go export into memory, matches GitHub advisories to Go entries, and reports coverage, delay and symbol data. With --vulndb pointing at a clone of the vulndb repository, it also separates deliberate exclusions from missing advisories. It uses only the Python standard library.
#!/usr/bin/env python3
"""How much of GitHub's Go advisory data reaches the Go vulnerability database
(what govulncheck reads), how long it takes, and how much of it has symbol data.
python3 go_vulndb_coverage.py [--vulndb /path/to/vulndb/clone] [--from 2026-01-01] [--to 2026-10-01]
Downloads OSV's Go export (GHSA-* and GO-* entries) into memory. With --vulndb,
also reads data/excluded from a clone of https://go.googlesource.com/vulndb to
separate deliberately excluded advisories from missing ones.
Standard library only.
"""
import argparse, collections, datetime as dt, glob, io, json, re, statistics, urllib.request, zipfile
OSV_GO = "https://osv-vulnerabilities.storage.googleapis.com/Go/all.zip"
def ts(s):
return dt.datetime.fromisoformat(s.replace("Z", "+00:00"))
def has_packages(entry):
return any((a.get("ecosystem_specific") or {}).get("imports") for a in entry["affected"])
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--vulndb")
ap.add_argument("--from", dest="start", default="2026-01-01")
ap.add_argument("--to", dest="end", default="2026-10-01")
args = ap.parse_args()
start, end = (ts(d + "T00:00:00Z") for d in (args.start, args.end))
data = urllib.request.urlopen(OSV_GO, timeout=120).read()
go, ghsa = {}, {}
with zipfile.ZipFile(io.BytesIO(data)) as z:
for name in z.namelist():
e = json.loads(z.read(name))
if e["id"].startswith("GO-"):
go[e["id"]] = e
elif e["id"].startswith("GHSA-"):
ghsa[e["id"]] = e
last = max(ts(e["published"]) for e in go.values())
print(f"OSV Go export: {len(go)} GO entries, {len(ghsa)} GHSA entries; newest GO entry published {last:%Y-%m-%d %H:%M} UTC")
in_db = collections.defaultdict(list)
for e in go.values():
for a in e.get("aliases", []):
in_db[a].append(e)
excluded = {}
if args.vulndb:
for f in glob.glob(f"{args.vulndb}/data/excluded/*.yaml"):
text = open(f).read()
reason = re.search(r"excluded:\s*(\S+)", text).group(1)
for g in re.findall(r"GHSA-\w{4}-\w{4}-\w{4}", text):
excluded[g] = reason
# 1. Coverage and delay for GitHub-reviewed Go advisories in the window
window = [e for e in ghsa.values() if start <= ts(e["published"]) < end and not e.get("withdrawn")]
lags, status, reasons = [], collections.Counter(), collections.Counter()
by_sev = collections.defaultdict(list)
for e in window:
sev = (e.get("database_specific") or {}).get("severity", "UNKNOWN")
if e["id"] in in_db:
first = min(ts(g["published"]) for g in in_db[e["id"]])
lag = (first - ts(e["published"])).total_seconds() / 86400
lags.append(lag)
by_sev[sev].append(lag)
status["in the Go database"] += 1
elif e["id"] in excluded:
status["excluded by the Go team"] += 1
reasons[excluded[e["id"]]] += 1
else:
status["missing"] += 1
n = len(window)
print(f"\nGitHub Go advisories published {args.start} to {args.end}: {n}")
for k, v in status.most_common():
print(f" {k:26} {v:5} {100 * v / n:5.1f}%")
if reasons:
print(" exclusion reasons:", ", ".join(f"{k} {v}" for k, v in reasons.most_common()))
q = statistics.quantiles(lags, n=20)
share = lambda f: 100 * sum(1 for x in lags if f(x)) / len(lags)
print(f"\nDelay from GitHub advisory to Go database entry (days):")
print(f" median {statistics.median(lags):.1f}, 25th pct {q[4]:.1f}, 75th pct {q[14]:.1f}, 90th pct {q[17]:.1f}")
print(f" within 1 day {share(lambda x: x <= 1):.1f}%, over 7 days {share(lambda x: x > 7):.1f}%, over 30 days {share(lambda x: x > 30):.1f}%")
for sev in ("CRITICAL", "HIGH", "MODERATE", "LOW"):
if by_sev[sev]:
print(f" {sev:9} n={len(by_sev[sev]):4} median {statistics.median(by_sev[sev]):.1f}")
# 2. Symbol data in GO entries published in the same year
year = [e for e in go.values() if ts(e["published"]).year == start.year]
review = collections.Counter((e.get("database_specific") or {}).get("review_status") for e in year)
without = sum(1 for e in year if not has_packages(e))
print(f"\nGO entries published in {start.year}: {len(year)} ({review['REVIEWED']} REVIEWED, {review['UNREVIEWED']} UNREVIEWED)")
print(f" with no package or symbol data: {without} ({100 * without / len(year):.1f}%)")
mods = collections.Counter(a["package"]["name"] for e in year if not has_packages(e) for a in e["affected"])
print(" modules with the most such entries:", ", ".join(f"{m} ({c})" for m, c in mods.most_common(5)))
if __name__ == "__main__":
main()
Our run on 16 October:
$ python3 go_vulndb_coverage.py --vulndb vulndb
OSV Go export: 4632 GO entries, 5003 GHSA entries; newest GO entry published 2026-10-08 22:31 UTC
GitHub Go advisories published 2026-01-01 to 2026-10-01: 1982
in the Go database 1965 99.1%
excluded by the Go team 11 0.6%
missing 6 0.3%
exclusion reasons: NOT_GO_CODE 7, EFFECTIVELY_PRIVATE 4
Delay from GitHub advisory to Go database entry (days):
median 10.7, 25th pct 5.0, 75th pct 44.1, 90th pct 72.0
within 1 day 6.9%, over 7 days 61.9%, over 30 days 33.2%
CRITICAL n= 246 median 10.9
HIGH n= 753 median 10.7
MODERATE n= 811 median 10.1
LOW n= 155 median 13.0
GO entries published in 2026: 2127 (356 REVIEWED, 1771 UNREVIEWED)
with no package or symbol data: 1802 (84.7%)
modules with the most such entries: github.com/mattermost/mattermost-server (117), github.com/siyuan-note/siyuan/kernel (100), github.com/mattermost/mattermost-server/v5 (57), github.com/mattermost/mattermost-server/v6 (57), github.com/mattermost/mattermost/server/v8 (57)
What to do
- Keep running govulncheck. Its coverage is close to complete, and for the standard library and
golang.org/xmodules its symbol data is the best available. - Add a source with no delay. Tools that read GitHub's advisory database or OSV's GHSA entries directly see new advisories a median of about 11 days sooner. Run one of them alongside govulncheck, at least for direct dependencies. Our post on scanning Go codebases with govulncheck covers the basics.
- Read "called" findings in context. Open the entry. If it has no
importsdata, check whether the vulnerable feature is one you use before you treat it as reachable. - Don't treat "not called" as all-clear for new advisories. A vulnerability that isn't in the database yet doesn't appear at all, in any section.
- Help with the review. If you know which packages and symbols an entry affects, the vulndb repository accepts issues and changes. Better data there helps every govulncheck user.
The fair view
None of this is a criticism of the Go security team. Unreviewed entries are a deliberate trade-off. Publishing a version-only entry quickly is better than publishing a precise one months later, and conservative matching is the right default when symbol data is missing. Coverage of 99.1% is excellent. The 10.7-day median reflects batch publishing by a small team, not neglect, and more than a quarter of this year's reviewed entries went to the code the Go team owns.
The point is calibration. "Reachability-aware" describes what govulncheck does with symbol data, and in 2026 most entries don't have any. Treat its findings as accurate on versions, often precise on the standard library, and as module-level for most third-party code, and plan for the 11 days before a new advisory reaches it at all.