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 docs/BENCHMARKS.md.
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.
.vdb7, and v6 files are not readv7 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.
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.
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.
version_compare is gone; use vers_comparevdb.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.
source_data is a plain dictWorth 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.
vdb db: get, refresh, and inspect local dataA 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.
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.
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.
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.
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.
Four rules, in the order they bite:
coverage_gap on a component means not checked, not not-vulnerable.VDB_AUTO_FETCH in air-gapped CI unless you want scans
reaching for the network.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.
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.
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.
vdb db takes over from depscan-vdbdepscan-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.
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.