A local-first dashboard that compares npm dependency versions across many projects at once and surfaces drift, outdated packages, CVEs, install-time risks, lockfile integrity drift, retroactive CVE exposure timelines, PR-review-grade dep diffs, binary file presence, deprecated versions, code obfuscation, third-party reputation (socket.dev / OpenSSF Scorecard / deps.dev), manifest red-flags, capability inventories and mutable lockfile resolutions in a single view. Backend lives inside a Vite dev server, frontend is plain TypeScript + DOM (no framework).
git + npm — the best combination. nppm draws its strength from both data sources at once: the npm registry carries versions, publishers, integrity, provenance and CVE links; git carries the time axis — which lockfile state belonged to which commit. Interweaving the two is what makes the retroactive vulnerability timeline, the PR-review dep deltas, and the git-backfilled history work at all. nppm shines on projects that are both versioned in git and reproducibly installed via
package-lock.json.
- 🇬🇧 User manual — screenshot-driven walkthrough
- 🇩🇪 Benutzerhandbuch — deutsche Version
- 🛠
CLAUDE.md— architecture reference (module map, API routes, conventions) - 📜
LICENSE— MIT
- Cross-project matrix — packages as rows, projects as columns, traffic
lights for
aligned / outdated / drift / unknown. Workspaces collapse to one column per project (with aWSbadge when the project's own workspaces disagree — clicking the badge opens a per-workspace breakdown with a jump to the per-project matrix). Git-only rows are recognised: no fakelatestfrom a same-named registry collision — the Latest column shows the upstream HEAD as1.0.28 · 7d3f12a(version + short SHA) for GitHub and Gitea hosts, falling back to a plaingitpill with an orange ⓘ when the host is unreachable. - Per-project matrix — workspaces split into individual columns so you see drift inside one project at a glance.
- Project sub-views for each configured project:
Declared(package.json),Installed(resolved from lockfile ornode_modules),History,Matrix,Tree,Unused,Vulns,PR. - Dependency tree — D3-based collapsible tree of every resolved package with status colouring (green / yellow / red / grey).
- Security scan
- OSV.dev CVE lookups (single + batched)
- Install-script heuristic (
curl | bash,node -e,eval, …) - Code-pattern heuristic (
eval(,new Function(,child_process, base64) - Binary-file detection (
.exe / .dll / .so / .node / .wasm+ bare binaries underbin/) - File-churn detection (suspiciously large patch bumps)
- Maintainer-handover detection — compares each version's
_npmUseragainst the publishers of the recent predecessors. Short gap + new publisher on a mature package raisesrisk(event-stream / ua-parser-js profile); long-silence handovers stayinfo(usually a legitimate community takeover). Risk + churn together surface a "possible supply-chain attack" banner. - License classification — five-bucket SPDX classifier (permissive /
weak-copyleft / strong-copyleft / proprietary / unknown) with a
mini SPDX-expression parser (
OR/AND/WITH/ parens). Own tab in the detail panel +GPL/UNLIC/LIC?matrix badges + a "Licenses" matrix filter. Compliance teams can plug in allow- / denylists viasecurity.licenseinnppm.json. - Lockfile-Integrity Cross-Check — compares the lockfile's pinned
resolved + integrityper entry against what the npm registry currently serves. Detects mirror-hijack / dependency-confusion / lockfile-injection asrisk, custom-mirror redirects asinfo. Surfaces in the Installed view as a new column + summary pill; network-free against the warm registry cache. - Provenance / sigstore — green
PROV ✓badge on packages whose latest is published with npm--provenance(Sigstore-anchored SLSA attestation tying the tarball to a specific CI build). - Freshness — flags packages or maintainer accounts that are
brand-new (
NEW!< 7 days,NEW< 30 days). Combined package + publisher age is the classic typosquat profile. - Release cadence — flags very stale (
STALE!≥ 730 days) or abandoned-looking packages. - Typosquat + Unicode homoglyph — Levenshtein distance to popular
packages plus confusables detection.
SQUAT!= distance 1 OR Unicode confusable;SQUAT?= distance 2. - Ignore-scripts recommendation — per-package verdict on whether
npm install --ignore-scriptsis safe / needed / risky. - Deprecation — reads per-version
deprecatedfrom the packument (free, no extra HTTP call).DEP!when the installed version is deprecated,DEP?when onlylatestis. Shows the maintainer's reason verbatim. - Code-obfuscation detection — per-JS-file heuristic over the
fingerprint cache: obfuscator.io
_0xidentifier density,eval(atob(…))chains, hex-string arrays, pathologically long lines. Path classifier capsdist//*.min.jsartifacts at info so legitimate minification doesn't get flagged. - Manifest red-flags — pure heuristics over
package.json: missing README, nodescription, nofiles[]allowlist, manybinentries, the native-build+postinstall combo, datedengines.noderange. Severity stacks: 1 flag = info, 2 = warn, 3 or the malicious combo = risk. - Capability inventory — per-package, which platform APIs the JS
files touch (
fs.read/write,http/fetch, raw sockets,child_process, credential-shaped env vars, native bindings,eval). Severity by combination: spawn+network or env+network land as risk, single capability as info. - External-sources aggregator — socket.dev (supply-chain risk score, needs API key), OpenSSF Scorecard (repo development practices, free), deps.dev (Google package index, free). Worst-of-three severity per package, configurable per source.
- Mutable-resolution scanner — per-project lockfile sweep for
entries that can't be reproduced deterministically: mutable git
refs (branch/tag instead of SHA), missing integrity hashes on
registry tarballs,
file:/link:local protocols.
- Cross-project Dashboard — every scanner's per-project verdict
collapsed into a
(project × scanner)ring matrix with 0–100 % scores under the Scanner Score tab. SSE-streamed per cell so a cold scan shows progress immediately (per-package sub-phase shown in the status line: "Fingerprinting lodash@4.17.21 (32/84) — kavula"); a snapshot under.nppm/cache/makes the next open instant. Lockfile-less projects (browser extensions, many libraries) are scanned against the registry'slatestfor every declared dep — same cells light up, with a small ⓘ on the project column carrying the "no lockfile — scanned against registry latest" note. Click any cell for the per-package drill-down with top-50 contributors; click any header to jump to the per-project view. The Scanner-Score tab leads with a header strip carrying a 3-segment macro-donut (healthy ≥ 80 / warning 60-79 / risky < 60) with the ecosystem average + a↑X pts vs last scandelta, alongside a Top-10 Worst Packages list aggregated by severity-weighted finding contribution. The Overall Evaluation tab renders an ecosystem hero card: a forest-themed 3:2 scene with ten clickable metric boxes, each with a hover tooltip and a detail modal. The Trend tab plots a rolling daily history of the ecosystem with four metric chips (Score / Packages / Size / Downloads) and three range chips (30 d / 90 d / 365 d) — one line per project plus a heavier ecosystem-overall line on top. Switching tabs leaves the SSE alone. Pickable via Settings → General → "Start page" as the default landing view. - Per-package Trends — the Package detail panel grows a seventh
Trendstab with five hand-rolled SVG sub-charts surfacing the full per-version history from the registry packument plus the npm public downloads API: unpacked size over versions, maintainer count over versions, direct dependencies over versions, releases per month (back-filled 24-month bar chart), and daily downloads over the last year. Each chart has an infoiicon with a viewport-safe tooltip explaining what to interpret — the classic "maintainer drop from 5 to 1 right before a takeover" and "stable utility quietly grew a framework" patterns are now visible at a glance. - Cross-project Impact Analysis — topbar Impact button
answers "which projects pull in
<name>(or<name>@<version>), directly or transitively, and via which shortest path?". BFS over every project's resolved dep graph. Hidden projects are included — incident response cares about every repo. - Badge filter — the global matrix grows a Badges toolbar
button that opens a checkbox-per-badge modal. Each row shows the
actual styled sample plus a one-line description so you can
decide what each badge means before hiding it. Default: all
visible. Selection persists in
localStorage. - Retroactive Vulnerability Timeline — answers "from when to when
was this project exposed to which CVE?". Forward-replays the
per-project history (live snapshots + git-reconstructed entries)
against the OSV cache to produce
[t_in, t_out)exposure windows per (CVE,name@version). Classifies each window asknown-at-install(red — installed an already-disclosed vuln),disclosed-during-use(yellow — vuln filed while you were running it), orpre-tracking(grey — bound to the first known timestamp). Compliance-ready data for ISO 27001 / SOC2 / DORA reports. - Git-backfilled history — the History view has a
Backfill from gitbutton (disabled when no.git/is detected) that walksgit log -- package-lock.json(orpackage.jsonwhen no lockfile was ever committed) and reconstructs the dependency history retroactively. Same path works for GitHub / Gitea via their commits API. Declared-only fallback entries get adeclared-onlypill so the user knows they don't carry CVE coverage. Re-running is idempotent by HEAD SHA. - PR-Review-Mode — diffs
package.json+package-lock.jsonbetween two git refs (defaultmainvsHEAD) and renders one card per changed dep with added / closed CVE pills. Local projects only in v1. Custom base / head via input fields in the view header. - One-click upgrade — outdated cells in the per-project matrix
show a small
↑button. The Upgrade modal previews the plannedpackage.jsonedit, surfaces a security heads-up on the target version (CVEs, install scripts, maintainer switch, churn), and offers two paths: "Edit only" (always available, just writes the file + suggests a manualnpm install) or "Edit + install--ignore-scripts" (gated byactions.allowInstall=trueinnppm.json). After install, the modal lists every install lifecycle hook innode_moduleswith a per-package "Run" button (npm rebuild <pkg>) so the user can re-fire only the scripts they've reviewed. Backups land in.nppm/backups/<timestamp>/. Every project-rooted write is routed throughSafePath.join, which refuses anything that isn't a strict descendant of the project root —../../etc/cron.d/evilworkspace paths and malicious template file entries are both contained at the endpoint boundary. - Open in IDE — small
IDEbutton next to every installed package's path in the Installed view. Opensnode_modules/<pkg>in the editor configured underactions.editor(vscode/vscodium/cursor/phpstorm/webstorm/idea/subl) via its://file/…URL handler. Off by default — hidden when no editor is configured. - Bulk-Update Wizard — the cross-project matrix grows a checkbox
on every outdated cell of a local project. A sticky footer shows
the running selection count; "Update selected" opens a wizard with
a per-project grouped preview (each pick's
from → toplus a security heads-up), then applies all edits — one shared backup per project — and optionally runsnpm install --ignore-scriptssequentially per project. Sameactions.allowInstallgate as the single-package modal. - SBOM export —
nppm sbom --format=cyclonedx|spdx(or theGET /api/projects/:id/sbom?format=…endpoint) emits a Software Bill of Materials for one project. CycloneDX 1.6 and SPDX 2.3 JSON. Walks the lockfile + registry for licenses/hashes — no fingerprint downloads. Drops into Trivy, Dependency-Track, FOSSA, npm audit signatures, anything that consumes the standards. - Headless CLI / CI mode —
nppm scanruns every scanner (OSV CVEs, scripts, patterns, binaries, maintainer, license, unused-deps) over every configured project, prints a compact text report or--jsonfor pipelines, and exits non-zero when any finding meets the--fail-on=info|warn|riskthreshold. Same caches as the dev server, so a warm second run is fast. - Unused-deps detector — depcheck-style per-project hygiene scan.
Three buckets: unused (declared but never imported), misplaced
(imported only from dev paths but listed as a regular dep), missing
(imported but undeclared). Built-in allowlist covers the usual
bin-tools (
vite,vitest,tsx,typescript,eslint,prettier,husky, …);scripts:references are recognised too. Pure regex + filesystem walk — no AST parse, no network. Remote projects are not in scope for v1. OwnUnusedtab in every per-project view. - History per project — every lockfile call snapshots the package state
and appends an entry for adds/removes/version changes (with CVE-hint
reason when applicable). Stored next to
nppm.jsonin.nppm/history/. Rendered as a vertical timeline with date-grouped pills and per-entry icons (+add-only,~update-only,−remove-only,●mixed). - Releases tab — registry timeline merged with GitHub release notes.
- Global scan — SSE-streamed CVE check across every project's lockfile, with progress bar in the topbar.
- Git dependencies —
git+https://,git@host:,github:/gitlab:/bitbucket:shorthand all fetch tarballs from the right host and feed the same scanners. - Templates / standards enforcement — declare which packages
(and which versions) a project should have, plus root metadata
(
engines,scripts,type,packageManager), plus shipped files (.editorconfig,tsconfig.base.json, …) with three per-file modes (create/merge-json/report-only). The Templates view renders a cross-project compliance matrix; the per-project Template tab shows the full diff grouped by severity, with a one-click "Apply selected" that writes a timestamped backup first. Templates live as JSON files atnppm-templates/<id>/template.jsonand / or come from remote URLs via the top-leveltemplateSources: string[](refresh on boot or via "+ Add remote source" in the Templates view). Supportsextendsfor inheritance, two modes (additive/strict),forbiddenlists, and per-workspace overrides. Project ↔ template assignment lives in the project form modal as a checkbox picker. - Health ring per project — every project entry in the treeview carries a small SVG progress ring with a 0–100 % score. Aggregates the per-package severity scores (CVEs, scripts, patterns, maintainer, integrity, freshness, cadence, typosquat) capped at the "risk" tier so a single loud package can't dominate, averaged across the project. Tiers: ≥80 green, ≥60 amber, <60 red. Fills in live as the asynchronous matrix data settles.
- Settings dialog + cache rebuild — gear button in the topbar
opens a tabbed editor over the non-
projectssections ofnppm.json(validated against the same VTS schema as the backend uses on load). The Cache section ships a "Clear cache now" button that wipes every pocket on disk, warms the registry via a fresh matrix walk, then re-runs the global OSV scan so the next interaction is against fresh data..nppm/history/is kept. - i18n — English by default, German included. Add a third language by
dropping
frontend/Util/Locales/<id>.tsand registering it inI18n.ts.
- Node ≥ 22 (current LTS line; Node 20 went EOL on 2026-04-30)
- A
nppm.jsonin the directory you launchnppmfrom - Projects either locally checked out or reachable via GitHub/Gitea contents API
git clone https://github.com/stefanwerfling/nppm
cd nppm
npm installCreate a nppm.json next to where you run nppm. Minimal example:
{
"projects": [
{"type": "local", "name": "kavula", "path": "/home/me/code/kavula"},
{"type": "local", "name": "swipemeister", "path": "/home/me/code/swipemeister"},
{
"type": "github",
"name": "vts",
"repo": "OpenSourcePKG/vts",
"ref": "main",
"token": "$GH_TOKEN"
},
{
"type": "gitea",
"name": "internal-app",
"url": "https://git.example.com/team/internal-app",
"token": "$GITEA_TOKEN"
}
],
"server": {"port": 5190},
"browser": {"open": false},
"registry": {"url": "https://registry.npmjs.org"},
"cache": {"dir": ".nppm/cache", "ttlMinutes": 60},
"security": {
"maintainer": {
"quickHandoverDays": 30,
"suspiciousGapDays": 180,
"matureVersions": 10,
"trustWindow": 20
},
"license": {
"allowlist": ["MIT", "Apache-2.0", "BSD-*", "ISC"],
"denylist": ["AGPL-*"],
"treatUnknownAs": "unknown"
},
"unused": {
"allowlist": ["my-internal-cli"],
"devPathGlobs": ["**/cypress/**", "**/*.bench.*"]
}
},
"actions": {
"allowInstall": false,
"editor": "phpstorm"
}
}The actions.allowInstall flag is off by default; while off the
Upgrade modal can only write package.json (a backup is taken first;
npm install is left for the user to run by hand). Setting it to
true unlocks both the "Edit + install (--ignore-scripts)" button
and the per-package "Run" button on the lifecycle-scripts list.
Always runs with --ignore-scripts — re-firing hooks is explicit and
per-package, never automatic.
The security.maintainer block is optional and tunes the publisher-
handover detector. Defaults reflect the empirical attack patterns:
handovers ≤ 30 d on a mature package land as risk, ≤ 180 d as
warn, longer gaps as info (likely community takeover of an
abandoned package). Strict projects can drop quickHandoverDays.
The security.license block is also optional. Without it the scanner
uses its built-in SPDX classification (MIT / Apache / BSD → permissive,
LGPL / MPL → weak-copyleft, GPL / AGPL → strong-copyleft, UNLICENSED /
free-text → proprietary). Patterns support a trailing * wildcard;
denylist wins over allowlist when both match. Set treatUnknownAs: "proprietary" to force a manual review for any package without a
recognised license.
The actions.editor field enables the per-row "Open in IDE" button in
the Installed view. Set it to one of vscode, vscodium, cursor,
phpstorm, webstorm, idea, subl; nppm builds the right
URL-handler link (vscode://file/…, phpstorm://open?file=…, …) and
relies on the OS to forward it to the running editor. Remote projects
and unknown editor keys hide the button silently.
The security.unused block is optional. allowlist is added to the
built-in bin-tool list (vite/tsx/eslint/…), so you only need to name
your project-specific extras — losing the defaults would re-introduce
a wall of false positives. devPathGlobs replaces the default
(**/*.test.*, **/*.spec.*, **/tests/**, **/*.config.*, …) when
non-empty so opinionated teams can shrink the dev-path set; leave it
out to keep the defaults.
$VAR_NAME references are expanded from the environment / .env at load
time, so secrets never live in the config file.
Optional .env next to nppm.json:
GH_TOKEN=ghp_xxx
GITEA_TOKEN=xxx
The GitHub token lifts the 60 req/h anonymous limit on the Releases API.
npm run dev
# 🚀 NPPM running at http://localhost:5190Open the URL in a browser. The default port is 5190 (Vite's 5173
would collide with vtseditor running alongside).
nppm scan # default: scan all, fail on risk
nppm scan --project=kavula --json # one project, machine-readable
nppm scan --fail-on=warn # tighter gate
nppm scan --no-osv --no-heuristics # offline / lockfile-free fast run
nppm scan --sarif > nppm.sarif # SARIF 2.1.0 for GitHub Code Scanning
nppm scan --help # full flag listThe repo ships its own composite action at
./.github/actions/scan. Two
output channels in one step:
- SARIF → fed to
github/codeql-action/upload-sarifso findings appear natively under the repo's Security → Code scanning alerts tab. - Sticky PR comment with the CVE delta between base and head
(re-uses the same
PrReviewBuilderthe dev UI's "PR" tab uses). Updated in place on every push — one comment per PR.
permissions:
contents: read
security-events: write
pull-requests: write
jobs:
nppm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: {fetch-depth: 0}
- uses: actions/setup-node@v4
with: {node-version: '22'}
- id: nppm
uses: stefanwerfling/nppm/.github/actions/scan@main
with:
fail-on: warn
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: ${{ steps.nppm.outputs.sarif-path }}Full inputs / outputs / sample comment in the action README.
nppm sbom --project=kavula # CycloneDX to stdout
nppm sbom --project=kavula --format=spdx # SPDX 2.3 JSON
nppm sbom --project=kavula --output=bom.json # write to file
nppm sbom --help # full flag listSame data via REST:
GET /api/projects/:id/sbom?format=cyclonedx (default) or ?format=spdx.
Content-Type is set to application/vnd.cyclonedx+json /
application/spdx+json so MIME-aware tooling can route the payload.
nppm scan reuses the same nppm.json and .nppm/cache/ as the dev
server, so a warm CI run skips network calls that have already been
made locally. Exit codes: 0 clean (or below threshold), 1 threshold
breached, 2 usage error (bad flag, missing config). Drop it into any
pipeline that understands non-zero exits.
Full screenshot-driven walkthrough lives in the per-language manuals linked at the top of this README. Quick chapter pointers:
- The cross-project matrix
- Drilling into one project
- Package detail panel
- Global CVE scan
- Headless CI mode
- SBOM export
- Upgrade modal
- Bulk-Update Wizard
- Vulnerability Timeline
- PR Review
- Cross-project Dashboard
- Impact analysis
- Badge filter
Cache pockets under .nppm/cache/ (configurable):
registry/— npm registry metadata (TTL)remote/— GitHub/Gitea contents API responses (TTL,{data: null}envelope for cached 404s)fingerprint/— full tarball fingerprints incl. per-JS file content (permanent — publishedpkg@versionis immutable)security/— OSV.dev responses (TTL)releases/— GitHub Releases API responses (TTL)bundlephobia/— gzip / minified size + transitive count perpkg@version(permanent)npm-user/— npm user-doc enrichment (2FA flag + account-creation date) keyed by username (TTL)templates-remote/— remote template bodies fetched fromtemplateSourcesURLs
The Settings dialog ships a "Clear cache now" button that wipes
every pocket in one shot (preserves the directory layout so the
in-memory JsonCache instances keep working) and re-warms the
registry + OSV cache afterwards.
History is not in the cache — it lives in .nppm/history/ next to
nppm.json so you can commit / inspect it independently.
npm testVitest, no network. Each scanner has unit tests; tarballs are built in-memory from synthetic tar blobs.
npm run docs:screenshotsSpawns the dev server, drives a headless Chromium through every view via
Puppeteer, and writes PNGs to doc/screenshots/. Requires npm install
to have brought in puppeteer.
For the module map and design decisions, see
CLAUDE.md.
MIT — see LICENSE.
