Upgrade note.
cve-lite . --check-overridescould report a clean override audit it had
never completed. OA010, the rule that finds stale override floors, was unreachable from the
documented scan path and no-oped silently, and any rule that could not run said nothing at all,
so missingnode_modulesor a registry failure produced a result that looked like a pass.
Both are fixed: the flag works on the scan path, and rules that did not run are now named on
every output surface. If you gate CI on--check-overrides, re-run it after upgrading.
Added
-
Contextual prioritization signals on each verbose finding:
reachability,exposure
and, when--usageran,usage.reachabilityreports the dependency path depth
(path_depth_2upward) rather than restatingrelationship, and falls back to
direct_dependencywhere there is no depth to report.exposureis a labelled
heuristic, flagging a finding whose path runs through a request-handling package, and
it earns its keep: onexamples/nestit marks 12 of 68 findings, around one in six
consistently across juice-shop, nest and analog, which is a cut that changes what you
read first.usageis omitted entirely on a default scan rather than printed asn/a
on every row. Deliberately kept as a separate layer from the EPSSprioritySignal,
because that measures global CVE exploitability while these are project-local. Thanks to @alex-powell. -
The eleven remaining braceless single-line
ifstatements insrc/cli/args.tsand
src/index.tsnow carry braces. Proven behaviour-neutral three
ways: AST canonicalization of both versions, a compiled-output diff showing only the expected
brace pairs in exactly two files, and 83 CLI invocations against both builds with identical
stdout, stderr and exit codes once temp-dir randomness and output timestamps are normalised.
Thanks to @prx-my.Worth keeping from the review: #420's body claimed thirteen of these, eight in
args.tsand
five inindex.ts. Measured on main it was ten and one, so eleven, and the issue body has
been corrected. Roughly 830 remain across 93 files, so ESLintcurlyis the route
rather than hand-editing. -
The seven near-identical
--X cannot be used with --Ymessages insrc/cli/validate.tsare
now oneconflictMessage(flag, withFlag)helper, so the wording cannot drift as flags are
added. Thanks to @prx-my. -
OA008 no longer calls every flagged copy "below the floor". Copies are partitioned by where
they actually fall: below the range, above it, or outside the accepted intervals of a union.
The cause hint, that a parent likely declares the dep as exact and wins resolution, is tied to
the direction of the mismatch rather than the shape of the pin, so a concrete pin always
carries it, a range pin carries it when at least one copy is below, and it is omitted where it
does not apply.positionAgainstRangealso now compares against the raw range instead of
validRange's normalized output, which had silently madeincludePrereleaseineffective on
lower bounds. Thanks to @anbv29, whose first contribution this is, and
to @prx-my who reviewed it.Detection is unchanged, verified across 280 pin-and-version combinations: identical findings,
severities, paths and version lists, with onlydetailsdiffering and only on range pins.
Follow-ups filed as #1255 (regression test for the prerelease fix, currently unpinned) and
#1256 (three behaviour-neutral tidy-ups).
Changed
-
New override-hygiene rule OA011: an override floor that exceeds every declaring
package's range. An override pinned above what any parent actually declares is not
protecting anything; it is forcing a version nobody asked for and can push a dependency
onto an incompatible major. The motivating case is real rather than theoretical: an
unbounded@hono/node-serverfloor in CopilotKit's tree, which a human reviewer there
found by hand."OA011"joins theOverrideRuleIdunion. Thanks
to @MRX-72. -
The update-notice install command is a named constant rather than an inline string. Thanks to @VolodymyrLinuxovich, whose first contribution this is.
-
The 39 inline CLI flag literals in
src/cli/args.tsare now named constants in
src/cli/flags.ts, so a typo becomes a compile error rather than a silently
unrecognised flag. Verified behaviour-neutral by parsing all 39 flags through both
builds and diffing the resulting options objects: zero differences. Thanks to @prx-my. -
The
--allfindings table no longer carries anIDscolumn. It rendered one
advisory id plus a+Nsuffix, too narrow to read and too narrow to copy from. A
CVE count was tried as a replacement and rejected: two of 68 findings on
examples/nestcarry a GHSA with no CVE alias, so the column would have printed
0beside a genuine critical finding. The ids remain in--verbose, JSON, SARIF,
CycloneDX and the HTML report. -
A dev finding no longer takes two rows in the findings table. The
Typecell
forced a line break before· dev, which saved 6 characters of width and cost up
to 47 percent of the table's height:examples/nestwent from 131 rendered lines
to 70 andexamples/lint-stagedfrom 30 to 16, since 61 of 68 and 14 of 14 of
their findings are dev dependencies.transitive · devnow renders on one line,
unchanged in content.
Fixed
-
An unconfirmed fix version is no longer printed as a runnable upgrade command. When the
advisory source cannot confirm that a candidate version is actually safe, the recommended
action now readsVerify a safe version of <pkg> before upgrading.in both the terminal
and the HTML report, rather than handing overUpgrade <pkg> to <version>+for a version
we could not vouch for. The distinction between "unconfirmed" and "unset" is now a named
predicate,isUnconfirmedFixinsrc/utils/finding.ts, used by both formatter paths and
the reporter so the three cannot drift. Thanks to @XonkelX, whose
first contribution this is. -
Compact output no longer hides findings and fix-command groups silently. It shows three
finding blocks and three command groups, and previously said nothing about the rest, so a
clean-looking scan could be concealing fifty findings. It now discloses what it withheld
and how to see it:Showing 3 of 53 findings. Run --verbose --all to see them all.and
4 more command groups (medium, low). Run --verbose to see them.The counts are derived
from what was actually printed, the finding notice is suppressed under--allwhere the
table already lists everything, and neither notice appears when nothing was withheld.
Verified againstexamples/juice-shop(53),examples/nest(68) andexamples/analog
(109). Thanks to @prx-my. -
PD001 and PD002 no longer report phantom dependencies that a workspace member
legitimately declares. Declaration resolution walked only the root manifest, so
scanning a monorepo root flagged every package declared in anapps/*or
packages/*manifest as a transitive-only phantom. It now resolves against the
nearest enclosing workspace member and falls back to the root. The same pass also
counted type-only imports as runtime imports, so a JSDoc/** @type {import('x')} */
or a TSimport type ... from 'x'made a transitive-only package look imported but
undeclared. Onexamples/analogPD002 drops from 10 findings to 5, and all five
removed are declared inapps/docs-app/package.json.A regex-literal bug in the shared
stripCommentspass was fixed in the same PR and
was the more serious half: inconst sep = /[/*]/;the/inside the character
class opened a block comment that ran to the next*/or to end of file, silently
dropping every import below it. That pass also feeds OA009 and--usage/--only-used,
so one glob regex in one file was degrading four features.
Thanks to @osfv, and to @alamb-hex who reported both defects. -
OA009's declaration guard now resolves declarations the way PD001 does. The guard
suppresses OA009 when an override anchors a source import that is not declared, since
removing the override would create a phantom PD001 would report. It resolved against the
root manifest only, while PD001 resolves against the nearest enclosing workspace member
as of #1114, so the two rules disagreed: a package declared inapps/web/package.json
made PD001 correctly fall silent while OA009 kept suppressing itself, and a genuinely
redundant override floor went unreported by either rule. The guard now calls the shared
undeclaredImportFilesfromphantom-utils.Same lines, second smaller fix: OA009 carried a private
getDeclaredPackagesreading
onlydependenciesanddevDependencies, while the shared one reads four sections, so
a package declared only as apeerDependencywas invisible to the guard and suppressed
the rule. That duplicate is deleted androotDeclaredis computed once rather than per
override entry. On a workspace fixture where the member declares the package and the root
does not, main reports a false PD001 and no OA009; this reports OA009 and no PD001. -
--check-overrides --sarifno longer fails outright. OA011 shipped with its detector wired
in but was never registered in the rule table the SARIF writer maps a rule id to, and that
writer throws on an unknown id rather than degrading, so one missing entry aborted the whole
scan and wrote no file. The error also fell through to the generic network hint, advising a
corporate SSL proxy and a CA certificate for what is a registry mismatch on our side. SARIF
is how adopters feed findings into GitHub Code Scanning and at least one runs it as a
required merge gate, so this was a failing build on a scan that previously passed.Two tests permitted it and both were replaced rather than patched: one asserted the registry
contained OA001 through OA009 viaarrayContaining, which passes whether or not a newer
rule is registered, and now derives the expected set fromALL_DETECTORSand asserts
equality; the other hardcoded PD001's array position, breaking on insertion while still not
catching a missing rule, and now asserts the entry at the reported index is the finding's own
rule. -
The repository no longer ships a tracked
node_modulesentry. It had been committed as a
symlink pointing at an absolute path on one machine, so a fresh clone got a dangling link into
a stranger's home directory..gitignorealso went fromnode_modules/tonode_modules,
because the trailing slash matches a directory and not a symlink of the same name. The npm
tarball was never affected, sincefilesships onlydistand the assets. -
brace-expansionupgraded to clear its advisory in our own dev dependencies, via the Self Fix
workflow. -
The usage scan's 5000-file cap is no longer silent. It stops after 5000 source files, and said
nothing about it, so--only-usedcould filter out a genuine finding while the scan printed
Scan complete. No known vulnerabilities found.and exited 0. Whether it bit depended on walk
order rather than file count alone, so the all-clear appeared and disappeared as a repo was
reorganised.The cap is now disclosed in the terminal, in
--jsonand in both HTML reporters, scoped per
folder in multi-folder mode. A truncated scan records "not examined" rather than "not imported",
so the contextual usage signal reportsunknowninstead of claiming a package is unused, which
keeps it distinct from theusage: nullthat #1175 uses for "--usagenever ran".Measured on a 5,001 file project with the importing file past the cap: before, no findings,
exit 0, and a SARIF file with zero results; after, the highaxiosfinding, exit 1 under
--fail-on high, and 24 SARIF results. Adopters gating GitHub Code Scanning on that file were
being handed an empty report. Thanks to @prx-my. -
The seven flag-conflict messages in
src/cli/validate.tsbuild their flag names from the
constants insrc/cli/flags.tsrather than inline string literals, so a typo is a compile
error instead of a wrong error message. Closes the magic-strings half of #420 for that file,
which #1229 could not do because #1230 added the constants two hours after it was approved.Behaviour-neutral, measured rather than asserted: 26 invocations through this build and a build
with onlyvalidate.tsreverted, compared on stdout, stderr and exit code, all identical across
12 distinct error messages. The two compound forms stay two different strings, one with a slash
and one with the word "or". Thanks to
@blackmore-technology-group, whose first contribution this is, and to @prx-my who reviewed it. -
The HTML report's embedded
report.jsonand the--jsonpayload no longer have two
different shapes. Both now come fromsrc/output/scan-json.ts, so they cannot drift again.
report.jsonis the--jsonpayload plusscannedAt,cliVersionand per-finding
riskSummaryandnextAction, which the page shows in expanded rows.--jsonitself is unchanged, verified byte-identical to a main build on single and multi
folder across twelve scenarios, as are SARIF, CycloneDX and SPDX.--ratchetstill
short-circuits before the JSON write. Thanks to @alex-powell. -
OA010 is reachable from the documented scan path, and a rule that could not run now says so.
cve-lite . --check-overridesrejected--check-networkas an unknown flag, so the only way to
reach OA010 was theoverridessubcommand, and the main scan reported a clean bill of health on an
override block full of stale floors. It now accepts the flag and threads a cert-aware fetch through,
which matters becauseNODE_EXTRA_CA_CERTSis read only at process start.The second half is the one that prevents a repeat: skipped detectors were populated in four places,
honoured by ten rules and never surfaced anywhere, so a registry or advisory failure silently
disabled a rule and the output looked clean. The skips now reach the terminal,--json, SARIF
(asdriver.notifications, deduped by id so a notification's descriptor resolves per SARIF
§3.58.2) and the HTML report, and the terminal tick is qualified to "No override hygiene issues
found in the rules that ran." when something was skipped.Verified on CopilotKit: on main the scan path rejects the flag and mentions OA010 zero times; with
the change it returns the same 109 findings as the subcommand including 20 OA010, compared
finding-by-finding with zero differences. -
Workspace member enumeration now matches a globstar and in-segment globs.
packages/**and
prefix patterns likepackages/pkg-*matched nothing at all, so a monorepo declaring its members
either way had no members enumerated, and since member manifests are what declaration resolution
reads, every package declared only by a member looked undeclared and PD001 and PD002 reported it
as a phantom. #1114 fixed the root-versus-member case; this is the other half. Globstar expansion
never descends intonode_modulesor a dot-directory, which without the guard would enumerate
every installed package as a member and let their declarations suppress real findings. Negation is
still ignored and fails safe, pinned by a test. -
The usage scanner's comment stripper classifies characters by code rather than running a regex
per character.stripCommentsexecuted/\s/and/[\w$]/against every character of every
scanned file, which on a 2.2 MB corpus cost about 37ms per pass against 1.3ms for the import regex
it feeds, so the stripping dominated the phase rather than the matching it protects. Now 17ms, with
byte-identical output across 314 files and ten adversarial Unicode cases. Code points above ASCII
fall back to the original regexes, so the semantics are unchanged rather than approximated. Cost
falls on every scan using PD001, PD002, OA009 or--usage.Two corrections to #1120's own analysis, both measured. It blamed
out += chand recommended an
array plus join; V8's rope strings make+=faster, so that change costs 44 percent. And the
regression was 32x the regex-only cost rather than the 11x filed, so roughly 680ms at the
5,000-file cap. -
OA011 no longer claims it checked every declaring package when it has read one manifest. Without
node_modules,walkInstalledTreecannot run, soparentDeclarationsholds the root manifest's
own declarations and nothing else, and the finding's headline still said "exceeds every declaring
package's range" with a detail naming the list. The remedy it prints is derived from that same set,
so a transitive declarer permitting a higher major would have made the advice wrong. The wording is
now scoped on that path: the root manifest rather than every declaring package, plus a note that
transitive declarers were not visible and to re-run after an install. Detection is unchanged, with
a test asserting both paths agree on rule id, severity, package and location.The flag is
declarationsRootOnlyon the override context, set from thenodeModulesExistscheck
the builder already does, deliberately optional so the sixteen test files that build contexts by
hand keep working and the default stays the common case. -
A
--fail-ongate that trips now says what tripped it. The scan exited non-zero and said nothing
about the flag, the threshold, or which findings crossed it, so a red CI job gave no reason. The
gate line names the count per class, the threshold and the raw--fail-onvalue, so a typo like
--fail-on hgihshows up asat or above critical (--fail-on hgih)rather than silently gating on
critical.shouldFailis now derived from that same summary rather than a duplicated severity
comparison. Suppressed under--jsonand under--fix, where the exit code is deliberately forced
to zero. Thanks to @theluckystrike, whose first contribution this is.One behaviour change, and it is a fix. Multi-folder with an empty
--fail-onexited 1 and now
exits 0. The old multi-folder gate term was a bare severity comparison with none of the
empty-value guard its two neighbours had, so an empty value normalised tocriticaland gated the
build, while the single-folder path did not.action.ymldocuments that input as "Leave empty to
run the scan in informational mode (no exit code on findings)", so multi-folder was contradicting
our own contract. Verified identical to main across 19 real invocations otherwise. -
positionAgainstRangeno longer throws on a version aboveNumber.MAX_SAFE_INTEGER, and OA008's
range handling is consolidated onto one comparison path.semver.satisfiesvalidates and
constructs versions internally so it cannot throw, whilesemver.ltrandsemver.gtrdo not
guard the same way, and that asymmetry is invisible from the library's API. The boundary is exact:
9007199254740991.0.0classifies asabove, one higher now returns null instead of throwing.
Also removes an unreachable singular branch and reuses the shared position labels. Thanks to @idrivehtbusiness, whose first contribution this is.
Docs
-
The README badge row now shows total npm downloads alongside the monthly figure. The URL names
npm/d18mrather thannpm/dt, because shields.io retired lifetime totals anddt301-redirects
to an 18-month rolling window; naming it directly keeps the markdown honest about what it fetches.
For this package the window equals the lifetime total today, since the first download was
2026-03-27. -
The native dependency's install behaviour is now documented across the README, the getting
started page and troubleshooting.cve-lite-clideclares no install hooks of its own; the one
install script belongs tobetter-sqlite3, and the deprecation warning comes from
prebuild-installbeing deprecated in the registry, so it fires during resolution whichever
path the install takes. A quietnpxrun does not prove the script was skipped, since a warm
cache simply reuses the already-built binding. Every npm-behaviour claim is scoped to the npm
version, because 12.0.1 and 12.1.0 differ. Crucially the page is explicit that online scanning
works with the install script blocked: the binding loads lazily and only--offline,
--offline-db,advisories syncandadvisories initneed it.
Thanks to @Anshulrajkumar, whose first contribution this is, and to @ecki who reported it.
Validation
- npm test
- npm run build
- Accuracy sweep on CopilotKit, 4,936 packages: 111 vulnerable per OSV, 111 reported, zero missed, zero extra, zero advisory-id disagreements
Contributors
Thanks to everyone who contributed to this release:
@alex-powell, @anbv29, @Anshulrajkumar, @blackmore-technology-group, @idrivehtbusiness, @MRX-72, @osfv, @prx-my, @theluckystrike, @VolodymyrLinuxovich and @XonkelX.
First contributions from @anbv29, @Anshulrajkumar, @blackmore-technology-group, @idrivehtbusiness, @theluckystrike, @VolodymyrLinuxovich and @XonkelX.
Reporter credit to @alamb-hex, who reported both defects fixed in the phantom-dependency work, and to @ecki, who reported the native dependency install behaviour the documentation now covers.