github OWASP/cve-lite-cli v1.38.0
v1.38.0 - Override audits that admit what they could not check

latest release: v1
2 hours ago

Upgrade note. cve-lite . --check-overrides could 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 missing node_modules or 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 --usage ran, usage. reachability reports the dependency path depth
    (path_depth_2 upward) rather than restating relationship, and falls back to
    direct_dependency where there is no depth to report. exposure is a labelled
    heuristic, flagging a finding whose path runs through a request-handling package, and
    it earns its keep: on examples/nest it 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. usage is omitted entirely on a default scan rather than printed as n/a
    on every row. Deliberately kept as a separate layer from the EPSS prioritySignal,
    because that measures global CVE exploitability while these are project-local. Thanks to @alex-powell.

  • The eleven remaining braceless single-line if statements in src/cli/args.ts and
    src/index.ts now 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.ts and
    five in index.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 ESLint curly is the route
    rather than hand-editing.

  • The seven near-identical --X cannot be used with --Y messages in src/cli/validate.ts are
    now one conflictMessage(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. positionAgainstRange also now compares against the raw range instead of
    validRange's normalized output, which had silently made includePrerelease ineffective 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 only details differing 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-server floor in CopilotKit's tree, which a human reviewer there
    found by hand. "OA011" joins the OverrideRuleId union. 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.ts are 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 --all findings table no longer carries an IDs column. It rendered one
    advisory id plus a +N suffix, 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/nest carry a GHSA with no CVE alias, so the column would have printed
    0 beside 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 Type cell
    forced a line break before · dev, which saved 6 characters of width and cost up
    to 47 percent of the table's height: examples/nest went from 131 rendered lines
    to 70 and examples/lint-staged from 30 to 16, since 61 of 68 and 14 of 14 of
    their findings are dev dependencies. transitive · dev now 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 reads Verify a safe version of <pkg> before upgrading. in both the terminal
    and the HTML report, rather than handing over Upgrade <pkg> to <version>+ for a version
    we could not vouch for. The distinction between "unconfirmed" and "unset" is now a named
    predicate, isUnconfirmedFix in src/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 --all where the
    table already lists everything, and neither notice appears when nothing was withheld.
    Verified against examples/juice-shop (53), examples/nest (68) and examples/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 an apps/* 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 TS import type ... from 'x' made a transitive-only package look imported but
    undeclared. On examples/analog PD002 drops from 10 findings to 5, and all five
    removed are declared in apps/docs-app/package.json.

    A regex-literal bug in the shared stripComments pass was fixed in the same PR and
    was the more serious half: in const 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 in apps/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
    undeclaredImportFiles from phantom-utils.

    Same lines, second smaller fix: OA009 carried a private getDeclaredPackages reading
    only dependencies and devDependencies, while the shared one reads four sections, so
    a package declared only as a peerDependency was invisible to the guard and suppressed
    the rule. That duplicate is deleted and rootDeclared is 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 --sarif no 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 via arrayContaining, which passes whether or not a newer
    rule is registered, and now derives the expected set from ALL_DETECTORS and 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_modules entry. 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. .gitignore also went from node_modules/ to node_modules,
    because the trailing slash matches a directory and not a symlink of the same name. The npm
    tarball was never affected, since files ships only dist and the assets.

  • brace-expansion upgraded 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-used could 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 --json and 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 reports unknown instead of claiming a package is unused, which
    keeps it distinct from the usage: null that #1175 uses for "--usage never 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 high axios finding, 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.ts build their flag names from the
    constants in src/cli/flags.ts rather 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 only validate.ts reverted, 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.json and the --json payload no longer have two
    different shapes. Both now come from src/output/scan-json.ts, so they cannot drift again.
    report.json is the --json payload plus scannedAt, cliVersion and per-finding
    riskSummary and nextAction, which the page shows in expanded rows.

    --json itself is unchanged, verified byte-identical to a main build on single and multi
    folder across twelve scenarios, as are SARIF, CycloneDX and SPDX. --ratchet still
    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-overrides rejected --check-network as an unknown flag, so the only way to
    reach OA010 was the overrides subcommand, 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 because NODE_EXTRA_CA_CERTS is 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
    (as driver.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 like packages/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 into node_modules or 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. stripComments executed /\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 += ch and 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, walkInstalledTree cannot run, so parentDeclarations holds 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 declarationsRootOnly on the override context, set from the nodeModulesExists check
    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-on gate 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-on value, so a typo like
    --fail-on hgih shows up as at or above critical (--fail-on hgih) rather than silently gating on
    critical. shouldFail is now derived from that same summary rather than a duplicated severity
    comparison. Suppressed under --json and 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-on exited 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 to critical and gated the
    build, while the single-folder path did not. action.yml documents 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.

  • positionAgainstRange no longer throws on a version above Number.MAX_SAFE_INTEGER, and OA008's
    range handling is consolidated onto one comparison path. semver.satisfies validates and
    constructs versions internally so it cannot throw, while semver.ltr and semver.gtr do not
    guard the same way, and that asymmetry is invisible from the library's API. The boundary is exact:
    9007199254740991.0.0 classifies as above, 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/d18m rather than npm/dt, because shields.io retired lifetime totals and dt 301-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-cli declares no install hooks of its own; the one
    install script belongs to better-sqlite3, and the deprecation warning comes from
    prebuild-install being deprecated in the registry, so it fires during resolution whichever
    path the install takes. A quiet npx run 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 sync and advisories init need 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.

Don't miss a new cve-lite-cli release

NewReleases is sending notifications on new releases.