vulnerability-db

Migrating to VDB 7

VDB 7 is not a drop-in upgrade from 6.x. The database files changed name and format, the published image URLs moved, the CLI download flags were removed, three environment variables were deleted, and the version comparison API was replaced. v7 does not read v6 databases and ships no migration path, so the upgrade is “rebuild or re-download”, never “convert in place”.

This guide is written for three readers. Read the section that describes you; the parts you need from the others are cross-referenced.

If you are Start at
Embedding the library in your own tool Section 1
Running a scanner and answering for its results Section 2
Working on dep-scan v7, or an agent working in it Section 3

Every console block below is real output from running the command shown. Benchmark figures come from building v6 (8e660b9) and v7 (1393288) from the same feeds minutes apart on one machine, which produced identical row counts on both sides, so the differences are implementation rather than different data. Method and per-type figures are in BENCHMARKS.md.

The five things that break

1. .vdb6 files          ->  not read, not migrated. Rebuild or re-download.
2. vdbxz*/vdbzst* URLs, ->  ghcr.io/appthreat/vdb7-{full,app,<type>};
   --download*-image       fetched with 'vdb db refresh full'
3. VDB_EXTENDED_*,      ->  constants deleted; the artifacts exist as v7
   USE_VDB_10Y              scope variants, reached with --image (1.3)
4. utils.version_compare->  vdb.lib.vers.vers_compare, different signature
5. exit 0 + "No results"->  exit 1 on an empty database

Numbers 4 and 5 are the ones that change behaviour silently if you miss them. Number 4 is an ImportError, so you will find it immediately. Number 5 is the opposite: it exists precisely so that a broken install stops looking like a clean scan.

1. Integrators

1.1 Database files are .vdb7, and v6 files are not read

v7 opens data.vdb7 and data.index.vdb7 under VDB_HOME. It never opens .vdb6 files. Public artifacts are rebuilt from scratch by the release workflows, so there is no in-place upgrade to perform.

Pointing v7 at a VDB_HOME holding only v6 files gives you a database with zero rows, and v7 refuses to let that look like “no vulnerabilities found”:

$ VDB_HOME=/private/tmp/v6guard-test vdb --search "pkg:pypi/requests@2.31.0"
The vulnerability database at /private/tmp/v6guard-test is empty — no search can
return results. Build one with 'vdb --cache' or fetch a published database with
'vdb db refresh full'.
Found v6 database files (data.vdb6, data.index.vdb6) in that directory. This
version of vdb reads .vdb7 databases only and does not migrate v6 artifacts;
rebuild or download a v7 database.
$ echo $?
1

The guard is _warn_if_database_empty in vdb/cli.py. It draws the line between “no data” and “no vulnerabilities”. For a vulnerability scanner, silence on an empty database is the dangerous answer, so any wrapper that read exit 0 plus No results found! as a clean bill of health must now treat exit 1 the way it treats a build failure: an unusable scanner, not a clean scan.

An empty VDB_HOME with no v6 files gives the same first message and the same exit code, without the second paragraph.

If your code calls vdb.lib.db directly, the connection helpers still live there under that module name. Only the file names they open changed.

1.2 Download URLs moved to ghcr.io/appthreat

Constant Default
VDB_DATABASE_URL ghcr.io/appthreat/vdb7-full:v7.0.x-xz
VDB_APP_ONLY_DATABASE_URL ghcr.io/appthreat/vdb7-app-only:v7.0.x-xz

Both are published in two compressions, :v7.0.x-xz and :v7.0.x-zst. Check any reference with oras manifest fetch <ref> --descriptor.

The -xz suffix is the always-available one: it unpacks with the stdlib tarfile alone. A -zst reference also works, but needs a zstd decompressor: Python 3.14+ (which ships compression.zstd), or a zstd binary on PATH (VDB_ZSTD_BIN points at one elsewhere). Without one, the refresh refuses the -zst fetch before downloading anything and names both remedies; xz is the default for exactly that reason.

The v6 defaults (vdbxz, vdbxz-app, vdbxz-10y, the *-extended images, every :v6.7.x tag) are gone. A pipeline still exporting one fails at download time.

The v6 CLI download flags are gone with them. vdb --download-image and vdb --download-full-image no longer exist; the published database is fetched with vdb db refresh full --app-only (application vulnerabilities) or vdb db refresh full (app+OS), optionally with --image <oci-ref> to override the URL for a single run. Unlike the v6 flags, which extracted the download straight into VDB_HOME and could leave a torn database behind on interruption, the refresh stages and validates the artifact (both .vdb7 files present, index non-empty, not a single-shard artifact) and only then swaps it in atomically. A script that ran the old flags fails with an unrecognised-argument error rather than doing something surprising.

1.3 Deleted environment variables

Removed, with no v7 equivalent constant. The rationale is recorded in vdb/lib/config.py:

VDB_EXTENDED_DATABASE_URL, VDB_APP_ONLY_EXTENDED_DATABASE_URL, USE_VDB_10Y.

The v7 line deliberately has no variable per feed scope: one variable per variant does not scale, and a constant that silently handed back the ordinary database to someone who asked for the extended one was the failure mode worth removing. The artifacts those variables selected do exist for v7, as app-scope complete databases reached with --flavor (see the table in 2.3):

Metadata searches (full text, alias, reference, package name, symbol, and the severity, source and date filters) need a database built with --include-metadata. The pre-built answer is the extended flavor, vdb db refresh full --flavor app-extended (2020+, image ghcr.io/appthreat/vdb7-app-extended:v7.0.x-xz) or --flavor app-10y-extended (2016+), or vdb db refresh full --image <ref> with the URL of your own metadata artifact. Or build locally with vdb --cache --include-metadata, --cache-os --include-metadata, or VDB_INCLUDE_METADATA=true. The default public databases keep cve_metadata and cve_metadata_text empty, and metadata-dependent APIs fail safe with no metadata matches on such a database rather than inventing results.

Ten-year coverage needs vdb db refresh full --flavor app-10y (image ghcr.io/appthreat/vdb7-app-10y:v7.0.x-xz, or the -extended form), or NVD_START_YEAR=2016 (or older) on your own build. Note that as of the 7.0 line this only affects NVD-style CVE data and the Linux distro feeds. Application ecosystem advisories are no longer subject to a year floor at all, so an old npm or Maven advisory is present regardless of NVD_START_YEAR. See VDB_APP_ECOSYSTEM_START_YEAR if you want to re-impose one.

1.4 version_compare is gone; use vers_compare

vdb.lib.utils.version_compare and the comparison machinery around it were deleted, 25 functions in total, including version_compare, the old local vers_compare copy, convert_to_semver, semver_compatible, version_len, convert_to_num, the _dpkg_compare / _apk_compare / _verrevcmp family, and the epoch and hash-mode helpers. v7 has one comparison implementation instead of two, and the duplicate in utils is what went.

The replacement is vdb.lib.vers.vers_compare(version, vers). Mind the signature: this is a replacement, not a rename.

# v6: an interval expressed as two bounds
version_compare(compare_ver, min_version, max_version, mie=None, mae=None)

# v7: a vers range string. Both parts of the prefix matter, the 'vers:'
# marker and the scheme segment before the '/'.
from vdb.lib.vers import vers_compare
vers_compare(compare_ver, f"vers:generic/>={min_version}|<={max_version}")

A two-bound string is an interval, because the VERS containment algorithm pairs each lower bound with the upper bound that follows it. Against vers:generic/>=2.0.0|<=2.7.9.4, 2.0.0 matches while 1.0.0 and 3.0.0 do not. An unbounded v6 max_version='*' becomes a bare vers:generic/>={min}; stray * bounds are dropped, as are 99.99.9 placeholder “unfixed” bounds.

A missing vers: marker or scheme segment is supplied for you, so vers_compare("3.0.0", ">=2.0.0|<=2.7.9.4") is False, evaluated as if you had written vers:generic/.... Write the fully qualified form anyway, because schemes other than generic change how versions are ordered. The point of the fallback is that a hand-built string will not silently match everything.

The one range that matches every version is an empty one. to_purl_vers returns "" when every constraint had to be dropped, and records with no version range genuinely do apply to all versions, so dropping those matches would under-report.

Everything else the package-level API exposes keeps its import path: the search functions, CVESource.store(), and the model classes.

1.5 source_data is a plain dict

Worth stating explicitly because 6.x-era snippets are still circulating: the source_data value on a search result is a dict, ready for json.dumps(). It is not a Pydantic model and has no .model_dump().

results = search.search_by_any("pkg:pypi/xml2dict@0.2.2", with_data=True)
print(json.dumps([r["source_data"] for r in results if r.get("source_data")]))

Any code calling res["source_data"].model_dump(mode="json") raises AttributeError on v7.

1.6 vdb db: get, refresh, and inspect local data

A new subcommand, dispatched as a leading token next to vdb split and vdb verify, implemented in vdb/lib/db_cmd.py:

$ vdb db refresh --help
usage: vdb db refresh [-h] [--all] [--registry REGISTRY] [--tag TAG]
                      [--compression {xz,zst}] [--store-dir STORE_DIR]
                      [--image IMAGE] [--app-only]
                      [targets ...]

positional arguments:
  targets               Shard names or purl types (npm, pypi, deb, nuget->app,
                        ...), or 'full' for the whole-database image. Omitted:
                        refresh everything already present in the local store
                        (sync what I have).

options:
  -h, --help            show this help message and exit
  --all                 Refresh every default shard
  --registry REGISTRY   Shard registry (default: local siblings record, else
                        ghcr.io/appthreat)
  --tag TAG             Shard tag (default: local siblings record, else
                        v7.0.x). The encoding suffix is appended for you; pass
                        it here only to override, e.g. --tag v7-xz for the
                        newest build.
  --compression {xz,zst}
                        Encoding suffix appended to the shard tag (default:
                        whatever the local store was fetched with, else xz).
                        zst needs a decompressor (Python 3.14+ or a zstd
                        binary on PATH); xz always works. Ignored when --tag
                        already ends in -xz or -zst.
  --store-dir STORE_DIR
                        Shard store directory (default: $VDB_SHARDS_DIR)
  --image IMAGE         With 'full': OCI image URL override (default:
                        VDB_DATABASE_URL)
  --app-only            With 'full': use VDB_APP_ONLY_DATABASE_URL

vdb db refresh with no targets means “sync what I have”: everything already in the store, plus the connected main database when it is itself a shard.

Shard references carry an encoding suffix, because that is all the build publishes: v7-<shard>:v7.0.x-xz and -zst exist, a bare v7-<shard>:v7.0.x does not. vdb db refresh appends it, inheriting the encoding the local store was fetched with, else xz, so --tag is for picking a release line (--tag v7-xz for the newest build), not for restoring a suffix. --compression zst selects the zstd encoding, which needs a decompressor (Python 3.14+ or a zstd binary on PATH); without one the refresh fails before downloading, naming both remedies. If you script against refresh_shards() directly, note that it resolves compression itself; passing registry= and tag= to shard_store.fetch_shards() without compression= suppresses its own resolution and builds an unsuffixed reference.

Refresh safety is a designed property rather than a side effect. The full image is staged, validated (both .vdb7 files present, index non-empty, not a single-shard artifact), and only then swapped in. Current files are stashed away before the new ones land, so an interruption leaves files missing, which is loudly reported with exit 1, rather than a mixed old and new pair that could answer subtly wrong. Shard refreshes place each shard with an atomic directory rename, so an interrupted multi-shard refresh leaves every shard either refreshed or at its previous complete artifact. A downloaded-but-empty database is rejected outright.

2. Operators

2.1 What v7 changes about the answers

The wins are build time and size. Query speed is unchanged:

  v6.7.2 v7.0.x
Build time (OSV only) 1,051 s 425 s (2.47x faster)
Database on disk 8.18 GiB 3.45 GiB (2.37x smaller)
Bulk query, 5,000 purls 205.3 ms 203.8 ms
Maven-only workload 3.45 GiB full DB 35 MiB shard, identical hits

Both sides had identical row counts (1,324,712 data / 1,324,661 index).

Do not expect v7 to query faster on a full local database. It does not. The 5,000-purl bulk query moved from 205.3 ms to 203.8 ms and every per-type figure is inside run-to-run noise. Anyone hoping 7.0 is “faster to scan” can reproduce the disappointment in a minute. What improves is build time, database size, and how little you have to download when your workload is type-scoped.

Matching behaviour did change, because vdb.lib.vers became the single comparison implementation. The user-visible effects, taken from the defended baseline change in contrib/match_set.md (2026-08-16, 2,981 to 2,956 matches on the fixture, each difference individually justified there):

npm @latest dist-tags no longer match fixed advisories. latest is a dist-tag, not a version. The old implementation coerced the non-numeric string to 0, below every upper bound, so @latest purls matched CVEs fixed anywhere from 1.4.18 to 6.0.6 (23 removed matches across django, ansible-core, brotli, idna, llama-index). A non-numeric version now sorts above numeric ones, so latest still matches unbounded advisories and no longer matches fixed ones. If your SBOMs carry @latest purls, resolve them to the concrete versions you deploy, or expect fewer and more honest matches.

Maven rows with self-contradictory !=X tails resolve differently. Some maven rows carry a merged vers string that both includes and excludes the same version, for example >=1.1|<=1.6.1|<1.6.1|!=1.6.1, where the != came from a CNA “unaffected” entry. != is now honoured, so the match comes from the consistent sibling row instead. For dom4j@1.6.1, velocity@1.7 and commons-configuration@1.10 the CVE is still reported; only the matched row changed.

Two golang true positives were added, golang.org/x/sys commit pseudo-versions that the old v-prefix code path answered inconsistently.

2.2 Coverage changes in the 7.0 line

Two changes affect which advisories exist at all, not just how they match.

Application ecosystem advisories are no longer filtered by year. The NVD_START_YEAR floor was an artifact-size policy sized for distro CVE volume, and applying it to library advisories deleted every application advisory older than it. Measured against the OSV feeds, a 2022 floor discards 1,760 npm, 3,310 PyPI, 600 RubyGems and 426 NuGet advisories. Those are lodash@4.17.15 and log4j-core@2.14.1 class findings, and a lockfile does not move forward the way a distro release does. Expect more findings on old pinned dependencies after upgrading. VDB_APP_ECOSYSTEM_START_YEAR re-imposes a floor if you would rather have the smaller artifact.

Malicious package advisories match more versions. Where the upstream feed states that every version of a package is malicious, v7 now honours that statement instead of narrowing it to the specific releases a reporter happened to analyse. Expect a scan of a known malicious package to report it at any version, where 6.x reported it only at the enumerated ones.

2.3 Choosing between full, app-only, and shards

The v6 name vdbxz-app becomes vdb7-app-ONLY, not vdb7-app. vdb7-app is a v7-only concept: the app group shard, a completeness=partial slice of the full database that belongs in the shard store. vdb db refresh full rejects it. If you are migrating a pinned vdbxz-app reference, the equivalent is ghcr.io/appthreat/vdb7-app-only:v7.0.x-xz.

Artifact Answers Cost Choose when
vdb7-full every purl type, plus CPE, CVE ID, alias, reference and full-text search largest download, refreshed as one unit you scan containers or VMs, or you need a shard-unsafe API
vdb7-app-only application ecosystems only no OS feeds, substantially smaller you never scan OS packages
type shards the purl types each shard serves; purl searches fan out across the local store each shard is an independent artifact you keep current your workload is type-scoped

The shard-unsafe APIs are search_by_cpe_like, search_by_cve, search_by_alias, search_by_reference, search_full_text, search_by_symbol and latest_malware. They raise PartialDatabaseError on a shard rather than under-report.

A Maven-only CI answers 2,370 hits from a 35 MiB shard instead of the 3.45 GiB full database, with identical hits.

The shard set is larger than early 7.0 documentation suggested. Five shards are structural (deb, rpm, apk, app, and the cpe complement) and the rest are per-type, one for each purl-spec type except deb, rpm, apk and generic. That is 43 possible shard names, and the splitter emits only the ones the build actually has rows for, which was 26 on a recent app+OS build. A shard’s own vdb.meta (artifact.types, siblings.available) is the authority on what exists and what it serves.

2.4 Shard semantics an operator has to know

The safety model is documented at the top of vdb/lib/shard_store.py. Four properties matter operationally.

Shards are independent artifacts, and mixed builds are normal. Refresh the npm shard daily and the deb shard monthly if that matches your exposure. A purl query is answered by exactly one shard per purl type, so mixing builds changes only which data each type is answered from. The one hard rule is that a main database and a store shard claiming the same shard name from different builds is an error for the operator to resolve, never something settled by directory ordering.

A missing shard is a coverage gap, not a clean empty. Single-purl searches raise ShardCoverageError. Batch searches (search_packages, search_packages_batched, search_bom_*) mark the affected components coverage_gap: true, attach a coverage companion listing requested, covered and uncovered types (.coverage on the result list, or vdb.lib.search.last_shard_coverage), and warn on stderr.

A stale shard is a warning, and deliberately a different condition. A shard lagging its newest sibling by more than 7 days (STALE_SHARD_LAG_DAYS) triggers a staleness warning. “I have no deb data” and “my deb data is three weeks old” travel through different channels and must not collapse into one signal.

On-demand fetch is opt-in through VDB_AUTO_FETCH=true, so a scan never reaches for the network mid-run by default. The explicit API is vdb.lib.shard_store.fetch_shards() and the CLI front end is vdb db refresh.

Here is a pypi-shard main database asked about an npm package:

$ VDB_HOME=/private/tmp/vdb81-demo/home3 vdb --search "pkg:npm/lodash@4.17.21"
vdb: coverage gap — purl type(s) npm were requested but no local shard serves
them. Results for those purls are NOT CHECKED (this is not a clean result).
Fetch the shard (vdb.lib.shard_store.fetch_shards or VDB_AUTO_FETCH=true) or use the
full database.
...
vdb.lib.search.ShardCoverageError: no local shard serves purl type 'npm':
pkg:npm/lodash@4.17.21 cannot be answered by this database. Covered types:
none. Fetch the shard (vdb.lib.shard_store.fetch_shards / VDB_AUTO_FETCH) or use the
full database. Batch searches (search_packages/search_bom_*) mark this as a
per-component coverage gap instead of raising.
$ echo $?
1

And a store with both conditions present, mixed builds (normal) and a stale deb shard (a warning, distinct from a gap):

$ VDB_HOME=/private/tmp/vdb81-demo/home1 vdb db status
VDB_HOME: /private/tmp/vdb81-demo/home1
main DB (full): built 2026-08-16T14:47:32+00:00, age 7.1h, 2.9 GB + 593.0 MB, 1324681 data / 1324630 index rows — ok
shard apk: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-08-16T14:47:32+00:00, age 7.1h, 681.7 MB + 151.2 MB, 1 types — ok
shard app: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-08-16T14:47:32+00:00, age 7.1h, 1.5 GB + 132.4 MB, 9 types — ok
shard cpe: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-08-16T14:47:32+00:00, age 7.1h, 44.2 MB + 5.7 MB, 9 types — ok
shard deb: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-07-17T14:57:20+00:00, age 30.3d, 299.5 MB + 99.4 MB, 1 types — STALE — much older than its siblings
shard golang: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-08-16T14:47:32+00:00, age 7.1h, 30.4 MB + 3.8 MB, 1 types — ok
shard maven: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-08-16T14:47:32+00:00, age 7.1h, 28.7 MB + 6.4 MB, 1 types — ok
shard npm: build 2026-08-16T14:56:10Z-2f0c0124, built 2026-08-16T14:55:47+00:00, age 7.0h, 1.0 GB + 73.7 MB, 1 types — ok
shard pypi: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-08-16T14:47:32+00:00, age 7.1h, 90.8 MB + 8.5 MB, 1 types — ok
shard rpm: build 2026-08-16T14:47:34Z-2f0c0124, built 2026-08-16T14:47:32+00:00, age 7.1h, 412.4 MB + 204.5 MB, 1 types — ok
store: full — mixed builds (2026-08-16T14:47:34Z-2f0c0124, 2026-08-16T14:56:10Z-2f0c0124)
note: the connected main database is not a shard (completeness != partial), so searches use the main database only; the shard store is consulted when the main DB is a shard.

--json emits the same report machine-readably, with build_ids, mixed_builds, coverage, missing_group_shards and per-shard stale.

2.5 Keeping a scan honest

Four rules, in the order they bite:

  1. Treat vdb exit 1 with “database … is empty” as scanner down, not clean.
  2. On a shard store, read the coverage companion after batch scans. A coverage_gap on a component means not checked, not not-vulnerable.
  3. Read a staleness warning as “answers for this type may be old” and decide whether that is acceptable. A gap and a stale shard are different conditions on purpose.
  4. Do not enable VDB_AUTO_FETCH in air-gapped CI unless you want scans reaching for the network.

2.6 Shard artifact availability

vdb7-full, the group shards and the per-type shards are all on the registry now. Early 7.0 runs published the full and app images and failed at the shard push, which is what previous versions of this guide described; that is fixed.

The -xz and -zst suffixes are not optional. Nothing publishes a bare v7.0.x tag, so a reference without one 404s no matter how the artifact is doing.

The scope variants (vdb7-app-only, vdb7-app-extended, vdb7-app-10y, vdb7-app-10y-extended) are built by their own jobs and land with the first build after they were added. Check before depending on one:

$ oras manifest fetch ghcr.io/appthreat/vdb7-app-10y-extended:v7.0.x-xz --descriptor
Error response from registry: denied: requested access to the resource is denied

A reference that resolves returns a descriptor instead. Where this guide names a shard tag, it names the tag the build publishes, not a promise that it is live right now. A failed shard fetch damages nothing, because refresh errors leave the local store intact.

3. dep-scan v7

dep-scan v7 is the v7 consumer. This section is the handover, and it is also the place to look if you are an agent making the change.

3.1 The pin must move, and one import must change

Today’s dep-scan pins "appthreat-vulnerability-db[oras]~=6.7.2" in pyproject.toml. That pin has to move to 7.x, and the move is not free: dep-scan imports version_compare from vdb.lib.utils (packages/analysis-lib/src/analysis_lib/utils.py:47, called at line 439 to pick a maximum version), and that symbol does not exist in 7.x.

The mechanical replacement for dep-scan’s version_compare(vl, min_ver, max_ver) interval is:

from vdb.lib.vers import vers_compare
vers_compare(vl, f"vers:generic/>={min_ver}|<={max_ver}")

Read section 1.4 before applying it, because the scheme segment matters for anything that is not generic.

While you are there, check for .model_dump() calls on source_data (section 1.5). Those are silent in a 6.x-era snippet and an AttributeError on v7.

3.2 What vdb db takes over from depscan-vdb

depscan-vdb (dep-scan’s depscan/vdb_cli.py) shrinks but does not disappear. The split is stated in vdb/lib/db_cmd.py’s module docstring, and this document should stay consistent with it.

Subsumed by vdb db:

Downloading a published full-database image into VDB_HOME. vdb’s refresh is staged, validated and atomically swapped, which is stricter than depscan’s direct download_image call, which extracts in place and can leave a torn database on interruption. Also fetching and refreshing type shards into the shard store, which dep-scan has no support for at all; reporting local state (per-shard build id, build time and age, size, coverage, full versus partial store), which covers depscan-vdb info and path; and the “sync what I have” refresh, which depscan approximates with its variant marker.

Still dep-scan’s concern:

Variant selection, meaning the mapping from user intent (scope, time window, extended tier, compression, distro) to a concrete image URL. vdb takes the URL from VDB_DATABASE_URL, VDB_APP_ONLY_DATABASE_URL or --image and does not model variants at all. dep-scan’s resolve_vdb_image can export the chosen URL into the environment and invoke vdb db as a thin wrapper. Retry policy (download_vdb_with_retries), the variant marker file and the VDB_AGE_HOURS freshness policy are dep-scan cache-keying concerns and stay there.

3.3 Practical notes for dep-scan on v7

dep-scan’s VDB_AGE_HOURS marker logic keeps working, but a fresh v7 install needs a v7 image URL. USE_VDB_10Y and the *-extended URLs dep-scan could previously resolve have no v7 constants (section 1.3); the underlying artifacts are the app-scope variants, so resolve to those with --image, to the defaults, a shard, or your own build.

dep-scan’s exit-code handling should treat vdb’s exit 1 empty-database message as scanner down (section 1.1).

If dep-scan serves shard stores, surface the coverage companion and staleness warnings rather than swallowing them (section 2.4). A scanner that hides a coverage gap is worse than one that has no shard support, because the gap looks like a clean result.

Expect the finding count to go up rather than down after the upgrade, mostly from section 2.2: old application advisories that the year floor used to delete, and malicious packages that now match at every version. Do not treat that as a regression in precision without checking which of the two produced it.