VDB
Sign up
Continuously updated

Changelog

Everything we've added or improved on VDB, recorded right after the deploy that ships it.

October 2026

fix

Risk scoring at scale: derived values take their source's origin, and saturation stops hiding the ranking

Run against open-webui, AutoGPT's platform backend, the full-stack FastAPI template, paperless-ngx and saleor. A value derived from a function parameter (`cmd = "convert " + name`) was scored as if an attacker had typed it; the abstraction now records where every source came from and a derived value inherits it through the assignment flows, so a parameter of unknown provenance is worth 0.5, a request 1.0. The noisy-OR project score saturates at 1.0 once a few hundred paths are open and then says the same of every large project: the response adds `open_paths`, `risk_mass` (the summed path scores) and `by_sink` (the highest path per sink class), the ranking orders candidates by the risk mass they close per cost once ΔS is below noise, a candidate reports the paths it closes and the ones it newly decides, and the combination counts a step only when it removes mass the steps before it had not. `--exclude` applies to files as well as directories (`*_test.py`); the report says when the per-run file limit or the path ceiling was hit; a cached analysis scored by an older engine is re-scored on read.

ui/ux

The hardening pages show the risk score and the remediation ranking from real runs

/harden and /examples now carry the CLI's actual output for their sample files, including the project risk block and `--remediate` ranking (regenerated from live runs, not mock-ups); /examples gains "Decide what to fix first" and explains the interval; /api, /about and /connect name the score, the event-driven re-scoring and the ranking. Coverage counts real packages only — the standard library's pseudo node no longer makes "3 analysed" read as "4 of 6" on the next line — and the CLI's risk list drops paths that would print as 0.00.

ui/ux

vdb harden accepts an unpinned requirements.txt, takes --exclude, and names files in its ranking

Checked against seven local projects: four could not be analysed at all because their requirements.txt had ranges instead of pins. An unpinned package is now a version-less node — no summary or advisory can be decided for it, the response says so in notes, its advisories are filtered by the declared range where provable, and the remediation ranking offers to pin it at a version that can be analysed ("unlocks analysis"). `vdb harden

--exclude ` (repeatable, globs allowed) leaves backups, vendored copies and fixtures out of a tree analysis. The report groups identical paths ("×20 … in a.py, b.py +9 more") and names files instead of F-numbers, the ranking folds look-alike single-call-site candidates and adds "harden all N call sites reaching ", and candidates that change nothing are no longer listed.

feature

vdb harden scores the project, watches it for events, and ranks what to fix first

POST /v1/harden/analyze now returns a `risk` block: every decided path scored from its entry point to its sink — a request argument, a hallucinated package name, a package registered days ago, an install script, a tool server with shell scopes, or a CVE (KEV/EPSS-weighted) — combined noisy-OR into a project score, with the share of the dependency graph the analysis covered; when coverage is low the score is an interval, not a single number. Stored graphs are re-scored when an advisory is published, a summary is built, the registry changes or a tool server's scopes move, and you are mailed only when your project's score actually moved (GET /v1/harden/projects shows the current scores). POST /v1/harden/remediate (`vdb harden … --remediate`) applies upgrade, boundary-hardening, removal and execution-policy candidates to the graph counterfactually and ranks them by risk reduction per cost, with the combination that reaches a target. Korean patent application JP260065.

ops

Privacy requests and account questions go to master@vdb.ai.kr

The privacy policy's contact address — for asking what we hold about you, having it corrected or deleting an account — and the address named in the sign-up limit message are now master@vdb.ai.kr, the same address as the site footer. Mail sent to the old address still reaches us.

ui/ux

Site copy corrected: MCP scopes are inferred, two endpoints need no key, and more

A pass over the pages against the code. The MCP registry said it recorded each server's "declared" scopes, download counts and known issues from official and community registries; in fact scopes are inferred from a server's name and description (we do not run servers or read their code), the count shown is GitHub stars and the date is the last push — the pages and labels now say so. The API page's limits section still said "there is no anonymous tier" although the slop list and the package gate answer without a key. llms.txt said an SBOM scan needed no account for the first scans (it needs a free one), listed pub.dev among advisory sources (it is a registry existence check only), and now also describes malware reports, the 100-package request limit and probe_timed_out. /about names NVD scores and malware reports among what VDB brings together. And a package check refused for being over 100 packages now answers agent_action REFUSE, like every other answer where nothing was checked, instead of CONFIRM.

fix

Sign-up can no longer be used to send verification mail to strangers

In one week 136 accounts were created through the sign-up form from 118 different addresses, with browser user-agents, for email addresses at gmail, hotmail, a law firm, a games studio — and not one was ever verified. Each made VDB send a verification link to someone who had not asked for it; the per-address limit never applied because every attempt came from a new one. The form now carries a signed token and a field people cannot see, and a submission that never loaded the form, fills the hidden field or arrives faster than anyone types gets the usual "check your inbox" page with no account created and no mail sent. Behind it, mail to unverified addresses (sign-up, re-sent links, emailed keys) has an hourly ceiling across the service — past it sign-up and /v1/auth/request-key answer 429 until the hour turns — and a verification link can be re-sent three times a day per account instead of once a minute without end.

ops

Contact address is master@vdb.ai.kr, and VDB mail now comes from vdb.ai.kr itself

The contact link at the bottom of every page is master@vdb.ai.kr. Account mail (verification links, API keys, password links) and alerts are now sent by VDB's own mail server at mail.vdb.ai.kr, signed for vdb.ai.kr (SPF, DKIM, DMARC), instead of through a Google account on another domain.

data

Advisories with no rating of their own take the NVD score of their CVE

The Go vulnerability database rates nothing, GitHub publishes almost no advisories for the Go standard library, and many PyPI advisories carry no severity: all 58 advisories affecting golang/stdlib@1.23.12 had none. VDB now collects CVSS base scores from the NVD and uses the score of an advisory's CVE when neither the advisory nor its OSV aliases carry one. check-packages marks it severity_source "nvd:CVE-…", and vulnerability pages and /v1/vulns/{id} show it with source "nvd". The first collection runs over a few days. This product uses the NVD API but is not endorsed or certified by the NVD.

fix

Scoped npm packages are found whether the @ is encoded or not

Advisories for scoped npm packages are stored under the encoded purl (pkg:npm/%40babel/traverse), and check-packages, /v1/query and the SBOM scan looked up only the spelling they were sent. pkg:npm/@babel/traverse@7.22.0 — the form most tools write — returned no advisories, missing CVE-2023-45133, a critical one; the same held for every @scope/ package. Both spellings are looked up now. The ecosystem/name form (npm/@babel/traverse@7.22.0) also has its version read; it used to stay attached to the name, so nothing was evaluated.

fix

Packages with more than 50 advisories are checked against all of them

check-packages and the SBOM scan read at most 50 advisories per package, /v1/query 100, in no particular order, and only then compared your version with them. A package with more — the Go standard library has 181 — was judged on whichever 50 came back: golang/stdlib@1.21.0 reported no advisories at all, because those 50 had all been fixed earlier. Every advisory of the package is now evaluated before the response is cut to the worst few. The SBOM scan also stops dropping scoped npm names written with a bare @ (pkg:npm/@babel/core@7.0.0 was looked up as pkg:npm/).

fix

Malware reports flag only the hijacked release, not every version of the package

An OSV advisory can name affected versions as a list instead of a range, and every malicious-package report does: chalk's lists only 5.6.1, the release that was hijacked in September 2025. VDB read ranges only, treated a list-only advisory as undecidable, and kept it for every version — so chalk@2.4.2, ansi-regex@5.0.1 and other untouched releases were reported as malicious and, since the same day's change, refused. Affected now means in a range or in the list, everywhere a version is evaluated (check-packages, SBOM scans, /v1/query, /v1/querybatch); a version given as a range such as ^5.0.0 is still never ruled out by a list. Separately, golang/stdlib and golang/toolchain are no longer reported as names that do not exist on the registry. Both found by sbomcmp scanning this repository.

perf

check-packages is fast again for Go and PyPI packages with many advisories

Since advisories without a rating of their own started borrowing their CVE or GHSA alias's, a package with many of them (Go and PyPI, mostly) slowed down by about a second per advisory — flask 12 s, cryptography 24 s, golang.org/x/crypto past a 60 s timeout. The lookup joined on "id equals the alias OR id is in a subquery", which no index can serve, so every alias scanned all ~290,000 advisories. It is two index lookups now: the same 86 aliases take under a millisecond instead of 33 seconds.

data

MCP servers are recognised by their npm and PyPI names

check-packages recognises an MCP server sent as pkg:npm/… or pkg:pypi/… by the package name the registry holds for it — and only 7 of 1,036 servers had one, because names were read only for the Anthropic reference servers. The crawler now reads the root package.json (unless private) and pyproject.toml of every community server, and fills in the existing ones over the next runs. Where several servers share a name — copies of a reference server keep its package.json — the most trusted, then the most starred, answers.

fix

Advisories rated only in CVSS 4.0 get a severity

An advisory whose only rating was a CVSS 4.0 vector, with no GHSA severity label beside it, was shown with no severity anywhere — 863 of them, mostly recent. CVSS 4.0 vectors are now scored (v3 still wins when a record has both), in check-packages, SBOM scans and on vulnerability pages. Records returned by /v1/vulns/{id}, /v1/query, /v1/recent and /v1/search carry vdb_severity: {bucket, score, source}, so clients and the site show the same rating instead of deriving their own.

perf

check-packages answers 100 packages in seconds, and says which it could not check

A request of 100 packages with a key could take over a minute — the live registry lookups ran 8 at a time and one slow registry held up the rest — and clients timed out with nothing. Registry lookups now run 16 at a time inside a shared time budget of about 20 seconds; a package whose registry did not answer in time comes back with registry.timed_out and risk_hint "unknown" and is listed in probe_timed_out, which means "could not check", never "does not exist". The slop-candidate lookup is one query instead of one per package, and it now also finds packages sent with a version, which it used to miss. One request with a key checks up to 100 packages; more is a 413 that names the limit.

ui/ux

check-packages says when no version was evaluated

A package sent without a version (pkg:npm/@modelcontextprotocol/server-filesystem) gets every advisory it has ever had, which reads the same as a list of advisories affecting what you run. Each result now carries version_evaluated: false in that case, so a client can show "has a history" instead of "affected".

feature

POST /v1/querybatch: point osv-scanner and other OSV clients at VDB

VDB answered OSV's single /v1/query but not its batch endpoint, which is the one OSV clients call first. /v1/querybatch takes up to 1000 queries in OSV.dev's shape — a purl or ecosystem + name, and a version — and returns ids and modified times per query, in order, from one database round trip. Versions are evaluated against each advisory's affected ranges, as in /v1/query; commit queries are not supported and answer empty. Needs an API key.

ui/ux

Without a key, the package gate counts packages, not requests

The keyless allowance for /v1/ai/check-packages was 20 requests an hour of at most 5 packages. A client checking a lockfile had to know to send exactly 5 at a time, and found out only from a truncated block after its first larger batch. It is now a budget of 100 packages an hour per address, up to 25 in one request; the anonymous block on every answer gives the remaining budget and max_per_request, and a truncated block says whether the cut was the request size (send the rest) or the hour (wait, or get a free key). Packages that were not checked are never billed. The agent manifest at /v1/ai/manifest now describes the two keyless endpoints the same way; it had said none existed while listing them.

fix

Malware reports are refused, and a cut advisory list no longer hides the worst one

OSV malicious-package reports (MAL-) carry no severity, so check-packages returned them as bucket "none", below every low advisory — the 2025-09 npm chalk/ansi-* compromise among them. They are now bucket critical, marked malicious: true, and always REFUSE. Go vulndb and PYSEC advisories without a CVSS of their own take their CVE/GHSA alias's rating, and every advisory says where its rating came from in severity_source (advisory, alias:, malicious or none — none means unrated, not low). The advisory list is still cut to 3, but the verdict now reads every advisory, the worst-rated one is always among the 3, and each result carries worst_bucket, worst_score, kev_count and malicious over all of them. Found by sbomcmp.

fix

Go, Rust, Ruby and PHP packages are checked against your version

check-packages, the SBOM scan and /v1/query read an advisory's affected version ranges only when the advisory's ecosystem label spelled the same as the purl type, which is true for npm and PyPI and nothing else. For Go, crates.io, RubyGems, Packagist and Pub every advisory on a package was reported for every version, with no fixed_in: gin@v1.9.1 and gin@v1.10.1 both came back with the same 8. Advisories are now matched by package identity, the version is evaluated, and fixed_in is filled. /v1/query honours the version, as its documentation already said. Found by sbomcmp.

data

Data milestone — crossed 1M AI models

Total collected AI models crossed 1,000,000 for the first time. Auto-emitted by the scheduler.

fix

Every model and dataset in the AI registry has its own page

/ai/models and /ai/datasets linked each row to a detail page that did not exist, so every one was a 404 (Search Console counted 83). They now open a page with the license, weights or file format, size, downloads, risk score and notes, and a warning when model weights are a pickle format that can run code on load. MCP server pages had a related bug: the ID in the address was encoded twice, so a server listed in the registry was shown as "not in VDB's MCP registry". It is decoded once now.

ops

IP addresses are erased after 30 days, request records after a year

Until now nothing was deleted: every API request record kept the caller's IP address indefinitely. A job now runs every day. IP addresses in request records and abuse blocks are erased after 30 days, leaving a one-way hash so traffic can still be counted; request records and page views are deleted after a year, abuse blocks a year after they end. Account data, SBOM Watch registrations and hardening analyses stay until you delete them. /privacy states these limits.

feature

A privacy policy, and ads from Google AdSense on content pages

/privacy now says what the service records and why — account data, API request records with IP address and User-Agent, page views, abuse blocks, SBOM Watch registrations, hardening analyses — who else receives data (Google for ads and email delivery, ip-api.com for traffic statistics over plain HTTP until it is replaced with a local lookup, the package registries by package name only, Vultr as host), that request records have no automatic deletion date yet, and how to have your data deleted. Content pages now carry Google AdSense; sign-in, sign-up, account, verification, bug-report and admin pages do not. ads.txt is published.

fix

Risk-path mail only for decided paths from a dependency analysed for the first time

The previous fix silenced the first re-run after an engine upgrade, but the summary rebuild that follows it drains over days, and every rebuilt summary re-ran its analyses and mailed what changed — one mail listed 889 places, led by paths the engine had partly assumed. Now a rebuilt summary never notifies; only a dependency summarised for the first time can. A mail lists only decided paths (above the confidence threshold, no assumed hop, no dynamic dispatch), and more than ten at once for a project is treated as the analysis changing rather than news: the report has them, the inbox does not. The queued backlog was cleared.

fix

Risk-path mail says where, what can happen and what to do — and stops after engine upgrades

After the hardening engine was upgraded, every stored analysis was re-run and each path the new engine decided differently was mailed as "new" — a daily stream titled "N new potential risk path(s)" that listed sink names and confidences and nothing else. Now an analysis remembers which engine produced its baseline, and a re-run under a new engine re-baselines without mail; the backlog was cleared. A mail goes out only when at least one path clears the engine's confidence threshold. It names the risk in its subject, lists the most consequential places first — your call and line, what can happen, the wrapper that closes it — then three steps to act and why you got it. Every mail carries a signed stop link (a button, so a mail scanner cannot press it) and one-click unsubscribe headers.

fix

Email confirmation takes a click, so a mail scanner cannot use up your API key

Opening the confirmation link used to verify the account and show the one-time API key on that page. Company mail gateways (Microsoft Safe Links, Mimecast, Proofpoint) open every link in a message before the person does, so the scanner saw the key and the person who clicked later got "token already used". The link now opens a page with one button; confirming is a POST, which scanners do not send. The API takes POST /v1/auth/verify {"token": "..."}; the GET form still works for older clients and is marked deprecated.

fix

The SBOM scan page works again, and the site says what the service actually does

/sbom-scan sent uploads to the API with no credential, so since keyless access was narrowed to the slop list and the package gate, every upload showed a raw missing_api_key error while the page said no account was needed. Scanning on the page is now for signed-in members (the file is parsed in memory and not stored); the API is unchanged. A pass over the rest of the site corrected what had drifted: /about listed NVD, ENISA EUVD and npm-advisory feeds VDB does not collect (advisories arrive through OSV, which carries GitHub advisories and their CVE ids), said cross-model agreement raises a slopsquatting score (it does not; age, existence and publish history do), and named a 10-call trial that is now a 20-checks-an-hour keyless quota; the keyless limit was described as 5 packages an hour instead of 5 per request; dataset pages named Kaggle, which is not collected; /harden and /examples showed an older report format, called the sink list six, said project names are never sent (the folder name is, for alerts), and now state what the analysis does not examine, with measured recall.

perf

Hardening analyses run on their own workers, with twenty times the room

Composition used to run inside the API's request processes, so its work budget was kept small: on 20 real advisories it cut 8 analyses short, one of them a release with two call sites. It now runs on a dedicated worker pool with a hard time limit — a job past 180 s is stopped, where before a client that gave up left the server computing — and the budget is 20x larger. On the same advisories two more are found and the slowest run takes 2.5 s. /v1/harden/analyze, /v1/vex and /v1/harden/verify keep their request and response shape; when the pool is busy they answer 503 with Retry-After, and when a run exceeds the limit, 504 with a suggestion to analyse a narrower path.

feature

vdb-mcp 0.2.6 — the local abstraction sends eval/exec and file modes

The vdb_harden tool builds its abstraction on your machine. 0.2.6 records eval() and exec() calls (unless your code defines or imports its own), and sends file-open modes such as "r" or "w:gz" instead of masking them, so the server can judge your own command, deserialization and decompression calls and tell reading an archive from writing one. Every other literal is still reduced to its shape. Earlier versions keep working; they just do not send those two things.

feature

Hardening now judges your own subprocess, eval, pickle and decompression calls

vdb harden used to follow arguments only into dependencies, so subprocess.run(cmd) or pickle.loads(blob) in your own handler produced nothing — every report said so. It now judges those calls directly: subprocess, os.system/popen/exec*, eval/exec, pickle/marshal/shelve, zlib/gzip/bz2/lzma decompression, zipfile/tarfile opened for reading, shutil.unpack_archive, urlopen and socket.create_connection. Only the argument that carries the danger counts (a tainted cwd= is not command injection), and an archive opened for writing is not decompression — file modes are the one literal the client now sends unmasked. On 19 published PyPI advisories of this shape it points at the blamed line in 15; across 39 real releases it adds 3.7 call sites on average. File paths and regular expressions in your own code are still not examined: with every parameter treated as a possible source they bury the rest (regexes were half of all such findings), so the report keeps pointing to Bandit or Semgrep for those. eval/exec need vdb-mcp 0.2.6; the hosted client.pyz is always current.

fix

Hardening reports regex risk only where you control the pattern, and names archive decompression

Run on our own service, vdb harden flagged 11 call sites; 25 of its 39 paths were "regular-expression evaluation" and none was real — httpx matches URLs against its own fixed patterns, and that was attached to every httpx call, a timeout argument included. regex_eval now counts only when the pattern itself is tainted, not the text matched against it, and a pattern built with re.escape is a literal. The one real problem in that run, an uploaded spreadsheet that expanded to nearly two million rows, was described as a path, regex and global-state risk; a new sink class, decompress, names it (zipfile, tarfile, gzip, zlib, bz2, lzma), with a suggested fix that measures expansion by decompressing. Sink names now resolve import aliases. On the same code: 11 call sites to 3, 39 paths to 15. The labelled benchmark is unchanged at 21/21 detected and 0/12 false alarms; summaries are rebuilt under generator 0.3.6.

ui/ux

The site is dark everywhere

The site used to follow the system's light or dark setting. Charts embedded as images can only follow the system setting, never the page's, so they could come out in the wrong palette. The site now uses its dark theme for everyone, set by the server so nothing flashes on load. Blog posts also show an image when a paragraph is just a link to one.

feature

Blog: beyond the package name

Name checks say whether a package is safe to install; they cannot say whether the way your code uses it is safe. We ran vdb harden on our own service: 11 call sites flagged, 3 real problems nobody had reported, 2 fixed the same day — including a spreadsheet upload that cost about a minute and a gigabyte, now a few seconds. Also covered: where the analysis was wrong, and what it does not examine. /blog/beyond-the-package-name

ui/ux

The hardening report says what it does not examine

vdb harden follows the arguments you pass into dependencies; it does not look at what your own code does with the standard library. With a tainted argument, os.system, subprocess with shell=True, eval, pickle.loads and open all produced zero paths — and "No risky dataflow path found" read as a clean result for them. Every report now carries a scope line saying so and pointing to a static analyser such as Bandit or Semgrep for those calls, and the documentation's "What this does not cover" section lists it first.

fix

An uploaded spreadsheet could hold a scan worker for a minute

An .xlsx is a zip, and one repeated row compresses to about eleven bytes, so the 20 MB upload limit held nearly two million rows. SBOM scan read all of them into memory and then kept the 20,000 a scan allows — about 54 seconds and 0.9 GB for one upload, measured on the production parser. Two bounds now apply. Reading stops one row past the component cap, and the result's format says when the file was not read to the end. And because opening a workbook costs time in proportion to its expanded size however few rows are read, the archive is first expanded and measured — by decompressing it, not by trusting the sizes it declares — and refused past 64 MB. The worst accepted spreadsheet now takes about 3.4 seconds; anything larger is refused in a tenth of a second, with a pointer to CycloneDX or SPDX JSON.

fix

The hardening path ceiling is per call site, so one noisy line cannot hide the rest

The path ceiling added earlier the same day was a single total of 500, spent in the order paths were discovered. On one project a single line produced all 500 and no other call site was reported; on another, 65 call sites became 18. The note said the analysis stopped after 500 paths, which does not tell a reader that whole call sites were never examined. The ceiling now applies to each call site (20, VDB_HARDEN_MAX_PATHS_PER_SITE): every call site stays in the answer with a bounded list under it, and the note says so. A total ceiling remains at 2,000 as a backstop, and if it is ever reached the note says that call sites beyond it were not examined.

feature

vdb-mcp 0.2.5: vdb harden names the project

vdb-mcp 0.2.5 sends the project with every `vdb harden` analysis — the folder it was run on, or `--project NAME` — so new risk-path mail arrives as one message per project. Update with `pipx upgrade vdb-mcp` or `uvx --from vdb-mcp@latest vdb harden ...`.

fix

A large hardening analysis failed on the first run and succeeded on the second

The client waited 60 seconds for /v1/harden/analyze. A large project takes longer than that on the server, so the client gave up while the server kept going, finished, and cached the answer — and the next run returned it instantly. While it ran, the abandoned analysis held an API worker, which is why requests that followed it could fail outright. The client now waits 240 seconds (VDB_HARDEN_TIMEOUT), matching --vex. The cache key also now includes the version of the path composer, so a change to what composition returns invalidates earlier answers; the path ceiling shipped without that, and results computed before it kept being served. Analysis notes are de-duplicated — one project's run carried 496 notes, 14 of them distinct.

fix

New risk-path mail is one message per project, and a path is told once

When a dependency summary landed, `vdb harden` analyses were recomposed and any new path was mailed at once — per analysis and per package version. Every run was its own analysis, and every rebuilt summary looked like a new version, so one project could get the same "a new version of python-dotenv (1.2.2)" mail after each deploy. Paths are now recorded once per user, project and path (versions and line numbers left out), paths already shown in a report count as known, and each project with something new gets one email a day. The project is the folder you ran `vdb harden` on; `--project NAME` overrides it, and the analyze API takes an optional `project`.

feature

Comments on blog posts

Signed-in users can now comment under every post on /blog. Comments are plain text, up to 4,000 characters, ten a day per account, and show the first two characters of the author's email rather than the address. Anyone can read them, and the list is also available from GET /v1/blog/{slug}/comments.

feature

Blog: eight things our slopsquatting scores got wrong

A new post at /blog/what-we-got-wrong-scoring-slopsquatting collects every place the slopsquatting risk score said more than its evidence supported — spelling, npm scopes, registry name folding, stale scores, build attestations, our own first sighting, undated packages and unrecorded low verdicts — with what changed for each and what is still open.

fix

A package we cannot date is no longer cleared, and Go modules are dated

When a registry resolved a name but gave no creation date, slopsquatting scoring returned low. That is a safe verdict on absence of evidence, and it applied to every Go module: the Go proxy answers existence and nothing else, so any module that resolved — one pushed yesterday included — was cleared. An undated package now scores medium with the status "registry gave no creation date". Go modules are dated from deps.dev, which lists every version the proxy has served with its time; that time is the commit's, which the module author sets, so it is weaker evidence than npm's or PyPI's upload clock.

perf

The hardening analysis has a path ceiling, and its deduplication is no longer quadratic

Running the analysis over ten widely-used Python projects: four completed, and one of those produced 1,744 paths across 65 call sites, with 264 from a single line. Composition now stops at 500 paths (VDB_HARDEN_MAX_PATHS) and marks the result incomplete with a note saying which limit was hit — the same contract the engine already had for its work ceiling, where absence is not a safety determination. Path de-duplication compared each new path against every path already collected, which is quadratic and cost seconds near the work ceiling; it now uses a set. Coverage on those projects was 25-56% of dependencies, and every completed run came back incomplete, which is why the client now prints what was analysed alongside what was found.

fix

A build attestation or an early sighting no longer clears a young package

Two rules could score a package registered in the last 60 days as low, and neither held. A signed build statement (npm provenance, PyPI PEP 740) proves which repository built the artifact, not whose repository it is: anyone can set up a Trusted Publisher for their own repo, so it cleared exactly the package it was meant to catch. And "registered before the name was ever suggested" compared against our first sighting, which dates our sampling rather than the suggestion; near the start of collection it read as "registered before we started looking". Both are gone. Provenance is still published as `built_from`, as evidence rather than a verdict. Findings withdrawn under either rule are scored again by the daily cleanup and restored if they are still a risk, and names a run clears are now recorded so the rule that cleared them can be audited. The young-package status now reads "registered in the last two weeks" instead of claiming to know when the name was first suggested.

ui/ux

The hardening report counts call sites, and says what it analysed

Running `vdb harden` on our own API module printed nine findings. They were one line of code — a MinIO client constructor — counted once per tainted argument per sink kind. The report now groups by call site and names the file and line, so nine blocks become one entry with three sink kinds under it, each listing the arguments that reach it. The server had also been reporting coverage that the text output discarded: how many dependencies are on the graph, how many have a summary, and how many taint actually reached. That line now prints on every run, including runs that find nothing — a clean result with no coverage line is the one that invites a false sense of safety.

fix

A crate held under two spellings was withdrawn under one and kept under the other

crates.io treats a hyphen and an underscore as the same name: a lookup for cdx_rs answers with the crate cdx-rs. We stored a finding per spelling, so withdrawing cdx-rs as a real unrelated project left cdx_rs — the same crate — on the published list. The cleanup now asks the registry what it calls the package and withdraws a finding whose name the registry does not use, comparing normalised forms so PyPI's display names (PyYAML for pyyaml) are not mistaken for a different package.

feature

A package the registry will vouch for is no longer scored like an anonymous upload

Slopsquatting scoring now reads the registry's own build statement: npm provenance attestations (/-/npm/v1/attestations) and PyPI PEP 740 provenance (/integrity/.../provenance). Both are produced by the registry at publish time from a Trusted Publisher flow, so unlike a repository field in a manifest they cannot be set by the package itself. A package registered after a model started suggesting its name, but built by a named workflow in a named repository, now scores low with that repository in the status. Absent provenance changes nothing, and a registry that did not answer is never treated as "no provenance" — most packages have none, and a brief outage must not look like evidence. Attestation never rescues a name that does not exist or one registered and left empty. The repository appears as `built_from` in the public dataset. crates.io and Go have no equivalent API yet.

fix

A package that resolves was scored on its age alone, and that was wrong

Of the published findings that resolve rather than 404, almost all were misclassified. Three npm names reported as "registered but never published" are tombstones — published and then unpublished in 2016, 2020 and 2022 — which the registry reports as 200 with no versions; they are now scored medium with that status. More importantly, "registered recently" was the wrong question: cdx-rs, a directory-navigation tool, scored high because a model suggested its name for CycloneDX parsing days after it was published, and the official opentelemetry-configuration scored medium for being new. A name registered BEFORE a model ever produced it is somebody's project that shares the name, not a response to the hallucination, and now scores low. Stored scores are also re-evaluated: slop_cleanup reclassifies every resolving finding and withdraws the ones that no longer score as a risk, so a package cannot stay on the list by having been new once.

data

The full slopsquatting list is published, and linked from the page

/ai/slopsquatting shows the 100 highest-risk names; all of them are now published as CSV and JSON at github.com/0pstech/slopsquatting-dataset, regenerated daily, CC BY 4.0. Each row carries which model produced the name and the task it was answering. The page, the blog post and the homepage JSON-LD link to it, and the page now shows the keyless curl call so a visitor can run the gate without an account.

feature

The package gate and the slop list now answer without an API key

GET /v1/ai/slopsquatting and POST /v1/ai/check-packages answer without an Authorization header, on a per-address hourly quota: 60 reads, 20 checks, at most 5 packages per keyless request. Every other data endpoint still needs a key, an invalid key is still 401 rather than a silent downgrade, and a key removes both caps. Each keyless answer carries an `anonymous` block saying what is left and how to get a key. A keyless request for more than 5 packages adds a `truncated` block naming how many were not checked and is never answered PROCEED, so a partial answer cannot read as all-clear. Everything that required a key in 2026-09 still does; what changed is that trying the gate no longer requires an email first.

data

The slopsquatting findings are now exportable as a public dataset

`python -m vdb.slop_export` writes the live slopsquatting findings to slopsquatting.csv, slopsquatting.json, stats.json and a README, for publishing to a public repository. Each row carries which models produced the name, the task each was asked to solve, and when it was first seen — evidence that exists nowhere else; the widely cited count of fabricated package names comes from a 2025 paper appendix, and no continuously updated list is published anywhere. Withdrawn findings are excluded, and the field order inside a row is stable so a regenerated file only differs where the data differs.

fix

Maven packages returned no advisories when asked for by the other spelling

OSV names a Maven package group:artifact; the purl spec gives the group its own segment, pkg:maven/group/artifact. Advisories arrive under both spellings, and a lookup matched only the one it was given. So POST /v1/query and /v1/ai/check-packages could answer "no advisories" for a Maven package we hold advisories on, and every /pkg/Maven/ page rendered empty and set noindex while the sitemap listed it — about a third of /sitemap-packages.xml. Both endpoints now match either spelling. The sitemap also stops listing packages whose OSV ecosystem label carries a repository suffix ("Packagist:https://packages.drupal.org/8"), which cannot round-trip through a /pkg URL.

September 2026

fix

Slopsquatting findings: 21 of the top 100 were not attack targets

We re-checked the 100 highest-risk slopsquatting findings against their registries. 79 were names anyone could register; 21 were not: npm names written with capital letters (Socket.IO, which exists as socket.io), pieces of a sentence the parser took for names, and names inside npm scopes that have an owner. The collector now reads names from list items only, folds each to the form its registry stores (lower case on npm, PEP 503 on PyPI) and rejects what no registry would accept. It also reads the creation date of npm packages, which it had been missing, so a name registered in the last 14 days is scored high; a name that is registered but has never published a release is scored high; and a missing name inside an owned npm scope is scored medium. Existing findings that were never targets are marked withdrawn: their pages stay, and they are no longer returned by /v1/ai/slopsquatting or used by /v1/ai/check-packages.

feature

Blog: what we find, with the data behind it

VDB now has a blog at /blog, with an RSS feed at /blog/rss.xml. The first post looks up package names that language models recommended, taken from the 100 highest-risk entries on the public slopsquatting list, in npm, PyPI, Go and crates.io. 87 names returned 404; 83 of them belong to nobody, and 4 sit inside npm scopes that have an owner. The post lists all 87 with their status.

fix

The hosted MCP endpoint ignored the Authorization header — every tool refused a caller who had done everything right

Found by running our own re-test script against VDB itself. `claude mcp add --transport http vdb https://vdb.ai.kr/mcp` — the homepage's first instruction — can carry a key only as an `Authorization: Bearer` header, and the hosted server read the key from query parameters alone. So a remote client that sent the key on every request got `REFUSE / missing_api_key` from every tool, with a message explaining how to obtain the key it had just sent. The header is now read (an explicit `vdbApiToken` session parameter still wins, for Smithery), and every connection example on the site, in llms.txt and in the README shows the header. Two smaller things from the same run: `vdb harden --vex` on a lockfile with no advisories relayed a 400 telling the user to scan the lockfile they had just scanned — it now says "0 findings, nothing to subtract" and, when VDB does not cover part of the lockfile, how much; and the SBOM scan of a real project came back PROCEED with half its components unknown to VDB, which the verdict text did not mention — noted as the next thing to fix, not fixed here. What the run confirmed working end to end, with the published 0.2.2 client: doctor, the package gate with agent_action on every result, the SBOM merge gate, the single-file client fetched by pinned address and verified by hash, analyze → issued wrapper → verify CLOSED with a signature, and a wrapper with its internal-range check deleted staying OPEN.

fix

The `vdb harden` command was never installable — now it is, and it also comes as one file

Every page said `vdb harden ...`, and no package installed a `vdb` command: the vdb-mcp wheel's only entry point was `vdb-mcp`. Two independent evaluations typed `uvx --from vdb-mcp vdb harden --doctor` exactly as told and would have failed on that alone; both flagged that the CLI's origin could not be found. From vdb-mcp 0.2.2 the package installs `vdb`, so `uvx --from vdb-mcp vdb harden app.py --manifest uv.lock` and `pipx install vdb-mcp` both work as documented. The same evaluations also ran where pypi.org itself did not resolve, and then no fallback inside a client that never starts can help. So the whole CLI is now also served by the API as one stdlib-only file — `GET /v1/harden/client.pyz`, hash at `/v1/harden/client.sha256`, built from the code the API is running — for any Python ≥ 3.10 to run directly, fetched by pinned address if need be, no install and no key required to get it. Every API response now carries an `X-VDB-Build` header naming the commit that produced it, so a report can cite the build behind a verdict. And two documentation defects they found are fixed: the SBOM-scan example described GHSA-29mw-wpgm-hmr9 (a ReDoS, moderate) as a high command injection with `template` functions — that is GHSA-35jh-r3h4-6jhm, which the example now names — and the check-packages example showed `agent_action` and `because` on one result out of four when the API returns them on every result.

feature

No DNS in your sandbox? The client now reaches VDB anyway

Two independent teams evaluated VDB from inside a container that could reach the internet but not resolve names, and both stopped at "cannot reach the VDB API" — the tool they had delegated to could do nothing about it, and a human had to go and touch a firewall. That is the wrong shape for something meant to be handed to an agent. `vdb harden` and the local MCP server now retry against a pinned address (144.202.127.83, override with `VDB_API_ADDR`) when — and only when — name resolution fails. The retry bypasses DNS and nothing else: the TLS handshake still presents `vdb.ai.kr` and validates its certificate, so a pinned connection is exactly as authenticated as a resolved one, and nothing is ever trusted by IP. HTTP statuses, refused connections and TLS failures propagate untouched, because a 401 retried elsewhere would turn a revoked key into a second, mysterious failure. `vdb harden --doctor` now says which of your network, your key, or our service is at fault instead of one flat sentence, probes the pinned address when DNS is dead, and when that is unreachable too tells you that egress is blocked, not just DNS. `/v1/version` reports the git SHA and build time of what is actually running, so a deploy can be verified rather than assumed.

fix

An external review read our docs literally — and found real defects

Unable to authenticate from their sandbox, the same two reviewers audited the published surface instead, and what they found was true. The SSRF wrapper we hand people — and credit signed CLOSED evidence for — let carrier-grade NAT (100.64.0.0/10) through, and let 6to4 addresses that are 127.0.0.1 in disguise pass as global. The check is now an allowlist (`is_global`) rather than a blocklist of flags, and embedded IPv4 is unwrapped first (IPv4-mapped, 6to4, Teredo); the analysis cache was re-keyed so no stale wrapper is re-served. Redirects were neither handled nor disclosed: a call-site wrapper checks the URL you pass, not where a 3xx sends the client, so every net_request answer now carries a `redirect-following` residual next to the existing `dns-rebinding-window` one, and both are documented. The docs contradicted the code — /api said "no anonymous tier" in one section and described a per-IP trial in the next, and the MCP tool counts were wrong in both directions (it is seven hosted, ten local). Every data endpoint now requires a key, and a missing or invalid key answers `agent_action: REFUSE` with the URL where an agent can request a free one by email, so it can recover without a human. Finally, our own changelog told people 0.2.0 fixed `uvx vdb-mcp` while PyPI still served the broken 0.1.2; 0.2.0 is now actually published, and the published prompt sends the key on every call.

August 2026

feature

VEX — which of your advisories can actually be reached

A scan on a real project returns hundreds of advisories, nearly all of them about code the application never puts attacker-controlled data into, and a queue nobody can work through is a queue everybody ends up ignoring. `vdb harden ./src --manifest uv.lock --vex` now answers which ones can be reached, and emits it as an OpenVEX v0.2.0 document so any tool that reads the format can subtract the same set. Each run also gets a URL, because the result only changes anyone's behaviour when a team looks at the same list. Point it at the source tree rather than a single file: a not_affected claim is only as wide as the code behind it, and deriving "nothing reaches urllib3" from one file while forty other modules import it would be wrong in a format built to be trusted automatically — below two analyzed files every such claim is withheld. Two things it deliberately never says. It never emits `affected`, because reaching a package is not reaching the specific function an advisory names and stating otherwise would be a guess wearing the clothes of a finding. And it never uses the `vulnerable_code_not_in_execute_path` justification, because the analysis follows tainted data: a package the app calls with only its own constants is still executed. The justification it does use, `vulnerable_code_cannot_be_controlled_by_adversary`, says exactly what was determined. Reachability comes from static per-package summaries, so a relation the analyzer missed produces a false negative; every reachability product has that property and this one states it on the document. Evidence for triage order, not proof of non-exploitability. Also available as `vdb_vex` over MCP in local mode.

fix

uv.lock and poetry.lock were being scanned wrong, not rejected

Neither format had a parser. That would have been fine if it produced an error, but the scanner falls back to requirements.txt parsing, which does not fail on TOML — it matches a couple of stray `key = "value"` lines and returns a confident, tiny component list. A real uv.lock with six packages came back as two components and zero findings, and a user reading that concludes the project is clean. Both formats are parsed properly now, including uv's virtual workspace root (which is the project being scanned, not a dependency), and TOML lockfiles are sniffed before the requirements fallback so an unhelpful filename cannot route them into the wrong parser. Silent partial coverage on a security scan is worse than an error, because an error gets investigated.

feature

MCP is now the way to connect — one command, and two new tools

Connecting used to mean pasting a prompt, and MCP was listed at the bottom of /connect under "other ways". That was backwards: a pasted prompt has to be repeated every session and competes for context, while `claude mcp add --transport http vdb https://vdb.ai.kr/mcp` is one command that stays. MCP now leads on both the homepage and /connect, and the prompt remains as a fallback for tools that cannot speak it. Two tools were missing and are now there. `vdb_scan_lockfile` is the merge gate — checking the packages someone chose misses the transitive ones nobody did, which is usually where the risk is. `vdb_harden` decides whether attacker-controlled data can reach a dangerous operation through your dependencies, with no CVE required. `vdb_harden` is offered only when the server runs on your own machine (`uvx vdb-mcp`), and that is deliberate rather than a limitation: it abstracts your file before anything is sent, and that has to happen on the machine that owns the source. On the hosted endpoint the file paths would be ours, and the only way to make it work there would be to ship your source to us. Two existing tools were also quietly wrong — `vdb_check_package` and `vdb_check_mcp_server` called endpoints that return raw records and no verdict, so they disagreed with the bulk tool about what a check even means. Both now go through the same gate. Failed calls through MCP answer `agent_action: REFUSE` as structured data rather than an error string, so failing closed does not depend on a model reading prose carefully. /connect itself went from 983 lines and a 20,778-character prompt to 336 lines and about 1,000.

fix

uvx vdb-mcp was completely broken — fixed in 0.2.0

The published package declared `mcp>=1.0.0` with no upper bound. The SDK's 2.0 release removed the decorator-based low-level API the server is written against, so a fresh install resolved mcp 2.x and died at import with an AttributeError before serving a single request. There is no useful symptom for this: the client simply reports a server that will not start. Anyone who tried the `uvx vdb-mcp` path — one of the two connection routes the homepage advertises — got nothing. The dependency is now pinned below 2 in both the package and the container image, and a test fails if that bound is ever removed without a matching migration to the 2.x API. If you had this configured and it was silently doing nothing, upgrading to 0.2.0 fixes it.

fix

npm slopsquatting detection was never firing — fixed

The npm registry probe asks for the abbreviated packument, which deliberately omits the `time` block. Our age heuristic read `time.created`, so every npm package resolved to "publish date unknown" and fell through to the safest verdict. In practice that meant the npm half of the freshly-registered-squat check had never fired once: a name registered yesterday scored the same as lodash. Two cheap signals replace it — the release count from the same response, and last-week downloads from an 82-byte endpoint. Two consequences. A name that is registered but has never published a release is now high risk: it resolves, so a naive existence check passes, and whoever holds it can publish anything under it later, under a name the model already trusts. And "we could not establish age or usage" no longer reads as "low" — unknown is not the same as safe, and treating it as safe is what hid this for so long.

api

Responses now carry the verdict — agent_action, and a 60% shorter prompt

Every result from /v1/ai/check-packages and /v1/sbom/scan now carries `agent_action` (REFUSE, CONFIRM, or PROCEED) and a short `because` citing the evidence. The gate rules that produce it — not_found refuses, KEV or EPSS at or above 0.5 refuses regardless of CVSS, an unverified MCP publisher asking for exec or filesystem scope refuses even with a clean record — used to live in the prompt you pasted, which meant the model had to hold them in context for an entire session. It didn't: context decay quietly switching the gate off was the most common way this stopped working. Deciding server-side means the instruction arrives with the answer, on every call. The published prompt drops from 2,617 to about 1,040 characters (down 82% from the original 5,420), and 14 of 14 live agent scenarios pass. Error responses decide too: a 429, a timeout, or an exhausted trial answers `agent_action: REFUSE`, so failing closed no longer depends on the model remembering to. One policy change came out of this: an MCP server VDB has never seen is now REFUSE rather than CONFIRM. An unknown library is a contained bet; installing an unknown MCP server hands an unvetted publisher the session's tools, filesystem, and secrets. Same verdict, very different exposure.

ui/ux

Homepage rebuilt around three moments, and a new /harden page

The homepage had grown into a feature inventory: a hero, a search box, a prompt, a changelog, a VS Code card, an SBOM Watch card, four coverage pillars (two of them labelled "coming soon"), two more stat cards, six vulnerability cards, a three-column competitor comparison, and a promo popup. Every block was individually defensible and the sum said nothing. It now tells one story — the three moments a bad dependency gets into a codebase, and the fact that VDB is at all three: when your agent picks it (the name may not exist), before it merges (the risky package is usually one nobody named), and after it ships (a new advisory, or a risky dataflow path that has no CVE at all). Each moment carries a concrete example rather than a claim, because "we detect supply-chain risk" means nothing and `pkg:npm/react-hook-forms → not_found` means everything. The primary action is the one-paste policy prompt; search moved below the value story, and the "coming soon" pillars were cut outright — advertising the absence of a feature is worse than staying quiet about it. Call-site hardening also gets a real destination at /harden, covering what it returns, exactly what does and does not leave your machine, how a path is decided, how re-verification proves you closed it, and what it structurally cannot cover.

change

VDB is now English-only — /en/* redirects to /

The site used to render every page twice, Korean at /foo and English at /en/foo. That is over: there is one document per URL now, and every /en/* address 301s to its unprefixed form, so existing links and bookmarks keep working and search engines move the ranking signal to the surviving URL. The change goes all the way down rather than stopping at the navigation bar — emails (verification, API key, SBOM Watch alerts), API error bodies, and the OpenAPI documentation are English throughout. The `message_ko` field is gone from 401 and 429 responses; clients read `message`, which is unchanged. Two upshots worth knowing: a page no longer needs a hand-written /en mirror file, which was a bug we shipped three separate times, and the duplicate-canonical ambiguity that Search Console kept flagging no longer has anything to be ambiguous about.

feature

Call-site hardening — find risky dataflow before a CVE exists

VDB can now answer a question no advisory database can: given this call site in your code, can attacker-controlled data reach a dangerous operation deep in your transitive dependencies — whether or not a CVE has ever been published for it? Run `vdb harden app.py --manifest uv.lock`. Your source never leaves your machine: the file is abstracted locally into a call-graph IR (identifiers renamed, literals reduced to shapes, bodies dropped) and only that IR plus your lockfile is uploaded — `--emit-ir` shows you exactly what would be sent, with no network call. Paths are decided by composing precomputed per-(package, version) taint summaries over the version-resolved dependency graph, across six sink classes: path operations, command execution, network requests, deserialization, regex evaluation, and global-state mutation. Summaries are built once per version from the real PyPI artifact by AST parsing only — package code is never imported or executed. The fix you get back is applied at your call site; VDB never asks you to patch, fork, or pin the dependency, because you can always fix your own boundary and usually cannot fix someone else's package. After you apply it, `--verify` re-analyzes and issues signed evidence bound to the dependency graph hash — and partial defense does not count as closed: apply two of the three defenses a network sink requires and you get `missing=block-internal-ranges`, not a false attestation. Every answer carries its residual risk: install-time malware, background exfiltration, and unanalyzed dynamic loads are never marked defended, and hops that had to be assumed conservatively lower the stated confidence instead of quietly disappearing. When a new version of a dependency is summarized, affected analyses are recomposed and you are emailed only about paths that were not there before.

ui/ux

Connect prompt ~53% shorter — less paste friction, same gating behavior

Feedback said the one-line connect prompt was too long to paste comfortably. Rewrote it as terse rules — dropping the per-rule explanations, justifications, and duplicate examples — cutting EN 5,420→2,557 and KO 3,691→1,728 chars (~-53%). The policy skeleton is intact: MUST-strength language, the check-packages/sbom-scan/request-key routes, purl examples (%40/huggingface/data/maven/mcp), per-risk actions (not_found/high/medium/unknown/low), KEV+EPSS auto-refuse, MCP scope orthogonality, fail-closed, lifetime trial, and the no-inventing-keys guard. Verified no behavioral regression: 15 structural tests + 28/28 on the live prompt-eval battery.

fix

check-packages — safe_upgrade suppressed when the pinned version already meets it

/v1/ai/check-packages returned safe_upgrade (the package-wide highest fix) without comparing it to the queried version, so clients told users already on a safe pin to "patch to X" where X was their current version — a real python-multipart 0.0.31 report. safe_upgrade is now null when the pinned version meets the target (kept conservatively when versions can't be compared). VS Code extension 0.4.0 adds the same guard client-side plus explicit "Safe version: X (you have Y)" wording.

July 2026

feature

VS Code extension 0.4.0 — inline diagnostics for pyproject.toml & pom.xml

The extension's live diagnostics were skewed toward package.json/requirements.txt; this release widens them. pyproject.toml: PEP 621 ([project] dependencies / optional-dependencies, single-line and multi-line arrays) and Poetry ([tool.poetry.dependencies] plus group tables), with diagnostics on the exact dep line. pom.xml: each is checked as pkg:maven/group/artifact@version, ${property} versions resolve from , BOM-managed (version-less) deps are name-checked, and test-scope deps are skipped. The "pin to latest" quick-fix now supports Maven Central too.

feature

Remote MCP endpoint — https://vdb.ai.kr/mcp (no install)

Opened a remote MCP endpoint so you can connect to VDB with just a URL — no install. Any streamable-HTTP MCP client (Claude Desktop, Cursor, …) takes { "vdb": { "url": "https://vdb.ai.kr/mcp" } } and gets all 6 tools (package/bulk check, lookup, search, MCP-server trust, slop list). Anonymous sessions share the per-IP free trial; users pass their own API key via session config for per-account metering. The local uvx vdb-mcp path still works too.

feature

vdb-mcp 0.1.1 — published to PyPI + listed in the official MCP registry (kr.ai.vdb/vdb)

vdb-mcp is now on PyPI — any MCP client connects to VDB with a single "uvx vdb-mcp" line. Also listed in the official MCP registry (registry.modelcontextprotocol.io) under the kr.ai.vdb/vdb namespace, verified via vdb.ai.kr DNS (ed25519) — directories that crawl the registry (PulseMCP, Glama, …) pick it up automatically. License: Elastic License 2.0 — free to use, including commercially; only reselling it as a hosted/managed service is prohibited.

seo

Added llms.txt + a citable “Quick fix” block on advisory pages

AI crawlers already index our advisory pages at hundreds of IPs per day. To make answer engines (ChatGPT/Claude/Perplexity) cite VDB precisely, we added /llms.txt (site guide + agent-facing API summary + citation guidance) and a “Quick fix” block at the top of every advisory page carrying the advisory id, package, and exact upgrade command in one liftable sentence.

feature

vdb-mcp installable via uvx + SBOM-scan GitHub Action

Repackaged the MCP server as an installable vdb-mcp package — one line in your client config ("uvx vdb-mcp"), zero configuration, hosted vdb.ai.kr by default (6 tools: package/bulk check, lookup, search, MCP-server trust, slop list). Also added a CI GitHub Action: one step (uses: 0pstech/vdb-scan-action@v1) scans your lockfile, gates the build on a fail-on severity threshold, and writes a job-summary table with advisory links and upgrade commands. A free API key is recommended in CI — anonymous calls share the runner IP pool's small lifetime trial.

ui/ux

Trial-exhausted responses are now AI-agent-actionable

Reworked the trial-exhausted bodies on /v1/ai/check-packages (429) and /v1/sbom/scan (401). They now always carry error: free_trial_exhausted plus signup_url, request_key_url, docs_url, and a bilingual message. The key addition is request_key_url — the passwordless path where a browserless AI agent just asks its human for an email, POSTs it, and a free key arrives by mail. The old body only said "sign up", which agents answered by retrying the same call hundreds of times and then giving up.

fix

SBOM Watch email language now defaults to your signup language

The language you sign up in (ko/en) is now stored on the account (users.lang). When an SBOM Watch registration carries no language signal, alerts follow it. Priority: explicit lang field at registration → the request's X-VDB-Lang/Accept-Language → signup language → ko. Previously, watches registered via the JSON API always received Korean mail — accounts that signed up in English now get English alerts.

feature

SBOM Watch — email alerts when a dependency in your registered SBOM gets an advisory (members)

/v1/sbom/scan was a point-in-time snapshot. Members can now register an SBOM (or lockfile) at /sbom-watch; a worker re-checks the stored components against the vulnerability DB every hour and emails you only when a NEW advisory matches. Each mail carries the facts (package, version, severity, KEV status) and the fix (fixed versions, per-ecosystem upgrade command, package-wide safe upgrade target). Every (SBOM, vulnerability, package) triple is notified exactly once — no repeat spam. Limits: 5 watches per account, 20,000 components per file. API: POST/GET/DELETE /v1/sbom/watches.

fix

Removed scan false positives caused by version-resolver failures

When scanning range specs rather than pinned versions (e.g. >=0.115, ^0.21.0 in requirements.txt / package.json), a failed registry lookup (offline, blocked, or rate-limited) left the spec unresolved, so the finding was kept to err safe — over-reporting (e.g. FastAPI's CSRF, fixed in 0.65.2, still flagged high at >=0.115). A finding is now excluded only when it is provably true, without any network call, that the spec's entire satisfiable version set lies outside every affected interval. The check is one-directional: it can remove a false positive but never hides a real vulnerability.

fix

False-positive fix for advisories with bad upstream data (uvicorn CVE-2020-7694)

GHSA-33c7-2mpw-hg34 (uvicorn log injection) is recorded upstream as "all versions affected, no patch" even though maintainers fixed it in 0.11.7 — flagging every uvicorn release as high. A serving-time correction layer now suppresses provably-fixed versions and attaches an override_note explaining any divergence from raw OSV.

June 2026

feature

VS Code & Cursor extension launched — inline slopsquatting / CVE / registry checks as you edit

Open or save package.json / requirements.txt / Cargo.toml / go.mod / SBOM and the extension extracts every dependency, scores it via POST /v1/ai/check-packages, and draws the verdict inline. Slopsquatted (non-existent) names flag as errors; known CVEs get a one-click upgrade to a safe version. Works with no account. Published to Marketplace and Open VSX.

security

SBOM upload — five-layer hardening for malformed / huge / deeply-nested files

Concentric defenses on the /v1/sbom/scan upload path. (1) Content-Length pre-check rejects oversized uploads before the read. (2) The read itself is capped to SBOM_MAX_BYTES+1 so a lying body can't sneak past. (3) Filename is sanitised — basename only, ../, NUL, and control bytes stripped, 255-char cap, so nothing weird flows into the format detector or audit log. (4) Parser failures broaden to catch RecursionError (deeply-nested adversarial JSON), MemoryError (billion-laughs-style blowups), and stray decode errors — all map to 400/413 instead of 500. (5) Post-parse filter drops purls / versions over 1024 chars or carrying control bytes, then caps total component count per call (5k anonymous, 20k authenticated). A new components_truncated field in the response tells the caller how many rows were dropped so they can decide whether to split the SBOM. 14 unit tests cover both helpers.

ui/ux

SBOM scan — 3 free anonymous scans per IP + helpful 401 with signup info

/v1/sbom/scan previously required an API key on the first call, returning a terse "missing bearer token" 401 with no hint at the fix. Anonymous callers now get 3 lifetime scans per IP (check-packages is 50), and the response carries a trial { used, limit, remaining } block. After exhaustion the 401 body becomes { error: "free_trial_exhausted", message, signup_url, docs_url } so callers know exactly what to do. The cap is tighter than check-packages because SBOM parse + bulk vuln JOIN is materially more expensive per call. /api docs updated with the new behaviour + sample response.

docs

About page — CISA KEV / FIRST EPSS added to the data-sources table

The "data sources" table at the bottom of /about now lists CISA Known Exploited Vulnerabilities and FIRST EPSS, with their daily cadence and upstream links. Visitors can audit where the kev / epss fields in our responses come from. The sample-response JSON on the same page also now shows both fields.

data

CISA KEV + FIRST EPSS signals on check-packages / SBOM responses

The new daily kev-epss collector pulls CISA Known Exploited Vulnerabilities (~1.1k entries) and FIRST EPSS scores (~250k CVEs) into ai_signals. /v1/ai/check-packages and /v1/ai/sbom-scan now return kev (boolean) and epss (0..1) per advisory. KEV=true or EPSS≥0.5 escalates the verdict to high regardless of base CVSS — Standard-tier consumers automatically catch "low CVSS but already being exploited" advisories. Sort order also changes to KEV → EPSS → CVSS so truncated clients (read only vulns[0]) get the right top hit.

data

Data milestone — crossed 500k AI datasets

Total collected AI datasets crossed 500,000 for the first time. Auto-emitted by the scheduler.

data

Data milestone — crossed 250k vulnerabilities

Total collected vulnerabilities crossed 250,000 for the first time. Auto-emitted by the scheduler.

ui/ux

Footer — partnership & service contact line added

A third slot alongside copyright and bug-report. mailto: link in font-mono; wraps cleanly on narrow viewports.

i18n

Comprehensive ko/en drift fix across account, auth, connect, api, admin pages

About twenty pages where /en/* mirrors leaked Korean strings are now fully bilingual. 90+ new i18n keys; ko/en check is now a standing rule for every page edit.

fix

Package matching — auto-normalise crates.io / RubyGems / Go user input to purl-spec canonical types

User input like pkg:crates.io/tokio now resolves against pkg:cargo/tokio internally. Closes a silent false-negative gap.

May 2026

feature

Reachability prompt split — members-only Standard + Reachability (§11)

Even with a CVE present, downgrade to advisory when the user's code path doesn't reach the affected function.

feature

MCP servers — scope drift detection + pkg:mcp/ identifier support

Silent capability elevation in minor releases is surfaced via the new mcp.scope_drift response field.

feature

AI dataset gating — pkg:data// covers license + PII signals in the unified gate

Full Hugging Face dataset catalogue mirrored. License + PII alongside model weights_format in one gate.

feature

SBOM merge gate — coverage_ratio in response + self-SBOM at /sbom.cdx.json

Coverage ratio surfaced on lockfile scan responses; VDB's own SBOM published for external verification.