Skip to content

stasis audit: use @preventive/upstream for advisories and semver; add --repo-advisories - #181

Merged
exo-nikita merged 3 commits into
mainfrom
claude/brave-mayer-5rk21b
Sep 28, 2026
Merged

exo-nikita merged 3 commits into
mainfrom
claude/brave-mayer-5rk21b

Conversation

@exo-nikita

@exo-nikita exo-nikita commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

stasis audit now gets npm advisories from @preventive/upstream instead of stasis's own client, and all of stasis's semver use (audit and sbom) goes through upstream as well. The audit also gains a --repo-advisories flag that checks each dependency's GitHub repository, a per-user cache for those repository lookups, and a one-line summary of what it found.

Changes

  • Dependency: @preventive/upstream is pinned exactly at 1.0.0-alpha.0 in @exodus/stasis, because pre-releases may change the API. The lockfile integrity matches the published tarball.
  • stasis/src/audit.js: Now uses upstream's advisories(), one { ecosystem: 'npm', name, versions } per package. flattenAdvisories() takes upstream's rows directly. Upstream already works out which audited versions each advisory covers and drops advisories that cover none, so the local semver matching is gone. Transport and HTTP failures are still wrapped as npm advisories request failed: …, and the cause is kept. Upstream's own assertions (refused input, malformed answers) pass through unchanged.
  • Semver: audit.js, audit-corrections.js and sbom.js now take valid, satisfies and compareVersions from @preventive/upstream/semver.js, which borrows the same npm semver that sits beside node.
  • Removed stasis/src/apis/, stasis's own npm client and semver.cjs, since nothing uses it any more.
  • Table: the url column is replaced by id, the GHSA or npm:<id>. npm's URL for these rows was just https://github.com/advisories/<GHSA>.
  • Summary line: stderr now reads Scanned 86 packages: 51 alerts, 22 high, 22 moderate, 7 low. It lists each severity present, most severe first, and counts a row with no severity as unrated so the numbers add up. With no hits it reads Scanned N packages: 0 alerts, replacing No advisories found.
  • --repo-advisories: Also asks each package's GitHub repository, found through npm's metadata, for the advisories its maintainers published there. npm's database only has those once GitHub has reviewed them. The CLI requires a token in GITHUB_TOKEN, and passes upstream's GitHub client and the option through to advisories() unchanged. Usage text and README are updated. No new tests cover it, since the behavior belongs to upstream.
  • Cache: the CLI points upstream's setCacheDir() at a per-user directory, so each package's npm-to-GitHub repository lookup is cached for a month instead of repeated on every run. The directory is $XDG_CACHE_HOME/stasis when that path is absolute, ~/Library/Caches/stasis on macOS, %LOCALAPPDATA%\stasis\Cache on Windows, and ~/.cache/stasis otherwise. Upstream answers from the cache without making a request, so it has to be a directory only that user writes. It's set in the CLI, not in audit(), because the setting is process-global.
  • Packaging fix: src/audit-corrections.js and src/why.js were missing from stasis/package.json's files, although audit.js imports both. That means a packed @exodus/stasis couldn't run stasis audit; npm pack --dry-run now includes them.

Behavior notes

Upstream checks every name and version before sending any request. It sends at most 250 names per request, limits response size and time, and refuses answers that are malformed or about packages that weren't asked for. It matches versions the way npm audit does, including prereleases and loose semver, and a range semver can't parse covers every version.

Testing

  • tests/audit.test.js is updated for the new row shape and the id column. Version-matching coverage moved from flattenAdvisories into an audit() test against a stubbed registry. New tests cover the summary line: severity breakdown, unrated, 0 alerts, and the singular forms.
  • Full suite with CI=true GITHUB_ACTIONS=true on Node 24.14.0, 26.0.0 and 26.10.0: 2030 tests, 2027 pass, 3 skipped. node --run lint is clean.
  • Live check against the real registry, on a lockfile from tests/fixtures/popular-npm-modules (86 packages): this branch and main report the same 51 advisories in the same order, with identical severity, package, installed, vulnerable and title columns. Each new id equals the GHSA at the end of main's URL. --why output is also unchanged.
  • stasis sbom SPDX and CycloneDX output for that lockfile plus its bundle is byte-identical to main's, apart from timestamps and ids.
  • --repo-advisories smoke test: without GITHUB_TOKEN it's a usage error. With a bad token it reaches GET https://api.github.com/repos/lodash/lodash/security-advisories and fails with GitHub's 401.
  • Cache check, against the live registry with a temporary XDG_CACHE_HOME: the first run over 3 packages made 3 npm lookups and wrote npm/repos/{lodash,axios,@babel+core}.json; the second run made none.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LfLZCFXdXdrNeAmHZxEegG

…sories

stasis audit now asks npm's bulk advisories endpoint through
@preventive/upstream (pinned to 1.0.0-alpha.0: pre-releases may change
the API) instead of its own client, which is removed
(src/apis/npm/index.js). Upstream checks every name and version before a
request, sends at most 250 names a request, bounds the response size,
refuses malformed or unasked-for answers, and matches the covered
versions the way `npm audit` does (prereleases and loose semver, and an
unreadable range covers every version). audit.js and audit-corrections.js
take semver from upstream too; sbom.js still uses src/apis/npm/semver.cjs.

The table's `url` column becomes `id`: the GHSA (or npm:<id>), which is
what npm's URL pointed to. Against a live lockfile with 86 packages, the
50 rows match main's exactly otherwise.

--repo-advisories also asks each package's GitHub repository, found
through npm's metadata, for the advisories its maintainers published
there before GitHub reviews them into npm's database. The CLI requires a
token in GITHUB_TOKEN and passes upstream's GitHub client and the option
through unchanged.

Also ship src/audit-corrections.js and src/why.js: audit.js imports
both, but they were missing from `files`, so a packed @exodus/stasis
could not run `stasis audit`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LfLZCFXdXdrNeAmHZxEegG
…tream

stasis audit's stderr line now counts what it found:
`Scanned 86 packages: 51 alerts, 22 high, 22 moderate, 7 low`, each
severity present in the table's order (most severe first), a row
without one counted as `unrated` so the parts add up. With no hits it
reads `...: 0 alerts` (replacing "No advisories found").

sbom.js compares versions with @preventive/upstream's semver too, so
src/apis/npm/semver.cjs, the last of src/apis/, is removed. SPDX and
CycloneDX output for a live lockfile + bundle are byte-identical to
main's apart from timestamps and ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LfLZCFXdXdrNeAmHZxEegG
@exo-nikita exo-nikita changed the title stasis audit: use @preventive/upstream for advisories; add --repo-advisories stasis audit: use @preventive/upstream for advisories and semver; add --repo-advisories Sep 28, 2026
--repo-advisories resolves each package's GitHub repository from npm's
`latest` document, and without a cache upstream asks for every package
on every run. stasis audit now points @preventive/upstream's cache at a
per-user directory: $XDG_CACHE_HOME/stasis (when absolute),
~/Library/Caches/stasis on macOS, %LOCALAPPDATA%\stasis\Cache on
Windows, else ~/.cache/stasis. Upstream keeps a resolved repo there for
a month and answers from it without a request, so it must be a
directory only this user writes. A second run over three packages made
no npm lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LfLZCFXdXdrNeAmHZxEegG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants