opiv0.14.0

Security

A secret scan, a parsed dependency audit per ecosystem, and three supply-chain questions for npm projects.

S in the list, or:

opi --security

Two questions, answered by focused security tools:

  • Secrets — a detected project scanner (nosecrets or secretlint), otherwise the bundled nosecrets engine. This also covers Rust-only projects without an installed scanner. The default reads .nosecrets.toml and .nosecretsignore at the repository root, scans the working-tree files Git does not ignore — tracked and untracked, but not a local .env, build output or a nested worktree — and reports locations and fingerprints without secret values. A high-entropy string is a guess rather than a known credential format: it is listed as a warning and does not fail the check. Invalid configuration or scan errors fail the check; this is not a history scan.
  • Dependencies — the package manager’s audit, parsed from its JSON: npm audit and pnpm audit share npm’s format, bun and yarn each have their own.

Example for a Rust project without package.json:

opi  security

✓ Secrets — nosecrets

✓ Dependencies (rust) — no known vulnerabilities

Nothing to act on.

Supply chain

A package.json project gets up to three more lines, each answered by the package manager itself — pnpm all three, npm and bun two (licenses from the lockfile and npm sbom), yarn only the signatures:

  • Signatures — npm audit signatures: whether every installed package carries a valid registry signature. It reads node_modules, so it works under pnpm and bun too; under Yarn Plug’n’Play there is nothing for it to read. A signature that does not verify is the only one of the three that fails the run.
  • Release age — whether freshly published versions are held back: minimumReleaseAge for pnpm, min-release-age for npm. Asked with config get, so a global setting counts as well as the workspace file. yarn and bun are not asked.
  • Licenses — the most common licenses with their counts, and by name every package under a copyleft, source-available, missing or UNLICENSED license. Production dependencies only: from pnpm licenses list --prod, from the license fields in package-lock.json for npm, from npm sbom --omit dev for bun, from cargo metadata for Cargo. A project without dependencies reads Licenses — no dependencies.
✓ Signatures — every installed package verified
! Release age — not set: a version installs the moment it is published
  minimumReleaseAge: <minutes> in pnpm-workspace.yaml
! Licenses — 480 MIT · 22 ISC · 11 Apache-2.0 · 9 BSD-2-Clause · 20 other
  @img/sharp-libvips-darwin-arm64  LGPL-3.0-or-later
  lightningcss  MPL-2.0

Four more lines appear where there is something to say:

  • Install scripts — dependencies that declare preinstall, install or postinstall, and whether the project allows them (onlyBuiltDependencies / ignoredBuiltDependencies for pnpm, trustedDependencies for bun, ignore-scripts for npm). Scripts nothing has decided on are named.
  • Sources — dependencies resolved from git, a tarball URL or a path outside the workspace, read from package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock and Cargo.lock.
  • GitHub Actions — uses: references in .github/workflows that name a tag or branch instead of a 40-character commit SHA, with file and line.
  • Dependencies (osv) — osv-scanner over all lockfiles, shown beside the native audits and only for what they did not report. Without osv-scanner the section says so.

An unset release age and a copyleft dependency are reported, not judged: both can be a deliberate choice, so neither changes the exit code.

SBOM

opi --sbom [dir] writes a CycloneDX SBOM per ecosystem: sbom-npm.cdx.json from npm sbom (or pnpm sbom), sbom-cargo.cdx.json from cargo cyclonedx. Without dir they go to .opi/sbom/, a folder with a .gitignore of its own. An npm project with a package-lock.json is read from the lockfile alone, so node_modules is not needed. The documents are not merged, so each stays attributable to its producer. A missing producer is named with the command that installs it, e.g. cargo install cargo-cyclonedx.

Findings

Advisories are condensed to one line per package and severity, which is what decides what to do about them — the severity counts are therefore counts of vulnerable dependencies, not of advisories. Where the advisory names a patched range, it becomes the remedy; for npm that is fixAvailable, the version npm audit fix would install, or the dependent to update where the fix comes through one. bun and yarn name only the vulnerable range, so their findings carry no remedy: deriving “update to 4.17.21” from <4.17.21 would be inventing the one number that has to be right.

Three shapes behind one question

Every manager is asked for its audit as JSON, and three of them answer in a shape of their own. Which one to read is decided once, by which manager was asked, never by guessing from the document:

  • npm and pnpm return an object carrying one advisory per package.
  • bun returns a map from package name to a list of advisories — which npm’s shape cannot express. Measured against bun 1.3.3: clean JSON on stdout, its banner on stderr.
  • yarn returns newline-separated JSON, one object per advisory, with capitalised keys. Measured against yarn 4.18.0 with yarn npm audit --all --recursive --json.

yarn is also the one manager that prints nothing at all when it finds nothing, rather than an empty report. An empty result is therefore read as clean only when stderr is empty too — the same distinction --updates makes for npm and pnpm.

Rust

A project with a Cargo.toml gets its own section, Dependencies (rust), answered by cargo audit. It is an external cargo subcommand; where it is missing, opi names the cargo install that adds it.

The Rust advisory carries no severity in its JSON, and opi invents none. The key is simply absent; only a CVSS vector string is there. cargo audit’s own console output does print Severity: 7.5 (high), because it scores that vector itself — so every Rust advisory counts here, and the next step points at cargo audit. Scoring the vector inside opi would mean reimplementing CVSS, and reading the number out of console text would mean a parser on an undocumented format.

opi --security exits non-zero when something needs acting on — including an audit that could not run, such as one stopped by a network error. That audit has said nothing about the dependencies, so it is not reported as clean. A missing cargo audit is named with its cargo install instead and does not fail the run.

Edit this page on GitHub · Docs for v0.14.0