Skip to content

npm CLI

The RingZero npm CLI (ringzero-sca, from the @ringzero/sca package) reads your project’s package-lock.json — the real resolved graph, direct and transitive, exactly as npm installed it — and uploads a dependency snapshot to RingZero for advisory matching. It covers the JavaScript/TypeScript row of the ecosystem table the way the Gradle plugin covers Gradle builds.

ComponentMinimum
Node.js20
Lockfilepackage-lock.json v2 or v3 (npm 7+)
CLI version0.1.0 (current)

Lockfile v1 (npm 6) is not supported — regenerate it with a current npm (npm install --lockfile-version 3). Yarn and pnpm lockfiles aren’t read yet; those projects can upload via the REST API directly.

No install step and no account needed for a first look — the CLI runs in report-only mode without an API key:

Terminal window
npx @ringzero/sca scan
RingZero: snapshot of 143 dependencies (12 direct, 131 transitive) from 4 workspaces
RingZero: no API key configured — snapshot written to .ringzero/sca/snapshot.json, upload skipped. Set RINGZERO_API_KEY to upload.

Inspect .ringzero/sca/snapshot.json — that’s exactly what an upload would send: package names, versions, and graph edges. No source code, no vulnerability data (advisory matching happens server-side).

To upload, provide a key and re-run:

Terminal window
RINGZERO_API_KEY=rz_key_5f2a… npx @ringzero/sca scan
RingZero: snapshot of 143 dependencies (12 direct, 131 transitive) from 4 workspaces
RingZero: uploaded — scan scn_7g8h queued
RingZero: https://app.ringzero.tech/scans/scn_7g8h

The scan id and app link are also written to .ringzero/sca/scan.json for tooling. Add .ringzero/sca/ to your .gitignore — the reports are build outputs, not config.

Uploads authenticate with a scoped API key (rz_key_…), issued once by an org admin in Settings → API keys. Create a dedicated key for this project with the minimum it needs — scopes sca:upload (add scans:read if your tooling polls results) and a project restriction covering just this project. See Authentication & permissions for the full scope and role model.

The key is read from the RINGZERO_API_KEY environment variable only — there is deliberately no --api-key flag, because command lines end up in process listings, shell history, and CI logs. The CLI never logs the key.

Local development — export the variable from your shell profile or a git-ignored .env file loaded by your shell tooling.

CI — inject it from your platform’s secret store (see Running in CI).

The snapshot names the RingZero project it belongs to, resolved in this order:

  1. The --project flag (highest precedence)
  2. The RINGZERO_PROJECT environment variable
  3. The root package.json name from the lockfile

The fallback is fine for a first run, but pin the slug explicitly so package renames don’t fork your scan history. A new slug creates the project on your first upload. If your API key carries a project restriction, a slug outside that list fails the upload with 422 unknown_project — restricted keys can never create projects.

There is no organization setting: the server derives your org from the API key itself.

ringzero-sca scan [path] parses package-lock.json at path (default: the current directory):

  • Lockfile v2 and v3 — the formats npm 7+ writes.
  • npm workspaces — each workspace becomes its own root module in the snapshot (with its relative package.json as the manifest), so findings point at the workspace that actually pulls the vulnerable package in. Workspace symlinks and nested node_modules version shadowing resolve the way Node itself resolves them.
  • Relationships — packages named in a workspace’s dependencies, devDependencies, optionalDependencies, or installed peer dependencies are direct; everything else is transitive. Dev-only trees are uploaded too (they run on your build machines), attributed to a devDependencies context per workspace.
  • Deterministic output — the same lockfile always produces byte-identical snapshot.json, so re-uploads are idempotent: two CI jobs building the same commit converge on one scan.

Suppressions upload from this CLI is Coming soon — snapshots currently carry an empty suppressions array; triaged findings can be suppressed in the app, and the suppressions file format is final.

With no RINGZERO_API_KEY set — or with --offline — the CLI still parses the lockfile and writes snapshot.json; it just skips the upload and succeeds, recording why in scan.json ({ "uploaded": false, "reason": "no-api-key" | "offline" }). This is the same zero-friction canon as the Gradle plugin’s report-only mode: inspect exactly what would be uploaded before creating a key, and keep air-gapped or deliberately offline runs green.

GitHub Actions example — key from the repository’s encrypted secrets, a fresh snapshot uploaded on every PR:

name: Build
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Upload dependency snapshot
env:
RINGZERO_API_KEY: ${{ secrets.RINGZERO_API_KEY }}
run: npx @ringzero/sca scan --project acme-web

The same pattern applies to any CI system: store the key in the platform’s secret store and expose it as RINGZERO_API_KEY. Concurrent jobs are safe — uploads are idempotent on the snapshot’s content hash. Transient failures (network, 5xx, 429) retry automatically with backoff, honoring the server’s Retry-After.

ringzero-sca scan [path] [options]
OptionDefaultWhat it does
--project <slug>RINGZERO_PROJECT, then the root package.json nameThe RingZero project (slug or prj_… id); created on first upload
--lockfile <file><path>/package-lock.jsonExplicit lockfile path
--ecosystem <name>javascriptjavascript or typescript — the snapshot’s ecosystem tag
--api-url <url>RINGZERO_API_URL, then https://api.ringzero.tech/v1API base URL
--output <dir><path>/.ringzero/scaWhere snapshot.json and scan.json are written
--retries <n>3Upload retries for transient failures
--offlineForce report-only mode; never uploads
--jsonMachine-readable result on stdout

Precedence is flag > environment variable (RINGZERO_API_KEY, RINGZERO_API_URL, RINGZERO_PROJECT — the same names the Gradle plugin reads). Exit codes: 0 success (uploaded or report-only) · 1 snapshot/upload failure · 2 usage error.

Every API response carries an X-Request-Id; the CLI prints it on failures — quote it to support.

SymptomCauseFix
401 invalid_tokenKey missing/revoked/expired, or the env var isn’t reaching the CLICheck RINGZERO_API_KEY in the environment that runs the scan; re-issue in Settings → API keys
403 insufficient_scopeKey lacks sca:uploadIssue a key with the sca:upload scope
422 unknown_projectThe configured slug/id isn’t covered by the key’s project restrictionPin --project to a slug/id in the key’s list, or issue a key without a restriction
lockfile v1 … not supportedThe project was locked with npm 6Regenerate: npm install --lockfile-version 3
Snapshot written but nothing uploadedNo API key resolved — report-only modeSet RINGZERO_API_KEY; the log line names the mode
413 snapshot_too_largeOver 10,000 nodes or 10 MB (also enforced client-side)Scan workspaces separately with [path] + --project