Watch the open-source packages your codebase depends on and get alerted the moment a new security advisory (including typosquat/malicious package takedowns) is published for one - built on the free OSV.dev vulnerability database. Only new advisories are reported after the first baseline check.
0.2.6 — Third cold review: every locked copy is checked
Fixed
A vulnerable copy nested in a lockfile was invisible. A real
package-lock.json routinely holds two versions of the same package — the
current one at the root, and an old one nested under a dependency that pins
it (node_modules/some-old-lib/node_modules/lodash at 3.10.1). Deduplication
ran on the package name, so only the root copy was checked and the tree
came back clean. Every distinct locked version is now its own check; rows
carry a version field saying which copy the finding is about. Verified
live: the nested lodash@3.10.1 now surfaces its 6 vulnerabilities.
In watch mode, a locked package's state is keyed by version
(npm/lodash@3.10.1); a name-only watchlist keeps its existing key and
history.
A version range in the hand-listed packages input was sent to OSV
verbatim ("version": "^4.17.0"). It is now treated like a manifest range
— checked by name — with a warning suggesting an exact version.
Under a tight max_results, the typosquat row was the first one cut.
Sorting was severity, then type alphabetically, which put advisory before
typosquat. Within a severity a typosquat now sorts first: "you are about to
install a booby-trapped package" is the most actionable row we ever emit.
A newly-listed twin of an already-reported advisory was re-fetched on every
run. It was correctly suppressed but never persisted. It is now recorded
as seen the first time.
Changed
Advisory detail lookups retry briefly on HTTP 429 and 5xx (honouring
Retry-After) instead of degrading straight to a "(no summary available)"
row during a big first audit.
Known limitation (documented, not a bug)
@evil/lodash — a scoped npm package riding a popular unscoped name
(dependency confusion) — is not a typosquat and is not flagged. @evil/lodahs
is.
0.2.5 — Second cold review: one row per vulnerability, no more phantom alerts
A second independent review of 0.2.3 found four further defects, three of
them affecting what the client is billed for. All fixed, all covered by
regression tests, all re-verified against the live OSV.dev API.
Fixed
Duplicate rows for the same vulnerability. OSV lists the same flaw once
per source — a PYSEC-… record and its GHSA-… twin, aliased to each other
— and each became its own billed row. On a real run that was 91 rows for 58
vulnerabilities (+57 %), and the twin was almost always the empty one (no
summary, severity UNKNOWN). Records are now grouped by alias and one row
represents the vulnerability: the richest record supplies vuln_id,
summary and reference_url; every other id lands in aliases; severity is
the worst across the group, malicious is true if any record says so,
published is the earliest. A new source_records field lists every OSV id
the row stands for. In watch mode a newly-listed twin of an advisory you
were already told about is no longer reported as new.
An OSV.dev outage wiped the watch-mode state. A failed query was read as
"no advisories", the state was overwritten with an empty list, and the next
run reported every historical advisory as NEW — old CVEs dated 2020 arriving
as fresh alerts, all billed. Packages OSV could not be asked about are now
skipped for the run with their state left exactly as it was, and the log
says so.
Manifest version ranges were sent to OSV as if installed.
"lodash":"~4.17.0"
in package.json was queried as lodash@4.17.0 — 10 advisories
— when 4.17.21, the version that range actually installs, has 3. Same for
composer.json and Cargo.toml (where a bare "1.36" is a caret range).
Only an explicit pin (==1.2.3, a bare 1.2.3 in package.json/composer.json,
=1.2.3 in Cargo.toml) or a lockfile entry is now treated as a version; a
range is queried by name only, so nothing is reported that the installed
version may already fix.
The lockfile lost to the manifest. When both package.json and
package-lock.json were pasted, the manifest's range floor won because it
came first. A locked/exact version now always beats a manifest range,
whatever the paste order.
Rows dropped by a partial push were marked as seen. State is now derived
from the rows push_data actually accepted.
17 % of rows had no summary although OSV had one. PyPA records put the
description in details, not summary; the first sentence is now used. In
practice these were the PYSEC twins above, so they are merged away anyway.
Changed
checked_at is now a full UTC timestamp (2026-09-09T12:46:30Z), not a
date.
CVSS v3 vectors are preferred over v4 regardless of the order OSV lists
them (v3 is computed exactly; v4 is only banded).
Manifest parsers report whether a version is exact (exact), which the
dedup step uses for precedence.
0.2.3 — Precision & correctness pass on the 0.2 features
A review of the 0.2.0 code found four real defects. All are fixed and covered
by regression tests.
Fixed
Typosquat false positives. 0.2.0 flagged anything within two edits of a
popular package, which meant task, mask, flash, block, clock,
star, lang, goto and mess were all reported as CRITICAL typosquats.
Detection now requires the difference to be an actual typing-error class —
a transposition, a missing/doubled/inserted character, an adjacent-key slip,
or a look-alike glyph — against a package name long enough to be worth
impersonating, and never fires on a name that is itself an ordinary word.
A substitution of two unrelated characters (flask→flash) is a different
word, not a typo, and is no longer reported. Real squats that 0.2.0 missed
(reqeusts) are now caught.
Repeated alerts for the same finding in watch mode. When a name sat one
edit from two popular packages, which one was reported depended on set
iteration order, which is not stable between runs. That name feeds the state
key, so the same finding re-alerted on every run. Candidate selection is now
deterministic.
Findings silently lost in watch mode. State was recorded before the
max_results cap was applied, so anything truncated was marked as reported
and never came back — with the free preview capped at 25 rows, most of a real
lockfile's findings. State is now written after the push and only for rows
actually delivered; the rest stay pending and are reported next run, with a
log line saying how many were held back.
Pipfile.lock returned nothing. It was routed to the TOML Pipfile
parser even though the lockfile is JSON. It now has its own parser.
Changed
Typosquat rows explain the exact slip ("two characters swapped ('sh' -> 'hs')
away from 'lodash'") instead of a bare edit distance.
checks values that aren't recognized now produce a warning instead of
silently running every check.
Name matching is ~8× faster (the popular-package index is built once at
import instead of once per dependency): 800 packages went from 10.0s to 1.3s.
Major upgrade. Everything from 0.1 still works unchanged; the hand-listed
packages input and the scheduled diff-based alerting are untouched.
New
manifest_files input — paste a real dependency file straight from your
repo instead of hand-listing every package. Supported: requirements*.txt,
Pipfile, poetry.lock, pyproject.toml, package.json,
package-lock.json, yarn.lock, pnpm-lock.yaml, go.mod, Gemfile,
Gemfile.lock, composer.json, composer.lock, Cargo.toml, Cargo.lock.
Parsed with the standard library only — no new dependencies pulled in.
Offline typosquat / name-confusion check — flags dependency names that
are a near-miss of a very popular package (reqeusts → requests,
python-dateutil typed as dateutil, an added -js suffix, an l↔1
swap, a doubled letter…). OSV can only warn about a malicious package after
a takedown advisory exists; this catches the install-time trap before that.
Runs with no API call.
mode input — watch (default, unchanged: scheduled, silent first-run
baseline, then only new findings) or audit (one-shot: report every current
advisory + name-confusion hit now, no baseline suppression) for scanning a
repo on demand.
Exact-version matching — when a manifest pins a version, it's passed to
OSV so a dependency only matches advisories that actually affect that
version (far fewer false alarms than the old name-only query).
Changed
Every row now carries a type field: "advisory" or "typosquat".
severity is normalized to CRITICAL / HIGH / MEDIUM / LOW /
UNKNOWN from whatever OSV provides (a CVSS vector, a GHSA word, or
nothing). Rows are returned severity-first.
Advisory rows gained aliases, malicious (true for OSV MAL- malicious
package records) and reference_url.
/v1/querybatch is now chunked at 500 packages per request so a full
lockfile with hundreds of entries works in a bounded number of calls.
Not changed
Scheduled watch mode behaviour, the packages input, the state store,
the free-tier preview cap (25 rows), the failure-alert email, empty-run =
no charge.
0.1 — Initial release
Watchlist of packages → OSV.dev advisory check on a schedule → report only
advisories new since the last run.