Skip to content

stefanwerfling/nppm

nppm logo

nppm — Node Project Package Manager

CI status License: MIT Release Node ≥ 22 Tests Scanners Security policy

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).

Matrix view

git + npm interweave into nppm — the best combination

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.

Documentation

Features

  • Cross-project matrix — packages as rows, projects as columns, traffic lights for aligned / outdated / drift / unknown. Workspaces collapse to one column per project (with a WS badge 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 fake latest from a same-named registry collision — the Latest column shows the upstream HEAD as 1.0.28 · 7d3f12a (version + short SHA) for GitHub and Gitea hosts, falling back to a plain git pill 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 or node_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 under bin/)
    • File-churn detection (suspiciously large patch bumps)
    • Maintainer-handover detection — compares each version's _npmUser against the publishers of the recent predecessors. Short gap + new publisher on a mature package raises risk (event-stream / ua-parser-js profile); long-silence handovers stay info (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 via security.license in nppm.json.
    • Lockfile-Integrity Cross-Check — compares the lockfile's pinned resolved + integrity per entry against what the npm registry currently serves. Detects mirror-hijack / dependency-confusion / lockfile-injection as risk, custom-mirror redirects as info. 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-scripts is safe / needed / risky.
    • Deprecation — reads per-version deprecated from the packument (free, no extra HTTP call). DEP! when the installed version is deprecated, DEP? when only latest is. Shows the maintainer's reason verbatim.
    • Code-obfuscation detection — per-JS-file heuristic over the fingerprint cache: obfuscator.io _0x identifier density, eval(atob(…)) chains, hex-string arrays, pathologically long lines. Path classifier caps dist//*.min.js artifacts at info so legitimate minification doesn't get flagged.
    • Manifest red-flags — pure heuristics over package.json: missing README, no description, no files[] allowlist, many bin entries, the native-build+postinstall combo, dated engines.node range. 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's latest for 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 scan delta, 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 Trends tab 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 info i icon 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 as known-at-install (red — installed an already-disclosed vuln), disclosed-during-use (yellow — vuln filed while you were running it), or pre-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 git button (disabled when no .git/ is detected) that walks git log -- package-lock.json (or package.json when 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 a declared-only pill 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.json between two git refs (default main vs HEAD) 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 planned package.json edit, 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 manual npm install) or "Edit + install --ignore-scripts" (gated by actions.allowInstall=true in nppm.json). After install, the modal lists every install lifecycle hook in node_modules with 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 through SafePath.join, which refuses anything that isn't a strict descendant of the project root — ../../etc/cron.d/evil workspace paths and malicious template file entries are both contained at the endpoint boundary.
  • Open in IDE — small IDE button next to every installed package's path in the Installed view. Opens node_modules/<pkg> in the editor configured under actions.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 → to plus a security heads-up), then applies all edits — one shared backup per project — and optionally runs npm install --ignore-scripts sequentially per project. Same actions.allowInstall gate as the single-package modal.
  • SBOM exportnppm sbom --format=cyclonedx|spdx (or the GET /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 modenppm scan runs every scanner (OSV CVEs, scripts, patterns, binaries, maintainer, license, unused-deps) over every configured project, prints a compact text report or --json for pipelines, and exits non-zero when any finding meets the --fail-on=info|warn|risk threshold. 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. Own Unused tab 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.json in .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 dependenciesgit+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 at nppm-templates/<id>/template.json and / or come from remote URLs via the top-level templateSources: string[] (refresh on boot or via "+ Add remote source" in the Templates view). Supports extends for inheritance, two modes (additive / strict), forbidden lists, 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-projects sections of nppm.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>.ts and registering it in I18n.ts.

Requirements

  • Node ≥ 22 (current LTS line; Node 20 went EOL on 2026-04-30)
  • A nppm.json in the directory you launch nppm from
  • Projects either locally checked out or reachable via GitHub/Gitea contents API

Setup

git clone https://github.com/stefanwerfling/nppm
cd nppm
npm install

Configuration

Create 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.

Run

npm run dev
# 🚀 NPPM running at http://localhost:5190

Open the URL in a browser. The default port is 5190 (Vite's 5173 would collide with vtseditor running alongside).

CI mode

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 list

GitHub Action (drop-in)

The repo ships its own composite action at ./.github/actions/scan. Two output channels in one step:

  • SARIF → fed to github/codeql-action/upload-sarif so 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 PrReviewBuilder the 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.

SBOM export

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 list

Same 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.

Usage

Full screenshot-driven walkthrough lives in the per-language manuals linked at the top of this README. Quick chapter pointers:

Caches

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 — published pkg@version is immutable)
  • security/ — OSV.dev responses (TTL)
  • releases/ — GitHub Releases API responses (TTL)
  • bundlephobia/ — gzip / minified size + transitive count per pkg@version (permanent)
  • npm-user/ — npm user-doc enrichment (2FA flag + account-creation date) keyed by username (TTL)
  • templates-remote/ — remote template bodies fetched from templateSources URLs

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.

Tests

npm test

Vitest, no network. Each scanner has unit tests; tarballs are built in-memory from synthetic tar blobs.

Generate screenshots for the manuals

npm run docs:screenshots

Spawns 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.

Architecture pointer

For the module map and design decisions, see CLAUDE.md.

License

MIT — see LICENSE.

About

Node Project Package Manager

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages