warden-sandbox
v0.1.2
Published
Runtime capability sandboxing for Node.js dependencies
Maintainers
Readme
warden-sandbox
Runtime capability sandboxing for Node.js dependencies.
Warden addresses a class of supply-chain attacks where malicious code executes at load time (require/import), not at installation — bypassing npm's postinstall block. It scans your node_modules with static AST analysis, generates a lockfile of declared capabilities per package, and in enforcement mode creates isolated Compartments per dependency using ses so that a compromised package cannot access the network, filesystem, or environment variables it never declared needing.
Table of Contents
- Installation
- Workflow
- Commands
- Configuration file
- Lockfile format
- Capabilities
- Package statuses
- Profiles
- Performance
- Security limitations
- License
- Contributing
Installation
npm install -g warden-sandbox
# or per-project
npm install --save-dev warden-sandboxRequires Node.js >= 18.19 for ESM enforcement (module.register). CommonJS enforcement works on all Node.js >= 16.
Workflow
npm install
warden scan # analyse node_modules, write warden.lock.json
warden approve --all # mark all packages as reviewed (first time only)
git add warden.lock.jsonAfter that, on every dependency update:
warden update # wraps npm update + auto-scan
warden diff # see what new capabilities appeared
warden approve <pkg> # approve each changed package individually
warden verify # CI gate — exits non-zero on any deviationTo investigate a specific package:
warden explain axios # show which files/lines triggered each capability
warden graph # visualise the full dependency tree with capabilities
warden audit # risk breakdown across all packagesTo run a script under observation or enforcement:
warden run script.js # logs violations, never blocks
warden run --enforce script.js # blocks violations at runtimeCommands
warden scan
Scans node_modules using static AST analysis and generates or updates warden.lock.json. Each package's JS files are parsed to detect which Node.js built-in modules are imported, and the results are grouped into capability categories.
Packages already present in the lockfile with a matching SHA-256 content hash are skipped — the scan is incremental and fast on subsequent runs.
warden scan [--dir <path>] [--no-progress] [--verbose] [--json]| Flag | Description |
|---|---|
| --dir <path> | Project directory to scan (default: cwd) |
| --no-progress | Suppress the progress bar — useful in CI |
| --verbose | Print per-package timing, file count, and cache hit/miss status |
| --json | Print the resulting lockfile JSON to stdout |
Commit warden.lock.json to version control. New packages appear as pending-review and must be explicitly approved before enforcement can be enabled.
Example output:
Warden Scan Summary
Total packages: 67
With capabilities: 23
Native addons: 1
Clean: 44
Packages with detected capabilities:
[email protected] [ENV] [FS-READ] [FS-WRITE] [NETWORK] [SPAWN]
[email protected] [ENV]warden diff
Compares the current state of node_modules against the committed lockfile and reports any capability changes. Run this after npm update to understand the security impact before approving anything.
warden diff [--dir <path>] [--from <lockfile>] [--to <lockfile>] [--since <date>] [-v]| Flag | Description |
|---|---|
| --dir <path> | Project directory (default: cwd) |
| --from <lockfile> | Baseline lockfile to compare against (default: warden.lock.json) |
| --to <lockfile> | Lockfile to compare to (default: scan current node_modules now) |
| --since <date> | Compare against the lockfile from git history at the given date (e.g. "2026-08-01"). Requires warden.lock.json to be committed. |
| -v, --verbose | Also show packages whose code changed but capabilities stayed the same |
warden diff --since 2026-08-01Example output:
Warden Diff
New packages: 1
New capabilities: 1
!! Packages with NEW capabilities (review required):
[email protected]
+ Added: [NETWORK]
note: New capabilities detected after update: network.warden verify
Compares the current state of node_modules against the committed lockfile. Exits 1 if any package has a changed content hash, new capabilities, or a pending-review status. Packages listed under ignore in .wardenrc.json are skipped.
warden verify [--dir <path>] [--json] [--sarif] [--junit]| Flag | Description |
|---|---|
| --dir <path> | Project directory (default: cwd) |
| --json | Output { "ok": boolean, "violations": string[] } |
| --sarif | Output SARIF 2.1.0 JSON for GitHub Advanced Security / code scanning integration |
| --junit | Output JUnit XML for Jenkins, GitLab CI, and other test-report consumers |
# GitHub Actions
- run: warden verify
# With SARIF upload for GitHub code scanning
- run: warden verify --sarif > warden.sarif || true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: warden.sarif
# With JUnit XML for GitLab CI / Jenkins
- run: warden verify --junit > warden-report.xml || truewarden approve
Marks packages as reviewed in warden.lock.json. Approval is tied to the current content hash — any code change resets the package to pending-review.
warden approve [packages...] [--all] [--rescan] [--capability <cap>] [--deny <cap>] [--note <text>]| Flag | Description |
|---|---|
| packages... | Package names (axios) or versioned keys ([email protected]). Multiple accepted. |
| --all | Approve all pending-review packages at once |
| --rescan | Re-scan packages from disk before approving |
| --capability <cap> | Approve only this one capability — all others will be blocked at runtime even if declared |
| --deny <cap> | Explicitly block one capability — the package is approved for everything else it declared, but this one is denied at runtime. Deny always wins: a cap in both allowedCapabilities and deniedCapabilities is blocked. |
| --note <text> | Attach a human-readable note to the approval (stored in warden.lock.json for audit trail) |
Examples:
warden approve axios
warden approve axios lodash debug
warden approve --all
warden approve got --capability network
warden approve axios --deny filesystem-write
warden approve some-pkg --note "reviewed by alice 2026-08-02, only reads config files"
warden approve --all --rescan--capability and --deny are complementary:
--capability network-> allow onlynetwork, block everything else--deny filesystem-write-> allow everything declared, but blockfilesystem-write
Re-approving a package without --deny preserves any existing deniedCapabilities. Use warden unapprove to fully reset a package's approval state.
warden unapprove
Reverts packages back to pending-review, removing all approval data (allowedCapabilities, deniedCapabilities, note). Useful when you realise an approval was too broad.
warden unapprove [packages...] [--all] [--dir <path>]| Flag | Description |
|---|---|
| packages... | Package names or versioned keys to unapprove |
| --all | Unapprove all currently approved packages |
| --dir <path> | Project directory (default: cwd) |
warden unapprove axios
warden unapprove --allwarden run
Executes a Node.js script with Warden's module hooks installed. Detects ESM (.mjs, or "type": "module") vs CommonJS automatically.
warden run <script> [--enforce] [--watch] [--profile <profile>] [--timeout <ms>] [--dir <path>]
[--json] [--on-violation <action>] [--stack]| Flag | Description |
|---|---|
| --enforce | Block capability violations instead of logging them |
| --watch | Re-run the script whenever it changes on disk |
| --profile <profile> | strict, default, or lenient — see Profiles |
| --timeout <ms> | Kill the script if it runs longer than N milliseconds |
| --dir <path> | Project directory where warden.lock.json lives (default: cwd) |
| --json | Emit violations as NDJSON ({"level":"warn","source":"warden",...}) instead of human-readable text |
| --on-violation <action> | log (default), block (throw on first violation), or prompt (interactive) |
| --stack | Capture and attach a V8 stack trace to each violation event |
Observation mode (default): every violation is logged and a summary is printed at the end. Execution is never interrupted.
Enforcement mode (--enforce): CJS packages run in ses Compartments with only approved capabilities as endowments. ESM imports are replaced with stub modules that throw on any access. Packages with pending-review status have zero capabilities in enforce mode.
warden run script.js
warden run --enforce script.js
warden run --enforce --profile strict script.js
warden run --enforce --watch --timeout 5000 script.jsWatch mode: spawns a fresh child process on each file save, so SES lockdown resets cleanly.
warden status
Shows all packages grouped by approval status. Reads warden.lock.json directly — no rescan.
warden status [--dir <path>]Warden Status
+ Approved 62
! Pending review 3
x Unsandboxed 1
~ Unanalyzable 1
Pending review — run `warden approve` to unblock:
- [email protected] [NETWORK] [ENV]warden audit
Shows which packages use each capability, grouped by risk level (HIGH / MEDIUM / LOW).
warden audit [--dir <path>] [--json] [--fix]| Flag | Description |
|---|---|
| --dir <path> | Project directory (default: cwd) |
| --json | Output { "total": number, "byCapability": { "[cap]": ["pkg@version"] } } |
| --fix | Auto-approve all pending-review packages whose only capabilities are env-access and/or filesystem-read (low-risk only). Writes the lockfile. |
warden audit --fixWarden Capability Audit
67 packages total — 44 clean, 23 with capabilities
HIGH RISK
[NETWORK] network 5 pkgs [email protected], [email protected] +3 more
[SPAWN] process-spawn 2 pkgs [email protected], [email protected]
MEDIUM RISK
[FS-WRITE] filesystem-write 8 pkgs [email protected] +7 more
[ENV] env-access 15 pkgs [email protected] +14 more
LOW RISK
[FS-READ] filesystem-read 12 pkgs [email protected] +11 morewarden report
Generates a Markdown security report summarising the current lockfile state. Includes a risk-scored table of all packages, packages pending review, and a capability breakdown.
warden report [--dir <path>] [--output <file>] [--format md]| Flag | Description |
|---|---|
| --dir <path> | Project directory (default: cwd) |
| --output <file> | Output file path (default: warden-report.md) |
| --format <fmt> | Output format. Currently only md is supported. |
warden report
warden report --output security/warden-report.mdThe report includes:
- A summary table (total, approved, pending, unsandboxed, high-risk count)
- Top 10 highest-risk packages by score with their capabilities
- All packages pending review
- Capability breakdown table
warden explain
Shows exactly which files and line numbers triggered each capability detection for a given package. Useful for understanding why a package was flagged and deciding whether it is a false positive.
warden explain <pkg> [--dir <path>] [--json]| Flag | Description |
|---|---|
| <pkg> | Package name (axios) or versioned key ([email protected]) |
| --dir <path> | Project directory (default: cwd) |
| --json | Output raw JSON with full evidence list |
Example output:
Warden Explain: [email protected]
Location: /project/node_modules/axios
[NETWORK] (3 occurrences)
lib/adapters/http.js:14
const http = require('http')
lib/adapters/http.js:15
const https = require('https')
lib/adapters/http.js:18
const followRedirects = require('follow-redirects')
[ENV] (1 occurrence)
lib/defaults/index.js:22
process.env.npm_package_versionwarden graph
Visualises the dependency tree with capability annotations. Shows which packages bring in high-risk capabilities (network, spawn, eval, native) transitively.
warden graph [pkg] [--dir <path>] [--depth <n>] [--json]| Flag | Description |
|---|---|
| pkg | Optional starting package. If omitted, shows the full graph from top-level packages. |
| --dir <path> | Project directory (default: cwd) |
| --depth <n> | Maximum tree depth to display (default: 3) |
| --json | Output adjacency JSON { "pkg@version": { capabilities, deps } } |
Example output:
Warden Dependency Graph
[email protected]
|-- [email protected] [NETWORK] [ENV]
| `-- [email protected] [NETWORK]
|-- [email protected]
| |-- [email protected] [EVAL]
| `-- [email protected] [FS-READ]
`-- [email protected] [ENV]Packages with high-risk capabilities ([NETWORK], [SPAWN], [EVAL], [NATIVE]) are shown in bold red.
warden install
Wraps your package manager's install command, then automatically rescans node_modules and reports packages needing review. All unknown flags are forwarded to the package manager. Warden auto-detects which package manager to use: if pnpm-lock.yaml is present it uses pnpm, if yarn.lock is present it uses yarn, otherwise it uses npm.
warden install [packages...] [--approve-all] [--dir <path>] [...pm flags]| Flag | Description |
|---|---|
| packages... | Packages to install — forwarded to the package manager |
| --approve-all | Auto-approve all new packages (or set autoApprove: true in .wardenrc.json) |
| --dir <path> | Project directory (default: cwd) |
warden install axios
warden install typescript --save-dev
warden install --approve-all
warden install some-package --legacy-peer-depswarden update
Wraps your package manager's update command, then rescans and reports packages whose capabilities changed. Uses the same auto-detection as warden install (pnpm, yarn, or npm).
warden update [packages...] [--approve-all] [--dir <path>] [...pm flags]| Flag | Description |
|---|---|
| packages... | Packages to update — forwarded to the package manager (updates all if omitted) |
| --approve-all | Auto-approve all changed packages after update |
| --dir <path> | Project directory (default: cwd) |
warden update
warden update axios
warden update --approve-allAfter update, Warden lists every package whose capabilities changed. Without --approve-all, run warden diff to review and then warden approve to accept.
warden watch
Watches node_modules for filesystem changes and automatically rescans when dependencies change. Reports a capability diff after each change and warns if new capabilities are detected.
warden watch [--dir <path>]| Flag | Description |
|---|---|
| --dir <path> | Project directory (default: cwd) |
warden watch
warden watch --dir /path/to/projectChanges are debounced by 1 second to avoid thrashing during multi-file installs. Press Ctrl+C to stop. Requires an existing warden.lock.json (run warden scan first).
warden integrity
Reads package-lock.json and checks that all packages were installed from the npm registry and have a valid integrity hash. Catches packages installed from git refs, local paths, or private URLs that bypass the registry.
warden integrity [--dir <path>] [--json]| Flag | Description |
|---|---|
| --dir <path> | Project directory (default: cwd) |
| --json | Output { "checked": number, "issues": [{ package, issue }] } |
Exit codes: 0 = all clear, 2 = issues found.
warden integrity
warden integrity --jsonExample output when issues are found:
Warden Integrity Check
Checked 67 packages from package-lock.json
2 package(s) with integrity concerns:
- [email protected]
non-registry source: git+ssh://[email protected]/acme/internal-tool.git
- [email protected]
missing integrity hash — package may have been installed from a local path or git refwarden config init
Interactive wizard that creates a .wardenrc.json configuration file in your project directory.
warden config init [--dir <path>]Warden config init
Default profile [strict/default/lenient] (default: default): strict
Auto-approve new packages after warden install/update? [y/N]: n
Packages to ignore in warden verify (comma-separated, leave empty for none): internal-pkg
Created .wardenrc.json:
{
"profile": "strict",
"ignore": ["internal-pkg"]
}warden config recommend
Analyses your package.json and generates a recommended .wardenrc.json tailored to your project type — no interactive prompts. Detected types: frontend, backend, cli, library, unknown.
warden config recommend [--dir <path>]| Project type | Recommended profile | Rationale |
|---|---|---|
| frontend | lenient | Bundlers and dev tools legitimately need broad filesystem and env access |
| backend / cli | default | Standard enforcement; known network packages added to ignore automatically |
| library | strict | Minimal surface area — env access blocked even if declared |
| unknown | default | Falls back to safe default when type cannot be inferred |
warden config recommend
# Detected project type: backend
# Recommended .wardenrc.json:
# {
# "profile": "default",
# "autoApprove": false,
# "ignore": ["axios"]
# }Configuration File (.wardenrc.json)
A .wardenrc.json file at the project root provides defaults for CLI flags. CLI flags always take precedence over config values.
{
"profile": "strict",
"autoApprove": false,
"ignore": ["my-trusted-internal-package"]
}| Field | Type | Description |
|---|---|---|
| profile | "strict" \| "default" \| "lenient" | Default profile for warden run |
| autoApprove | boolean | Equivalent to passing --approve-all to warden install / warden update |
| ignore | string[] | Package names excluded from warden verify violations (still scanned and in lockfile) |
Create it interactively with warden config init, or write it manually.
warden.lock.json Format
{
"version": 1,
"generatedAt": "2026-08-02T10:00:00.000Z",
"packages": {
"[email protected]": {
"contentHash": "sha256:abc...",
"capabilities": [],
"status": "approved"
},
"[email protected]": {
"contentHash": "sha256:def...",
"capabilities": ["network", "env-access"],
"allowedCapabilities": ["network"],
"status": "approved",
"note": "reviewed by alice 2026-08-02 — only uses network for HTTP requests"
},
"[email protected]": {
"contentHash": "sha256:ghi...",
"capabilities": ["network", "filesystem-write"],
"deniedCapabilities": ["filesystem-write"],
"status": "approved"
},
"[email protected]": {
"contentHash": "sha256:jkl...",
"capabilities": ["network", "env-access"],
"status": "pending-review",
"note": "New capabilities detected after update: network."
}
}
}| Field | Type | Description |
|---|---|---|
| contentHash | string | SHA-256 of all JS files. Any code change invalidates the approval. |
| capabilities | string[] | Full list detected by static analysis. Do not edit manually. |
| allowedCapabilities | string[]? | Set by --capability. The runtime enforces this restricted list instead of capabilities. |
| deniedCapabilities | string[]? | Set by --deny. The runtime removes these from the effective capability set. |
| status | string | approved, pending-review, unsandboxed, or unanalyzable |
| note | string? | Human-readable note (auto-generated on capability changes, or set with --note). |
Capabilities
| Label | Key | What it covers |
|---|---|---|
| [NETWORK] | network | node:http, node:https, node:net, node:tls, node:dns, node:dgram, node:http2 |
| [SPAWN] | process-spawn | node:child_process, node:cluster, node:worker_threads |
| [EVAL] | dynamic-code | eval, new Function(), node:vm |
| [NATIVE] | native-binding | .node binary addon |
| [FS-WRITE] | filesystem-write | writeFile, mkdir, unlink, rename, ... |
| [ENV] | env-access | process.env reads |
| [FS-READ] | filesystem-read | readFile, stat, readdir, createReadStream, ... |
Package Statuses
| Status | Meaning |
|---|---|
| approved | Capabilities reviewed and accepted. Hash matches last approval. |
| pending-review | New package, or capabilities/code changed since last approval. In enforce mode: zero capabilities allowed. |
| unsandboxed | Native .node addon — cannot be sandboxed. Runs with full OS access. |
| unanalyzable | All JS files failed to parse (obfuscated code). No capability claims can be made. |
Exit Codes
All Warden commands follow a consistent exit code convention:
| Code | Meaning |
|---|---|
| 0 | Success — no issues |
| 1 | Policy violation — a package violated its declared capability policy |
| 2 | Runtime error — invalid arguments, missing file, corrupt lockfile, etc. |
Profiles
The --profile flag (or profile in .wardenrc.json) adjusts what is enforced beyond the lockfile.
| Profile | Behaviour |
|---|---|
| default | Packages may use any capability that is declared and approved |
| strict | env-access is additionally blocked even if declared and approved |
| lenient | filesystem-read and env-access are always allowed regardless of approval status |
warden run --enforce --profile strict script.js
warden run --enforce --profile lenient script.jsPerformance
Warden maintains a shared scan cache at ~/.warden/cache/. After a package has been scanned once on your machine, subsequent scans of the same version (by content hash) are instant regardless of which project you are scanning. The local project lockfile cache is also used as a first-level cache, with the global cache as a fallback.
To disable the global cache for a single run, you can clear the directory:
rm -rf ~/.warden/cache/Security Limitations
These are design constraints, not bugs.
Static analysis has blind spots
The AST scanner cannot detect capabilities hidden behind runtime eval, dynamic require(variable), or obfuscated code. When these patterns appear, Warden flags the package as dynamic-code / pending-review. approved status is not a security proof — it means declared capabilities were reviewed by a human.
Native addons cannot be sandboxed
A .node addon is a compiled binary loaded directly into the process. Warden detects these and marks them unsandboxed, but cannot restrict them at runtime.
Observation mode never blocks
warden run without --enforce only logs violations. This is intentional — a false positive that crashes production is worse than no protection. Test under --enforce in staging before enabling in production.
SES lockdown is process-wide and irreversible
Enforcement mode calls SES's lockdown(), which deep-freezes all JavaScript primordials. This is irreversible for the lifetime of the process and may surface latent bugs in code that mutates built-in prototypes. --watch works around this by spawning a fresh child process per run.
ESM enforcement requires Node.js >= 18.19
The ESM hook uses module.register(), stabilised in Node.js 18.19 / 20.6. On older versions, ESM hooks are silently disabled and only CJS enforcement is active.
License
BSD 2-Clause License — Copyright (c) 2026, r-seize. See LICENSE.
Contributing
Issues and pull requests are welcome at github.com/r-seize/warden-sandbox.
