ReleasedSecurityOpen Source

VeriPatch

Verified remediation for npm vulnerabilities. Scans a project, applies candidate fixes inside a sandbox, proves the vulnerability is eliminated and the fix is safe, then emits an audit-grade evidence report.

TypeScriptNode.jsDockerCLI

The problem

Dependency auto-fixes are applied on trust: nothing proves the vulnerability is gone or that the fix did not break the project.

Context and constraints

Dependency scanners produce a queue of advisories and an automated fix command. What neither produces is evidence: after the bump, nothing has checked that the vulnerable package is actually gone from the resolved tree, or that the project still builds. The fix is applied on trust.

  • The code being verified is attacker-controlled input. A package.json, a lockfile or an installed package can be crafted to attack the parser or to run code during install.
  • Verification has to execute the project's own install, build and test commands, which means running untrusted code on a developer machine or a CI runner.
  • Advisory data arrives over the network from a third party and cannot be trusted blindly.
  • The original working tree must never be modified by a verification run.
  • It has to be usable in CI, which means deterministic exit codes and machine-readable output rather than log text.

What I built

A TypeScript CLI, published on npm under Apache-2.0, that scans a lockfile against OSV.dev, ranks findings by severity against fix feasibility, applies a candidate remediation to a staged copy of the project inside a hardened Docker sandbox, independently re-scans the resolved dependency tree, runs the build and tests, and emits a Markdown and JSON evidence report describing what was actually observed.

Architecture

Two paths from one CLI: a read-only scan that produces a ranked queue, and a verification run that executes inside a container and ends in an evidence report. The verdict comes from exit codes and an independent re-scan, never from parsing log text.

parsequeryadvisoriescandidate fixmount copyresolved treeverdictVeriPatch CLINode 20+, commanderLockfile parsernpm, yarn, pnpmOSV.devAdvisory intelligenceRule engineRanking and fix resolverStaged copyExcludes .git and .envDocker sandboxNon-root, caps droppedIndependent re-scanResolved treeEvidence reportMarkdown and JSON
Architecture as described in the project's own documentation. The repository is public, so this can be checked directly against the source rather than taken on trust.
VeriPatch CLI
Node 20+, commander
Lockfile parser
npm, yarn, pnpm
OSV.dev
Advisory intelligence
Rule engine
Ranking and fix resolver
Staged copy
Excludes .git and .env
Docker sandbox
Non-root, caps dropped
Independent re-scan
Resolved tree
Evidence report
Markdown and JSON

My contribution

Author and maintainer.

Technical decisions

What was chosen, why, and what it cost.

Decide from an independent re-scan, never from log text

Decision
A verification verdict is computed from process exit codes, an independent re-scan of the resolved dependency tree, the build result and the test result. No decision is made by matching strings in output.
Why
Log-text heuristics are what make a tool confidently wrong. Re-scanning the tree checks the thing that actually matters — whether the vulnerable package is still resolved — instead of whether the fixer said it succeeded.
Trade-off
A re-scan and a full install cost far more time than reading a fixer's output, so verification is measured in minutes rather than seconds.

Install with lifecycle scripts disabled

Decision
The sandboxed install runs with lifecycle scripts disabled, so a malicious postinstall in a scanned or bumped dependency never executes at all.
Why
Install-time script execution is the actual attack path for a hostile package. Disabling it removes the vector rather than trying to contain it after it runs.
Trade-off
Packages that genuinely need a postinstall step — native builds, for example — will not be exercised the way they would be in a real install.

A fix can only ever be a version bump of the same package

Decision
The fix resolver enforces structurally that a remediation is a version change to the same package, never a substitution with a different one, and the invariant is covered by property-based tests.
Why
Advisory data comes from the network. If poisoned data could name a replacement package, the tool would become a delivery mechanism for dependency confusion.
Trade-off
A vulnerability whose only real remedy is switching to a different package cannot be remediated automatically; it has to be reported and handled by a person.

Refuse to verify yarn and pnpm projects rather than guess

Decision
Scanning supports npm, yarn classic, yarn berry and pnpm lockfiles, but verify and update replay fixes with npm only. Yarn and pnpm projects are explicitly refused instead of being attempted.
Why
Replaying an npm fix into a yarn or pnpm lockfile risks corrupting it. Refusing is honest about the tool's reach; attempting it would trade a clear limitation for a silent one.
Trade-off
A large share of real projects can be scanned but not verified, which is tracked as open work rather than presented as solved.

Verify a staged copy, never the working tree

Decision
The container bind-mounts a staged copy of the project that excludes node_modules, .git and any .env file, so the original tree and any secrets in it are never exposed to the sandbox.
Why
Verification deliberately runs untrusted code. Giving it the developer's real working tree — including credentials and git history — would make the tool the risk it exists to reduce.
Trade-off
Staging a copy costs disk and time on every run, and a project that depends on git metadata at build time will not behave identically inside the sandbox.

Security and reliability

The failure modes that shaped the implementation.

Hardened container

The sandbox runs as a non-root user with all capabilities dropped, no-new-privileges set, pid, memory and CPU limits applied, and the container removed on teardown.

Two-phase network

The container gets a dedicated per-run bridge network during install, then is fully disconnected before the build and test phases run.

Hostile lockfiles are parsed defensively

Inputs are size-capped before parsing, handled only by real parsers rather than evaluation, stripped recursively of prototype-pollution keys, and validated against the real npm package-name grammar. Yarn classic uses a deliberately rigid grammar where anything unexpected is a hard error rather than a guess.

Advisory data is validated at the boundary

Advisories arrive over HTTPS with certificate validation and are schema-validated at the OSV adapter; malformed entries are dropped and counted rather than trusted.

Report and terminal injection

Every externally sourced string — advisory text, package names, sandboxed process output — is ANSI-stripped and metacharacter-escaped before it reaches a terminal or a Markdown report.

Its own supply chain

Minimal dependencies, a committed lockfile, GitHub Actions pinned by commit SHA, and publishing with provenance. The tool writes only inside its own project-local and home cache directories, handles no secrets and sends no telemetry.

Verification

How the implementation was checked, and how much of that can be shown publicly.

  • Six categories of testevidenced

    The repository separates unit, integration, contract, end-to-end, benchmark and fixture suites, run with Vitest under a single check script alongside type-checking, linting and format verification.

  • Property-based tests on the fix resolverevidenced

    fast-check is used to cover the invariant that a resolved fix is always a version bump of the same package, which is the property that keeps poisoned advisory data from becoming a package substitution.

  • Architectural boundaries enforced by lintevidenced

    eslint-plugin-boundaries enforces the separation between cli, core, services, adapters and shared, so the layering is checked mechanically rather than by review.

  • Published and releasedevidenced

    Five versions published to npm under Apache-2.0, current release v0.3.1, with nine GitHub releases.

Results

  • The CLI contract and the report.json schema are treated as stable, while the project itself is deliberately still pre-1.0.
  • Scanning covers npm v2 and v3 lockfiles, yarn classic, yarn berry and pnpm v6 and v9; verification and update are npm-only by design.
  • Ranking orders findings by severity against fix feasibility, so the output is a remediation queue rather than an undifferentiated alert list.

Timeline

  1. v0.1.0 published

    First npm release.

  2. v0.2.0

    Second published version.

  3. v0.3.1 current

    Current published release.

Limitations and disclosure

What this project does not do, and what cannot be shown publicly.

  • Network isolation is bridge-level, not domain-level: during the install phase the container has general egress, so the real defence against a malicious postinstall is that lifecycle scripts are disabled, not the network boundary.
  • A confidence verdict reflects the project's own build and test commands exiting successfully, not whether those checks are honest. The re-scan independently confirms the vulnerability is gone; it does not re-verify the project's test assertions.
  • Docker containers share the host kernel, so a container-escape vulnerability in the runtime itself is outside what this can mitigate.
  • Verification and update replay fixes with npm; yarn and pnpm projects are refused rather than risking lockfile corruption.

The repository is public and Apache-2.0 licensed, so everything described here can be checked against the source. The residual risks below are the project's own documented limitations, reproduced rather than softened: a successful verification is not a claim that a package or an application is secure.

Other projects in the same engineering domains.