@arcane-spark/ubel-node
v0.2.0
Published
Supply-chain security firewall for Node.js — resolves dependencies, scans via OSV.dev and NVD, and enforces configurable vulnerability policies before anything reaches node_modules.
Maintainers
Readme
UBEL — Unified Bill / Enforced Law
Node.js Supply-Chain Security CLI
Ubel resolves dependencies, generates PURLs, scans them through OSV.dev and NVD, and enforces configurable security policies at install-time to block supply-chain attacks before they reach production.
This document covers the Node.js ecosystem (npm, pnpm, bun, yarn).
Features
- Full dependency resolution with PURL generation via lockfile dry-run
- OSV.dev vulnerability scanning via batched API queries and NVD's APIs
- Concurrent vulnerability enrichment (CVSS, fix recommendations, references)
- Policy engine — block/allow by severity threshold and unknown-severity packages
- Malicious package (infection) detection — always blocked regardless of policy
checkmode — dry-run resolution and scan with no side effectsinstallmode — scan-gate before installation; blocks if policy violatedhealthmode — scan the current project's installed dependencies- Atomic lockfile revert — originals are always restored on violation or error
- Disk-based lockfile backup under
.ubel/lockfiles/<timestamp>/with manual recovery on failure - Dependency graph with introduced-by and parent tracking
- Automatic report generation: timestamped JSON (
*.json) + HTML (*.html) + SBOM (*.cdx.json) + SARIF (*.sarif.json) per scan, pluslatest.*convenience links - Zero external runtime dependencies (Node.js stdlib only)
- Complete compliant, and enriched SBOM Cyclonedx v1.6 files with full dependencies and vulnerabilities data in VEX.
- Complete compliant, and enriched SARIF v2.1.0 files.
Installation
npm install -g @arcane-spark/ubel-nodeAfter installation, the following entry-point binaries are available:
| Binary | Package Manager |
|---|---|
| ubel-npm | npm |
| ubel-pnpm | pnpm |
| ubel-bun | bun |
| ubel-agent | AI agent workspace scan ( OS, runtimes, tools, dependencies ) |
| ubel-platform | Host platform scan (OS, runtimes, tools) |
yarn does not support a lockfile-only dry-run —
yarn addalways writesnode_modules. UBEL supports yarn inhealthscan mode only and cannot provide install-blocking firewall coverage for it.
Requirements
- Node.js
>=18.0.0 - The package manager binary being targeted (
npm,pnpm, orbun) must be available onPATH
Usage
ubel-npm <mode> [packages...]
ubel-pnpm <mode> [packages...]
ubel-bun <mode> [packages...]Package arguments are optional for check and install — when omitted, the existing lockfile in the working directory is used as the dependency source.
Firewall Mechanics
npm
ubel-npm check and ubel-npm install <pkg> invoke npm's --package-lock-only flag, which resolves the full dependency tree and writes a candidate package-lock.json without touching node_modules/. UBEL scans the candidate lockfile, then makes a binary decision:
- Clean — the candidate lockfile is accepted and the actual install proceeds via
npm ci. - Violation —
package-lock.jsonis reverted to its pre-scan state from the disk backup.node_modules/is never touched. The process exits non-zero.
pnpm
Identical flow to npm, using pnpm's --lockfile-only flag. The candidate pnpm-lock.yaml is written, scanned, then either accepted or reverted. node_modules/ is never written during the scan phase.
bun
Uses bun's --lockfile-only flag. The candidate bun.lock is written and scanned before any node_modules/ mutation. The revert path is identical to npm and pnpm.
UBEL's firewall always blocks pre/post install scripts to prevent running malicious scripts
All the 3 package manager are triggered with the flag: --ignore-scripts
Lockfile backup and recovery
Before any dry-run mutation, originals are backed up to .ubel/lockfiles/<timestamp>/. If the revert itself fails (e.g. a disk error mid-restore), the original lockfile is preserved at the backup path and its location is printed to stderr so the user can recover manually.
TOCTOU integrity protection
After the dry-run completes and the scan passes policy, there is a window between the scan decision and the real install during which the on-disk lockfile or package.json could be mutated — by another process, a racing script, or a compromised tool. UBEL closes this window with SHA-256 integrity checks before any real install is allowed to proceed.
At the end of every dry-run, UBEL captures two digests in memory:
_candidateLockfileHash— SHA-256 of the raw candidate lockfile bytes written to disk by the dry-run (package-lock.json,pnpm-lock.yaml, orbun.lock)._candidatePackageJsonHash— SHA-256 ofpackage.jsonas it exists on disk after the dry-run. For npm, this digest is re-captured after UBEL regeneratespackage.jsonwith exact pinned versions from the lockfile, so the hash always reflects the file that will be present at install time.
Immediately before invoking the real install command (npm ci, pnpm install --frozen-lockfile, bun install --frozen-lockfile), both files are re-hashed from disk and compared against the in-memory digests. If either hash does not match, the install is aborted and the lockfile is reverted — nothing is written to node_modules/. The mismatch details (expected hash, actual hash, file path) are printed to stderr.
Lockfile integrity check FAILED — the lockfile was modified after scanning.
Expected : a3f1…
Got : 9c2b…
File : /project/package-lock.jsonIf no lockfile existed before the dry-run (fresh project), the absence itself is recorded as the expected state and enforced the same way.
This protection also extends to the backup manifest files created earlier before reverting the changes.
Modes
health
Scans the current project's installed dependency graph without running any install. Reads the existing lockfile directly and submits resolved packages to OSV.dev and NVD's APIs.
ubel-npm health
ubel-pnpm health
ubel-bun healthFull-stack monorepo scanning
When invoked programmatically with full_stack: true, health walks the entire directory tree from the project root and collects packages across all supported ecosystems in a single pass — no per-language configuration required. Mixed-stack monorepos (e.g. a Node.js frontend, Python backend, Rust service, and Go tooling in the same repo) are fully covered in one invocation.
| Ecosystem | Package Manager | Resolved From |
|---|---|---|
| Node.js | npm, pnpm, yarn, bun | node_modules/ (on-disk walk) |
| Python | pip / virtualenv | .venv, venv, virtualenv directories |
| PHP | Composer | vendor/ |
| Rust | Cargo | Cargo.lock |
| Go | Go Modules | go.sum |
| C# / .NET | NuGet | packages.lock.json / obj/project.assets.json |
| Java | Maven | pom.xml resolved dependencies |
| Ruby | Bundler | Gemfile.lock |
Each discovered package is deduplicated by PURL before submission, so packages shared across sub-projects are scanned exactly once.
Platform scanning (Linux)
When invoked with scan_os: true on Linux, the scanner reads the host's system package database directly — no elevated privileges required — and includes all installed system packages in the scan inventory.
| Distribution | Package Manager | Source | PURL type |
|---|---|---|---|
| Ubuntu | dpkg | /var/lib/dpkg/status | pkg:deb/ubuntu/ |
| Debian | dpkg | /var/lib/dpkg/status | pkg:deb/debian/ |
| Alpine / Alpaquita | apk | /lib/apk/db/installed | pkg:apk/alpine/ |
| Red Hat / RHEL | rpm | rpm -qa | pkg:rpm/redhat/ |
| AlmaLinux | rpm | rpm -qa | pkg:rpm/almalinux/ |
| Rocky Linux | rpm | rpm -qa | pkg:rpm/rocky-linux/ |
| CentOS / Fedora | rpm | rpm -qa | pkg:rpm/redhat/ |
Each package entry includes its binary install paths and direct dependency edges as reported by the package database.
Platform scanning (Windows)
When invoked with scan_os: true on Windows, the scanner probes the registry and known binary paths — no elevated privileges required — and enumerates the following software components using CPE 2.3 identifiers:
| Category | Components | |---|---| | Operating system | Windows 10 / 11 (build-accurate CPE version) | | Security | Windows Defender | | Runtimes | Node.js, Python, PHP, Go, Rust, Ruby, JRE, JDK | | .NET | All installed .NET Core / Desktop / ASP.NET runtimes (multi-version) | | Browsers | Chrome, Firefox, Microsoft Edge | | Developer tools | Git, Docker Desktop, Visual Studio, Cursor | | Shell | PowerShell |
check
Dry-run: resolves the given packages (or the existing lockfile) via a lockfile-only pass, scans the resolved set, and exits. Nothing is installed and lockfiles are fully reverted to their original state afterwards.
# Scan specific packages without installing
ubel-npm check lodash express
# Scan the current lockfile with no changes
ubel-npm checkExits 0 if policy passes, 1 if policy blocks or the scan fails.
install
Same pipeline as check, but proceeds to install (via npm ci / pnpm install --frozen-lockfile / bun install) if and only if the policy decision is allow.
ubel-npm install [email protected] express
ubel-npm install # resolves from existing lockfile
ubel-pnpm install react react-dom
ubel-bun installIf the policy blocks, installation is aborted, the lockfile is reverted, and the process exits 1.
threshold
Sets the severity level at or above which vulnerabilities block the scan. Accepts low, medium, high, critical, or none (disable threshold blocking).
ubel-npm threshold high # block high and critical
ubel-npm threshold critical # block critical only
ubel-npm threshold none # disable severity blockingInfections (MAL-* advisories) are always blocked regardless of this setting.
The threshold is persisted to the local policy file and applies to all subsequent scans until changed.
block-unknown
Controls whether packages with unknown-severity vulnerabilities are blocked.
ubel-npm block-unknown true
ubel-npm block-unknown falsePolicy
Policy is stored as JSON at .ubel/local/policy/config.json relative to the project root.
Default policy created on first run:
{
"severity_threshold": "high",
"block_unknown_vulnerabilities": true
}Severity threshold — vulnerabilities at or above this level cause a block. Severity order: low → medium → high → critical.
Block unknown — when true, any vulnerability whose severity cannot be determined also causes a block.
Infections — advisories with IDs beginning MAL- are always blocked and are not subject to either setting above.
Package Argument Validation
All package specifiers passed to check and install are validated against an allow-list pattern before any subprocess is invoked. Accepted formats:
name
name@version
@scope/name
@scope/name@versionSpecifiers containing shell metacharacters or other unsafe characters are rejected immediately and the process exits non-zero before any filesystem or network operation occurs.
Programmatic API
main() doubles as a programmatic entry point for agents, platform scanners, and the VS Code extension:
import { main } from "@arcane-spark/ubel-node";
const report = await main({
projectRoot : "/abs/path/to/project",
engine : "npm",
mode : "health",
is_script : true,
save_reports: true,
scan_os : false,
full_stack : false,
scan_node : true,
scan_scope : "repository", // repository | agent | developer_platform | editor_extension
});
// report is the full finalJson object (inventory, vulnerabilities, decision, …)When called this way, the banner and interactive console output are suppressed. The return value is the same machine-readable report object written to disk.
IMPORTANT: The programmatic API is primarily exposed for lightweight integrations and automation around UBEL's CLI and VS Code workflows. UBEL is designed first and foremost as a single-operation CLI/runtime scanner, not as a multi-tenant concurrent scanning library.
main() is therefore currently not concurrency-safe. Do not invoke multiple install, check, or health operations in parallel within the same process, as report generation and internal scan state may overlap.
Run operations sequentially until parallel execution support is introduced in a future release.
Reports
Every scan writes two files to a timestamped path and overwrites the latest* convenience links:
.ubel/reports/latest.json ← always current
.ubel/reports/latest.html ← always current
.ubel/reports/latest.cdx.json ← always current
.ubel/reports/latest.sarif.json ← always current
.ubel/local/reports/<ecosystem>/<mode>/<YYYY>/<MM>/<DD>/
<ecosystem>_<mode>_<engine>__<timestamp>.json
<ecosystem>_<mode>_<engine>__<timestamp>.html
<ecosystem>_<mode>_<engine>__<timestamp>.cdx.json
<ecosystem>_<mode>_<engine>__<timestamp>.sarif.jsonThe HTML report is fully self-contained (no server required) and includes:
- Dashboard with severity breakdown chart and policy decision
- Searchable, filterable vulnerability table
- Full inventory with state (safe / vulnerable / infected / undetermined)
- Interactive force-directed dependency graph with vulnerable-subtree filter
- Per-vulnerability detail modals (CVSS vector, fix recommendations, OSV/NVD references)
- System and runtime metadata
The JSON report contains the full machine-readable equivalent and can be consumed by CI/CD tooling directly.
CI/CD Integration
All CLI commands exit non-zero on policy violations, making them native to any CI runner:
# GitHub Actions
- name: UBEL dependency scan
run: ubel-npm check
- name: UBEL firewall-gated install
run: ubel-npm install# Dockerfile
RUN ubel-npm installQuick-start examples
# Scan the current lockfile without installing anything
ubel-npm check
# Gate the actual install behind a policy scan
ubel-npm install
# Scan a single package for vulnerabilities before it touches node_modules
ubel-npm check [email protected]
# Tighten policy, then re-scan
ubel-npm threshold critical
ubel-npm check
# Scan the installed project dependencies
ubel-npm health
# Same workflows with pnpm and bun
ubel-pnpm install react react-dom
ubel-bun checkUbel — Secure every dependency, before it reaches production.
