Skip to content

depkeep/ossrisk

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

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.

CI npm License: MIT


Install

npm install -g ossrisk

Or 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.json

Risk 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 the event-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 as low

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.lockdefault group 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. rnm, 1l). 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.3

Both 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/lodash@4.17.11, pkg:pypi/django@3.2.0.

  • CycloneDX additionally folds ossrisk's findings back into the document: any CVEs are written to the standard vulnerabilities section (with OSV as the source and severity ratings), declared non-permissive licenses appear under licenses, and each component carries ossrisk:riskLevel / ossrisk:direct / ossrisk:via properties. So the SBOM doubles as a vulnerability report.
  • SPDX lists each package with its purl external reference and declared license (falling back to NOASSERTION when ossrisk didn't resolve one), plus a DESCRIBES relationship from the document to every package.

Tip: pair --format cyclonedx/spdx with --fail-on none when 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.rego

Requires 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

  • CVEsOSV.dev batch API
  • EOL datesendoflife.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)
  • Licenseslicense field from npm registry; info.classifiers and info.license from PyPI
  • Maintainer signalsmaintainers, versions[v]._npmUser, and time from the npm registry (npm only)
  • Install scriptsscripts field 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 source

Before submitting a PR, run npm run build and commit the updated dist/ so the GitHub Action stays functional.


License

MIT © DepKeep

About

Scan dependencies for EOL versions, CVEs, and abandonment signals — CLI + GitHub Action

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors