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.
- 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
v0.1.0 published
First npm release.
v0.2.0
Second published version.
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.