niyantri
v0.1.0
Published
Dependency vulnerability scanner that ranks by real exploitability — reachability analysis, CISA KEV, EPSS, and CI gating for npm and PyPI projects
Maintainers
Readme
Niyantri
Multi-source dependency vulnerability scanner for npm and PyPI projects.
Merges findings from Trivy, npm audit / pip-audit, OSV, RetireJS and the GitHub Advisory Database onto one dependency graph, de-duplicates them, then ranks by what is actually exploitable in your project — not by raw CVSS.
What makes the ranking different
Every finding gets a 0–10 risk score:
risk = CVSS band + reachability + exposure + input surface + EPSS
(floored to 9.5 if the CVE is in CISA's KEV catalog)- Reachability — is the package actually imported in your source? A CVE in a package nothing imports scores 0.2 instead of 2.5.
- Dev-only — packages reachable only from
devDependenciesare not part of your deployed attack surface, even if a test file imports them. - KEV / EPSS — CISA's actively-exploited catalog and FIRST.org's 30-day exploitation probability, both cached per day.
- Confidence — every finding reports whether its score rests on evidence
(CVSS/EPSS/KEV) or on a regex over the advisory title. A
7.3built from hard data and a7.3built from a keyword guess are not the same number.
Requirements
Node >= 16, plus two external binaries on PATH:
Optional, used when present: osv-scanner, retire, pip-audit.
Usage
niyantri test [project-path] [options]
# or
node scan.js [project-path] [options]Output options
| Flag | Effect |
|---|---|
| --json-out <path> | Structured JSON for API consumers |
| --sarif-out <path> | SARIF 2.1.0 for GitHub Code Scanning |
| --show-all | Include not-imported and dev-only findings |
| --risk-sort | Order all findings by risk score |
| --severity=<level> | Only show findings at or above this severity |
CI gating
| Flag | Effect |
|---|---|
| --fail-on=<spec> | Exit 1 when findings match the spec |
| --baseline [path] | Compare against a baseline; marks findings as new |
| --write-baseline [path] | Record current findings as the accepted set |
| --no-ignore-file | Skip .niyantriignore.json |
| --strict-sources | Exit 2 if any data source failed (scan incomplete) |
--fail-on spec — comma-separated, OR'd together:
| Token | Meaning |
|---|---|
| critical high medium low | Severity at or above |
| risk:N | Risk score >= N |
| kev | Any CISA-KEV finding |
| any | Any finding at all |
| new | Modifier — restrict to findings absent from the baseline |
niyantri test . --fail-on=high # fail on any high or critical
niyantri test . --fail-on=risk:7.5 # fail on genuinely exploitable issues
niyantri test . --fail-on=kev # fail only on active exploitation
niyantri test . --baseline --fail-on=new # fail only on newly introduced issues--fail-on=risk:N is usually the better gate. It already accounts for
reachability, so an unreachable CRITICAL will pass while a reachable MEDIUM in a
request-handling path will fail.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Clean, or nothing matched --fail-on |
| 1 | Findings matched --fail-on |
| 2 | Scan could not run (missing tool, bad path, bad flag) |
Without --fail-on the scan always exits 0.
Data source health
A scanner that loses a source and says nothing is worse than one with fewer sources, because a degraded scan looks identical to a clean project. Every run reports what each source actually did:
Data sources 4 of 7 sources active — 1 FAILED
✓ Trivy 6 findings
✓ npm audit 4 findings
– pip-audit not a Python project
✓ RetireJS 0 findings
– OSV Scanner osv-scanner not on PATH
✗ GitHub Advisory HTTP 401 Bad credentials — GITHUB_TOKEN expired
✓ OSV API 14 findings
⚠ A failed source means findings may be MISSING.The distinction is the point:
| Mark | Meaning |
|---|---|
| ✓ | Ran and produced usable output (0 findings is legitimate) |
| – | Skipped — prerequisite genuinely absent. Expected. |
| ✗ | Failed — configured but errored. Findings may be missing. |
Use --strict-sources in CI to exit 2 when any source failed, so an
incomplete scan can never be mistaken for a passing one. The same data is in
--json-out under source_health; consumers should check degraded before
treating a low finding count as clean.
Adopting on an existing codebase
Nobody triages an inherited backlog. Record it once, then gate only on what's new:
niyantri test . --write-baseline # writes .niyantri-baseline.json — commit it
niyantri test . --baseline --fail-on=newFindings that disappear are reported as resolved. On a first run with no
baseline, nothing is marked new — so a --fail-on=new build won't fail on the
backlog it just inherited.
Suppressing accepted findings
.niyantriignore.json in the project root:
{
"ignore": [
{
"id": "CVE-2026-4800",
"package": "lodash",
"reason": "only reached by the build script, not shipped",
"expires": "2026-12-31"
}
]
}id is required; package, reason and expires are optional.
Expiries matter. When a rule lapses, it stops applying and the finding comes back — which forces someone to re-make the decision instead of the suppression quietly becoming permanent blindness. Rules expiring within 14 days are flagged in the summary.
Forcing a fix for transitive dependencies
When a vulnerable transitive has no parent release that upgrades it, the scan prints a paste-ready block instead of a dead end:
FORCE A FIX · 2 packages with no parent upgrade path
{
"overrides": {
"lodash": "^4.18.0",
"qs": "^6.15.2"
}
}Add it to package.json (yarn users: resolutions, with a **/ prefix). When
several advisories affect one package the highest fixed version is chosen, so a
single pin closes all of them. Verify nothing breaks — an override diverges from
what the parent package declared.
GitHub Actions
name: Dependency scan
on: [pull_request]
permissions:
contents: read
security-events: write # required to upload SARIF
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- name: Install scanner prerequisites
run: |
npm install -g @cyclonedx/cdxgen
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /usr/local/bin
- run: npm ci
- name: Scan
run: node scan.js . --sarif-out results.sarif --baseline --fail-on=new
- name: Upload to code scanning
if: always() # upload findings even when the gate fails
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarifFindings then appear inline on the PR diff and in the Security tab. GitHub ranks them by the risk score, so the reachability discount carries through.
Development
npm test # unit tests (node:test, no dependencies)
npm run test:watchlib/ holds the extracted, independently testable pieces of the pipeline —
scoring, reachability, intel feeds, policy, baseline, SARIF. Each is pure or
takes its I/O by injection; see the header comment in each file.
Server mode
npm run serveAccepts a zipped project, runs a scan, streams output back and persists results
to MongoDB for the dashboard. Requires MONGODB_URI, REDIS_HOST, REDIS_PORT,
REDIS_PASSWORD and HMAC_SECRET in the environment.
Note: the server runs scanner tooling over uploaded archives. Run it in a sandboxed container, not alongside anything else you care about.
