Safeguard
Supply Chain Security•

cargo audit vs Dependabot in 2026: Rust's Two Advisory Databases Have Drifted Apart

Since August, 115 of 117 new RustSec advisories never reached GitHub's reviewed database, so Dependabot misses them. The 2026 data and a lockfile checker.

Safeguard Security Team
Security Research
13 min read

If your Rust project relies on Dependabot alone, it has been missing most new Rust advisories since August. Of the 117 vulnerability advisories RustSec added from August to mid-October 2026, 115 weren't in GitHub's reviewed advisory database on 17 October, and Dependabot only alerts on reviewed advisories. The gap runs the other way too: 364 crates.io advisories GitHub reviewed this year have no RustSec entry, so cargo audit and cargo deny don't report them.

This guide is for Rust teams choosing how to scan Cargo.lock in CI. It covers where each tool gets its data, what the two databases held for 2026, a real sandbox escape that only one of them listed, and a script that shows which source reports each advisory in your lockfile.

The short answer

cargo audit and cargo deny read the RustSec Advisory Database. Dependabot reads GitHub's reviewed advisories. In 2026 the two have drifted apart: on 17 October, 185 of the 310 RustSec vulnerability advisories added this year had no GitHub reviewed entry, including almost every one since August, and 364 of the 472 crates.io advisories GitHub reviewed this year had no RustSec entry. Run a RustSec-based check and Dependabot, or use a scanner that queries OSV.dev, which carries both. Don't read a quiet Dependabot tab as a clean Rust dependency tree.

Where each tool gets its data

ToolAdvisory source
cargo auditRustSec Advisory Database
cargo deny check advisoriesRustSec by default (db-urls), configurable
Dependabot alertsGitHub Advisory Database, reviewed advisories only
OSV-Scanner and other OSV.dev clientsOSV.dev, which includes both RustSec and GitHub reviewed advisories

GitHub lists RustSec as one of its sources, and RustSec entries often carry a GHSA alias, so it's natural to assume the two stay in sync. GitHub's own documentation explains why they can't fully: advisories arrive in its database as unreviewed, and "Dependabot doesn't create Dependabot alerts for unreviewed advisories". Until a GitHub curator reviews an imported advisory, Dependabot users don't see it.

What the two databases held in 2026

OSV publishes a dump of every crates.io advisory from both sources. RustSec entries carry the disclosure date rather than the day they were added, so the script below takes the add date from the RustSec repository's git history instead. It matches entries across databases by ID, GHSA alias or CVE, and ignores RustSec's informational notices for unmaintained or unsound crates. Counting by the date each entry was added is why our 2026 total is 310: counted by the date printed in each advisory, it would be 255, because RustSec added some older advisories this year:

#!/usr/bin/env python3
"""Compare RustSec and GitHub's reviewed advisory database for crates.io advisories.

Inputs:
  crates.zip   OSV's crates.io dump:
               https://osv-vulnerabilities.storage.googleapis.com/crates.io/all.zip
  added.txt    when each RUSTSEC id first appeared in rustsec/advisory-db, from:
               git log --no-renames --diff-filter=A --format='C %cI' --name-only -- crates
Usage: rustsec_vs_github.py crates.zip added.txt [YEAR]
"""
import json
import statistics
import sys
import zipfile
from datetime import datetime

zpath, apath = sys.argv[1], sys.argv[2]
year = int(sys.argv[3]) if len(sys.argv) > 3 else 2026
z = zipfile.ZipFile(zpath)
recs = {d["id"]: d for d in (json.loads(z.read(n)) for n in z.namelist())}
added, when = {}, None
for line in open(apath):  # git log prints newest first, so the last date seen is the first add
    line = line.strip()
    if line.startswith("C "):
        when = line[2:]
    elif line.endswith(".md"):
        added[line.rsplit("/", 1)[-1][:-3]] = when


def ts(s):
    return datetime.fromisoformat(s.replace("Z", "+00:00"))


def informational(d):
    return any((a.get("database_specific") or {}).get("informational") for a in d.get("affected", []))


def rustsec_seen(d):  # when cargo audit / cargo deny could first see it
    return ts(added[d["id"]]) if d["id"] in added else ts(d["published"])


def github_seen(d):  # when Dependabot could first see it
    return ts((d.get("database_specific") or {}).get("github_reviewed_at") or d["published"])


def keys(d):
    return {d["id"], *d.get("aliases", [])}


rustsec = [d for i, d in recs.items() if i.startswith("RUSTSEC-") and not informational(d)]
ghsa = [d for i, d in recs.items() if i.startswith("GHSA-")]
by_key = {}
for g in ghsa:
    for k in keys(g):
        by_key.setdefault(k, g)

pairs, rs_only = [], []
for r in rustsec:
    g = next((by_key[k] for k in keys(r) if k in by_key), None)
    (pairs.append((r, g)) if g else rs_only.append(r))
matched = {g["id"] for _, g in pairs}

in_year = lambda dt: dt.year == year
pair_of = {r["id"]: g for r, g in pairs}
rs_year = [r for r in rustsec if in_year(rustsec_seen(r))]
gh_year = [g for g in ghsa if in_year(github_seen(g)) and not g.get("withdrawn")]
rs_matched = [r for r in rs_year if r["id"] in pair_of]
gh_unmatched = [g for g in gh_year if g["id"] not in matched]
print(f"{year}: RustSec vulnerability advisories added: {len(rs_year)}")
print(f"  with a GitHub reviewed entry: {len(rs_matched)}; RustSec only: {len(rs_year) - len(rs_matched)}")
print(f"{year}: crates.io advisories added to GitHub's reviewed database: {len(gh_year)}")
print(f"  with a RustSec entry: {len(gh_year) - len(gh_unmatched)}; GitHub only: {len(gh_unmatched)}")
lag = sorted((github_seen(pair_of[r["id"]]) - rustsec_seen(r)).total_seconds() / 86400 for r in rs_matched)
rs_first = [x for x in lag if x > 1]
gh_first = [-x for x in lag if x < -1]
print(f"Timing for the {len(lag)} RustSec {year} advisories GitHub also has:")
print(f"  RustSec first by more than a day: {len(rs_first)}; GitHub first: {len(gh_first)}; "
      f"within a day: {len(lag) - len(rs_first) - len(gh_first)}")
for name, xs in (("GitHub after RustSec", rs_first), ("RustSec after GitHub", gh_first)):
    if xs:
        print(f"  {name}: median {statistics.median(xs):.0f} days, over 30 days {sum(x > 30 for x in xs)}, "
              f"over 90 days {sum(x > 90 for x in xs)}")
$ curl -O https://osv-vulnerabilities.storage.googleapis.com/crates.io/all.zip && mv all.zip crates.zip
$ git clone https://github.com/rustsec/advisory-db
$ git -C advisory-db log --no-renames --diff-filter=A --format='C %cI' --name-only -- crates > added.txt
$ python3 rustsec_vs_github.py crates.zip added.txt 2026
2026: RustSec vulnerability advisories added: 310
  with a GitHub reviewed entry: 125; RustSec only: 185
2026: crates.io advisories added to GitHub's reviewed database: 472
  with a RustSec entry: 108; GitHub only: 364
Timing for the 125 RustSec 2026 advisories GitHub also has:
  RustSec first by more than a day: 49; GitHub first: 30; within a day: 46
  GitHub after RustSec: median 6 days, over 30 days 16, over 90 days 1
  RustSec after GitHub: median 16 days, over 30 days 11, over 90 days 5

Where the two overlap, the gap is usually short. RustSec went first more often, and when it did, GitHub followed after a median of six days. The problem is the advisories that never crossed over. By month:

Month (2026)RustSec advisories addedNot in GitHub's reviewed database on 17 October
January101
February287
March6838
April296
May170
June235
July1813
August3231
September3231
October (to the 17th)5353

Until July, most RustSec advisories reached GitHub's reviewed database. From August on, almost none did. We can't see GitHub's queue, so we can't say why. But 74 of the 185 RustSec-only entries already carry a GHSA ID, usually because the maintainers published a repository advisory on GitHub. The three we looked up returned a 404 from GitHub's global advisory API.

A sandbox escape only one database had

On 14 October, Wasmtime published GHSA-m9f7-9w4p-gv38, a high-severity filesystem sandbox escape in wasmtime-wasi on macOS and FreeBSD. A guest with write access to any directory in its sandbox could move outside directories into it and read or write them. It's fixed in 49.0.3, 48.0.6 and 36.0.18. RustSec added it the same day as RUSTSEC-2026-0352. On 17 October GitHub's global advisory API returned 404 for that GHSA ID, and the advisory has no CVE yet.

So a team on wasmtime-wasi 48.0.1 that runs cargo audit saw it within hours. A team relying only on Dependabot saw nothing. The reverse happened earlier this year: Wasmtime's CVE-2026-58494, fixed on 24 June, reached RustSec on 29 June but GitHub's reviewed database only on 16 October.

The other direction: GitHub-only advisories

The 364 GitHub-only advisories cluster in a few projects that publish their own repository advisories on GitHub. The largest groups in 2026 are surrealdb (55), coreutils (44), russh (15), deno (14), zebrad (13), deepseek-tui (13) and rustfs (12). Several are applications more than libraries. But plenty of them, russh and the gix crates among them, are dependencies other projects pull in. Those findings reach Dependabot and not cargo audit.

Check which source reports what

This script reads a Cargo.lock, queries OSV.dev for every registry crate and version, and groups each finding by which database carries it. It skips RustSec's informational notices and malware records. It uses only the Python standard library:

#!/usr/bin/env python3
"""List known advisories for a Cargo.lock and which database each comes from.

Queries OSV (api.osv.dev), which carries both RustSec and GitHub's reviewed
advisories, then groups each finding by source:
  RustSec only  -> cargo audit / cargo deny report it, Dependabot doesn't
  GitHub only   -> Dependabot reports it, cargo audit / cargo deny don't
  both
Usage: cargo_lock_sources.py path/to/Cargo.lock
"""
import json
import re
import sys
import tomllib
import urllib.request


def query(batch):
    req = urllib.request.Request("https://api.osv.dev/v1/querybatch", json.dumps({"queries": batch}).encode(),
                                 {"Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req, timeout=60))["results"]


def details(vid):
    with urllib.request.urlopen(f"https://api.osv.dev/v1/vulns/{vid}", timeout=30) as r:
        return json.load(r)


lock = tomllib.loads(open(sys.argv[1]).read())
pkgs = sorted({(p["name"], p["version"]) for p in lock.get("package", [])
               if p.get("source", "").startswith("registry+")})
hits = {}
for i in range(0, len(pkgs), 500):
    chunk = pkgs[i:i + 500]
    res = query([{"package": {"ecosystem": "crates.io", "name": n}, "version": v} for n, v in chunk])
    for (n, v), r in zip(chunk, res):
        for vuln in r.get("vulns", []):
            hits.setdefault((n, v), set()).add(vuln["id"])

groups = {"RustSec only": [], "GitHub only": [], "both": []}
for (n, v), ids in sorted(hits.items()):
    seen = set()
    for vid in sorted(ids):
        if vid in seen or vid.startswith("MAL-"):
            continue
        d = details(vid)
        if any((a.get("database_specific") or {}).get("informational") for a in d.get("affected", [])):
            continue  # unmaintained/unsound notices are not vulnerabilities
        same = {vid, *d.get("aliases", [])} & ids
        seen |= same
        has_rs = any(x.startswith("RUSTSEC-") for x in same)
        has_gh = any(x.startswith("GHSA-") for x in same)
        label = "both" if has_rs and has_gh else ("RustSec only" if has_rs else "GitHub only")
        ref = ", ".join(sorted(same))
        groups[label].append(f"{n} {v}: {ref}: {d.get('summary', '')[:70]}")

print(f"{len(pkgs)} registry crates checked")
for label, rows in groups.items():
    print(f"{label}: {len(rows)}")
    for row in rows:
        print("  " + row)

On a small lockfile built from crate versions common in real projects:

$ python3 cargo_lock_sources.py Cargo.lock
8 registry crates checked
RustSec only: 15
  quinn-proto 0.11.17: RUSTSEC-2026-0338: Remote peer can panic quinn-proto with zero-length STREAM frames durin
  quinn-proto 0.11.17: RUSTSEC-2026-0340: Peer minimum ACK delay can panic quinn-proto when ACK_FREQUENCY transm
  quinn-proto 0.11.17: RUSTSEC-2026-0343: Outgoing datagram buffer accounting can panic the sender in quinn-prot
  rustls 0.23.43: RUSTSEC-2026-0346: `Debug` impls for client session values expose TLS secrets
  rustls 0.23.43: RUSTSEC-2026-0348: Brotli certificate decompression can allocate up to 1 GiB before authe
  rustls 0.23.43: RUSTSEC-2026-0349: Unauthenticated ServerHello can downgrade later connections to classic
  wasmtime-wasi 48.0.1: RUSTSEC-2026-0314: Guest can panic host through filesystem datetime overflow
  wasmtime-wasi 48.0.1: RUSTSEC-2026-0321: WASI preview 0 implementation of `poll_oneoff` circumvents fuel consum
  wasmtime-wasi 48.0.1: RUSTSEC-2026-0322: Excessive allocated memory on the host when guests don't have stdio
  wasmtime-wasi 48.0.1: RUSTSEC-2026-0323: fd_readdir copies uninitialized struct padding into guest memory
  wasmtime-wasi 48.0.1: RUSTSEC-2026-0324: Guest can panic host through filesystem timestamp before the epoch on 
  wasmtime-wasi 48.0.1: RUSTSEC-2026-0352: Filesystem sandbox escape when renaming symlinks with trailing slashes
  zip 1.1.4: RUSTSEC-2026-0367:  Insecure Preservation of SUID/SGID Bits during ZIP Extraction Leading
  zip 1.1.4: RUSTSEC-2026-0368: zip: `ZipArchive::extract` creates symbolic links whose attacker-contr
  zip 1.1.4: RUSTSEC-2026-0369: ZipStreamReader::extract() and ZipArchive::extract() apply Unix permis
GitHub only: 2
  gix-fs 0.18.2: GHSA-f89h-2fjh-2r9q: gix-fs: Symlink prefix-reuse allows worktree escape during checkout
  opentelemetry_sdk 0.29.0: GHSA-w9wp-h8wv-79jx: opentelemetry_sdk has unbounded memory allocation in W3C Baggage propa
both: 4
  rustls 0.23.43: GHSA-2mjx-qc3c-rqvc, RUSTSEC-2026-0285: Rustls: TLS 1.3 handshake messages incorrectly accepted across encrypt
  rustls-webpki 0.101.7: GHSA-82j2-j2ch-gfr8, RUSTSEC-2026-0104: rustls-webpki: Denial of service via panic on malformed CRL BIT STRING
  rustls-webpki 0.101.7: GHSA-965h-392x-2mh5, RUSTSEC-2026-0098: webpki: Name constraints for URI names were incorrectly accepted
  rustls-webpki 0.101.7: GHSA-xgp8-3hg3-c2mh, RUSTSEC-2026-0099: webpki: Name constraints were accepted for certificates asserting a wi

Dependabot would raise the four in "both" and the two GitHub-only ones. cargo audit would raise the four in "both" and the 15 RustSec-only ones, including the Wasmtime sandbox escape. Neither raises all 21.

We also ran it against the lockfiles of three large open-source Rust workspaces on 17 October. One had 44 open findings, all RustSec-only. One had 17 RustSec-only, 5 GitHub-only and 10 in both. The third had a single GitHub-only finding. A lockfile entry isn't proof the vulnerable code is reachable, and some findings, like the rsa timing issue, have no fix to move to. But in the two larger workspaces, most of what remained was something Dependabot doesn't report.

What to do

  1. Run a RustSec check in CI if you rely on Dependabot. cargo deny check advisories or cargo audit on every pull request, and on a schedule so new advisories surface on unchanged code.
  2. Keep Dependabot or another GitHub-sourced check if you run cargo audit, for the advisories RustSec doesn't carry.
  3. Or use one OSV.dev-based scanner for both. OSV-Scanner reads Cargo.lock and queries OSV.dev, where both databases meet. Our script above shows which database each result came from.
  4. Watch repository advisories for critical dependencies. For a runtime like Wasmtime, a security advisory on GitHub can be the first and, for days, the only public record of a fix.
  5. Treat an empty alerts tab as a statement about one database. Before calling a Rust tree clean, check which source the tool read.

We've written before about using cargo audit with the RustSec database and how cargo audit and cargo deny compare. This year's data changes one recommendation: for Rust, one advisory source is no longer enough.

The fair view

Both databases are doing hard work, and much of it is done by a few people. RustSec is volunteer-run and lists advisories quickly. GitHub's reviewed database adds severity scores, CWE tags and version ranges that tools rely on, and reviewing takes time. The 2025 numbers looked very different: 77 of 78 RustSec advisories added that year had a GitHub reviewed entry by 17 October. The backlog since August may clear quickly.

These counts are one snapshot, taken on 17 October from the OSV dump of 16 October, and matching by alias will miss a few pairs that use different identifiers. But a gap measured in months, on a sandbox escape, is a reason not to depend on one feed. The cheapest fix is a second scanner in CI.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.