ossrisk
v0.9.0
Published
Scan dependencies for supply-chain risk: EOL versions, CVEs, abandonment, typosquatting, license compliance, maintainer takeover patterns, and OPA policy gating
Maintainers
Readme
ossrisk
Scan your dependencies for long-term viability and supply-chain risk: EOL versions, known CVEs, abandonment signals, typosquatting, license compliance, and maintainer takeover patterns.
Supports package.json / package-lock.json (npm) and requirements.txt / Pipfile.lock (PyPI). When a lockfile is present, the full resolved tree (direct + transitive) is scanned and each flagged transitive shows the direct dep it came in through.
Install
npm install -g ossriskOr run without installing:
npx ossrisk .CLI usage
ossrisk [path] [options]| Option | Default | Description |
|---|---|---|
| [path] | . | Path to project directory to scan |
| -f, --format <fmt> | table | Output format: table, json, markdown, cyclonedx, spdx |
| --fail-on <level> | high | Exit 1 if any dep reaches this risk level (none|low|medium|high|critical) |
| --policy <path> | | Evaluate results against OPA Rego policies — file or directory (requires the opa CLI) |
| -c, --concurrency <n> | 8 | Concurrent API requests per batch |
| --no-eol | | Skip EOL checks |
| --no-cve | | Skip CVE checks |
| --no-activity | | Skip abandonment/staleness checks |
| --no-outdated | | Skip latest-version checks |
| --no-typosquat | | Skip typosquatting checks |
| --no-license | | Skip license compliance checks |
| --no-maintainer | | Skip maintainer/publisher checks |
| --no-install-script | | Skip install-script (preinstall/postinstall) checks |
| --direct-only | | Scan only direct dependencies, skip transitives |
| --list-checkers | | List the available risk checkers and exit |
Examples
# Scan the current directory
ossrisk
# Scan a specific project
ossrisk /path/to/project
# Output as JSON
ossrisk . --format json
# Fail on medium risk or above
ossrisk . --fail-on medium
# Skip CVE checks, output markdown
ossrisk . --no-cve --format markdown
# Gate entirely on an OPA Rego policy
ossrisk . --fail-on none --policy ./policy/supply-chain.rego
# Export a CycloneDX SBOM (with the CVEs ossrisk found embedded)
ossrisk . --fail-on none --format cyclonedx > sbom.cdx.json
# Export an SPDX SBOM
ossrisk . --fail-on none --format spdx > sbom.spdx.jsonRisk levels
| Level | Triggers |
|---|---|
| critical | CVE with CVSS ≥ 9.0 |
| high | CVE with CVSS 7.0–8.9, EOL version, or suspected typosquat of a popular package |
| medium | CVE with CVSS 4.0–6.9, no release in 24+ months (abandoned), strong-copyleft license (GPL/AGPL/SSPL/…), or new-publisher pattern on a >180-day-old package |
| low | CVE with CVSS < 4.0, no release in 12–24 months (stale), newer version available, weak-copyleft license (LGPL/MPL/EPL/…), unknown license, sole maintainer, or install lifecycle hooks (preinstall/install/postinstall) |
| none | No issues found |
Maintainer / publisher signals
Two patterns surfaced from npm packument metadata:
- new-publisher (
medium) — the latest release is published by an account that did not publish any of the first three releases, and the package is older than 180 days. This is theevent-stream-style takeover pattern: long-running package, sudden new face on the most recent publish. False positives are possible (legitimate maintainer handoffs); treat as "review before pinning." - sole-maintainer (
low) — only one maintainer is registered on the package. Informational bus-factor signal, not a vulnerability.
Both checks are npm-only for now; PyPI's JSON API doesn't expose comparable per-version uploader history. Use --no-maintainer to skip these checks.
Install scripts
npm packages can declare lifecycle hooks (preinstall, install, postinstall) that run automatically on npm install. These are a known supply-chain attack vector — several high-profile compromises (e.g. event-stream, node-ipc) used postinstall scripts to exfiltrate data or deliver payloads.
ossrisk fetches the version-specific manifest from the npm registry and flags any package that declares one or more of these hooks as low risk. The signal is informational: many legitimate packages (native add-ons, build tools) use node-gyp rebuild as an install script. The intent is to surface the signal so you can consciously decide whether a package's install-time code execution is expected. Use --no-install-script to suppress this check.
Licenses
ossrisk reads each package's declared license from the npm or PyPI registry, normalizes common variants to SPDX identifiers, and categorizes them:
- permissive (MIT, Apache-2.0, BSD, ISC, …) — not flagged
- weak-copyleft (LGPL, MPL, EPL, CDDL, …) — flagged as
low - strong-copyleft (GPL, AGPL, SSPL, OSL, EUPL) — flagged as
medium - unknown (missing,
UNKNOWN, or unrecognizable text) — flagged aslow
The check exists to surface licenses that need legal review before commercial
use; it is not a judgement that any of these licenses are bad. Use
--no-license if your project doesn't need this signal.
Transitive dependencies
ossrisk scans the full resolved dependency tree when a lockfile is present:
- npm:
package-lock.json(lockfileVersion 2+) — every package listed is scanned; dev-only deps are excluded. - PyPI:
Pipfile.lock—defaultgroup is scanned; if the lockfile preserves_meta.pipfile.packages, entries are tagged as direct or transitive accordingly.
For flagged transitives, the report shows the direct dependency that pulls them in
(via express), so you know which top-level package to update or replace. Use
--direct-only to skip transitives entirely.
Without a lockfile (e.g. only package.json or only requirements.txt), ossrisk
falls back to direct-only scanning.
Typosquatting
ossrisk compares each dependency name against a curated list of popular npm and PyPI
packages, flagging anything within edit distance 2 or matching common homoglyph
substitutions (e.g. rn ↔ m, 1 ↔ l). A scoped package like @vendor/lodash
is compared by its basename. The check is purely local — no API calls.
SBOM export
ossrisk can emit a Software Bill of Materials over the same resolved dependency tree it scans, in either of the two industry-standard formats:
ossrisk . --fail-on none --format cyclonedx > sbom.cdx.json # CycloneDX 1.5
ossrisk . --fail-on none --format spdx > sbom.spdx.json # SPDX 2.3Both documents are written to stdout as JSON. Every dependency (direct and
transitive) becomes a component/package identified by a
package URL — e.g.
pkg:npm/[email protected], pkg:pypi/[email protected].
- CycloneDX additionally folds ossrisk's findings back into the document: any
CVEs are written to the standard
vulnerabilitiessection (with OSV as the source and severity ratings), declared non-permissive licenses appear underlicenses, and each component carriesossrisk:riskLevel/ossrisk:direct/ossrisk:viaproperties. So the SBOM doubles as a vulnerability report. - SPDX lists each package with its purl external reference and declared
license (falling back to
NOASSERTIONwhen ossrisk didn't resolve one), plus aDESCRIBESrelationship from the document to every package.
Tip: pair
--format cyclonedx/spdxwith--fail-on nonewhen you only want the artifact — otherwise a risky dependency still makes the process exit 1 (useful if you want CI to both produce an SBOM and gate on risk).
Checkers
Each risk dimension is an independent checker registered against a common
interface. ossrisk --list-checkers prints the active set:
cve Known CVEs / advisories via the OSV batch API
eol End-of-life runtime/framework versions
activity Abandonment / staleness from last-release date
outdated A newer version is available
license Copyleft / unknown license compliance
maintainer New-publisher / sole-maintainer takeover patterns
install-script preinstall / install / postinstall lifecycle hooks
typosquat Typosquat of a popular package (local, no API)Every checker has a matching --no-<name> flag to skip it. A checker either
runs once over all dependencies (a batched batch pre-pass, like cve) or
per-dependency (a check), returning risk signals that are merged into the
report. The registry lives in src/checkers/index.ts —
adding a dimension is a matter of implementing the Checker interface and
appending it there.
GitHub Actions
Add ossrisk to your CI pipeline to automatically scan dependencies on every pull request.
- name: Scan dependencies
uses: depkeep/ossrisk@v1
with:
fail-on: high
github-token: ${{ secrets.GITHUB_TOKEN }}When github-token is provided and the workflow runs on a pull request, ossrisk posts a markdown report as a PR comment.
Action inputs
| Input | Default | Description |
|---|---|---|
| path | . | Path to the project directory |
| fail-on | high | Exit 1 if any dep reaches this level or above |
| no-eol | false | Skip EOL checks |
| no-cve | false | Skip CVE checks |
| no-activity | false | Skip abandonment/staleness checks |
| no-outdated | false | Skip latest-version checks |
| no-typosquat | false | Skip typosquatting checks |
| no-license | false | Skip license compliance checks |
| no-maintainer | false | Skip maintainer/publisher checks |
| no-install-script | false | Skip install-script (preinstall/postinstall) checks |
| direct-only | false | Scan only direct dependencies, skip transitives |
| github-token | | GitHub token for posting a PR comment |
Action outputs
| Output | Description |
|---|---|
| risk-level | Highest risk level found across all dependencies |
Policy as code (OPA)
--fail-on is a single threshold. For richer rules — "no critical CVEs anywhere, no strong-copyleft in direct deps, block install scripts from brand-new publishers" — ossrisk integrates with Open Policy Agent:
ossrisk . --fail-on none --policy ./policy/supply-chain.regoRequires the opa CLI on your PATH. --policy accepts a single .rego file or a directory of policies.
Policies receive the scan result as input — the same JSON that ossrisk --format json prints, which is a stable contract. They live in package ossrisk and add messages to a deny set; any message is reported as a violation and makes ossrisk exit 1:
package ossrisk
import rego.v1
# Block strong copyleft, but only for direct dependencies.
deny contains msg if {
some dep in input.results
dep.isDirect
some sig in dep.signals
sig.type == "license"
sig.category == "strong-copyleft"
msg := sprintf("%s@%s is %s (strong copyleft)", [dep.name, dep.version, sig.license])
}See examples/policies/supply-chain.rego for a fuller policy, including a cross-signal rule for the event-stream takeover pattern (install scripts + new publisher).
--fail-on and --policy are independent — either can fail the scan. Use --fail-on none to delegate gating entirely to your policy. Violations print to stderr, so --format json output stays pipeable.
Programmatic API
import { scan } from 'ossrisk';
const result = await scan({
path: '/path/to/project',
format: 'json',
failOn: 'high',
concurrency: 8,
noEol: false,
noCve: false,
noActivity: false,
noOutdated: false,
noTyposquat: false,
noLicense: false,
noMaintainer: false,
noInstallScript: false,
directOnly: false,
});
console.log(result.summary);
// { total: 42, critical: 0, high: 1, medium: 3, low: 5, clean: 33 }Data sources
- CVEs — OSV.dev batch API
- EOL dates — endoflife.date API
- Activity — npm registry / PyPI JSON API
- Latest versions — npm registry / PyPI JSON API
- Typosquatting — local curated list of popular npm & PyPI packages (no API calls)
- Licenses —
licensefield from npm registry;info.classifiersandinfo.licensefrom PyPI - Maintainer signals —
maintainers,versions[v]._npmUser, andtimefrom the npm registry (npm only) - Install scripts —
scriptsfield from the version-specific npm registry manifest (npm only)
All checks are read-only and require no API keys.
Contributing
git clone https://github.com/depkeep/ossrisk.git
cd ossrisk
npm install
npm test # run tests
npm run dev . # run CLI from sourceBefore submitting a PR, run npm run build and commit the updated dist/ so the GitHub Action stays functional.
License
MIT © DepKeep
