Introduction
GoAudit is a sandbox security scanner for JavaScript package installs. It inspects npm, pnpm, and bun installs and project upgrades for suspicious file reads, writes, process execution, and network behavior.
Use goaudit scan to audit a single npm, pnpm, or bun install command, or goaudit scan-project to audit a JavaScript project before upgrading dependencies. Other ecosystems and arbitrary shell commands are rejected.
Installation
Install the latest version with Homebrew (macOS and Linux):
$brew install --cask KushalMeghani1644/tap/goauditAlso available via mise:
$ mise use -g github:KushalMeghani1644/GoAudit-CLI
Or install directly with Go:
$ go install github.com/KushalMeghani1644/GoAudit-CLI/cmd/goaudit@latest
Requires Docker with the gVisor (runsc) runtime registered. GoAudit refuses to run when runsc is not registered with Docker — see gVisor Setup.
Usage
Audit one npm, pnpm, or bun install command inside the sandbox. Other ecosystems and arbitrary shell commands are rejected:
$ goaudit scan "npm install lodash"$ goaudit scan "pnpm add <package>"$ goaudit scan "bun add <package>"# Live findings + CI JSON$ goaudit scan "npm install <package>" --verbose$ goaudit scan "npm install <package>" --ci# Network controls$ goaudit scan "npm install <package>" --network off$ goaudit scan "npm install <package>" --offline# Sandbox options$ goaudit scan "npm install <package>" --skip-probe$ goaudit scan "npm install <package>" --warm-cache$ goaudit scan "npm install <package>" --no-cache$ goaudit scan "npm install <package>" --fail-on malicious
Yarn is not supported: there is no Yarn sandbox profile, so goaudit scan "yarn install" will not run a meaningful install, and scan-project rejects Yarn projects (yarn.lock). Convert to npm, pnpm, or bun if you need project-level scanning.
Common Flags
Flags shared by scan and scan-project unless noted:
| Flag | Purpose |
|---|---|
| --ci | JSON output for CI |
| --verbose | Live findings during the scan |
| --offline | Skip host-side npm registry requests |
| --network | Sandbox network policy: auto, on, or off (see Network Policy) |
| --skip-probe | Skip the post-install runtime probe |
| --fail-on | Exit non-zero on malicious, inconclusive, or both (default: never) |
| --warm-cache | scan only: prepare the sandbox without running a scan |
| --no-cache | Do not store a warm container after this run |
| --timeout | Max time for the install command |
| --probe-timeout | Max time for the runtime probe (default: 30s) |
| --mount-cwd | scan only: allow mounting CWD for multi-local package installs |
Package Manager Support
| Manager | scan | scan-project |
|---|---|---|
| npm | Yes | Yes |
| pnpm | Yes | Yes |
| bun | Yes | Yes |
| pip | No | No |
| yarn | No | No |
| Arbitrary shell commands | No | No |
npm, pnpm, and bun get the full workflow: npm registry metadata checks, sandbox install tracing, and a Node runtime probe after install.
yarn is not supported. There is no Yarn sandbox profile, so goaudit scan yarn install will not run a meaningful install. Yarn projects (yarn.lock) are rejected by scan-project. Convert to npm, pnpm, or bun if you need project-level scanning.
Other ecosystems (pip) and arbitrary shell commands (curl ... | sh) are rejected outright — scans are scoped to JavaScript installs only.
Network Policy
--network controls sandbox network access and, together with --offline, host-side npm registry requests.
With --network auto (default):
| Profile | Sandbox network |
|---|---|
| npm, pnpm, bun | On |
Use --network on or --network off to override. Combine with --offline to block host-side fetches even when the sandbox has network access.
scan-project
Audit an existing JavaScript project before upgrading dependencies. GoAudit reads package.json, detects npm / pnpm / bun, checks packages against the registry, then runs the upgrade install in a sandbox. Your host node_modules is not modified.
$ goaudit scan-project .$ goaudit scan-project ~/mywebsite --upgrade-mode ncu$ goaudit scan-project ~/monorepo --upgrade-mode update --ci$ goaudit scan-project ~/app --manager pnpm$ goaudit scan-project ~/app --include-transitive$ goaudit scan-project ~/app --probe-all$ goaudit scan-project ~/app --skip-probe$ goaudit scan-project ~/app --mount-project
Upgrade modes
| Mode | Behavior |
|---|---|
| refresh-lock | Remove lockfile and reinstall (default) |
| ncu | Run npm-check-updates, then install (bun uses bun update) |
| update | Run the package manager's update command |
Project-only flags
| Flag | Purpose |
|---|---|
| --upgrade-mode | refresh-lock, ncu, or update |
| --manager | Force npm, pnpm, or bun |
| --include-transitive | Also registry-check packages listed in the manager's lockfile (package-lock.json, pnpm-lock.yaml, or bun.lock) |
| --probe-all | Probe all direct dependencies at runtime, not only suspicious ones |
| --mount-project | Stage the full project tree (secret paths redacted) instead of manifests/lockfiles only |
A binary bun.lockb is detected but not parsed by --include-transitive. --warm-cache applies to scan only; project-staged sandboxes are not warm-cached.
Runtime Probe
After install, GoAudit can load each package entrypoint (require / dynamic import) and then run the package's declared CLI bin entries with --help, all under strace. Imports and bin probes share one probe timeout, and missing bins, launch failures, signals, and non-zero exits are reported instead of silently ignored, and the report explicitly states the probe covers imports and --help only. This applies to npm, pnpm, and bun only.
Default probe scope:
| Command | Default probe scope |
|---|---|
| scan | Packages named in the install command |
| scan-project | Packages with suspicious registry findings only |
- It does not exercise delayed timers, workers, interactive prompts, or arbitrary exported APIs. Only imports and
--helpinvocations are performed, and the report says so. - Disable with
--skip-probe. - For projects,
--probe-allprobes every direct dependency instead of only suspicious ones.
Project Staging
scan-project stages install inputs (manifests and lockfiles) into the sandbox by default so install scripts cannot read host secrets via /project-ro.
- Pass
--mount-projectfor a full-tree stage with known secret paths redacted. - Multi-local
scaninstalls refuse mounting the working directory unless--mount-cwdis set.
Static Analysis Coverage
Host-side static analysis checks install specs against the npm registry before the sandbox runs. Coverage limits are surfaced as explicit findings instead of silent skips:
- CLI scans registry-check at most 25 package specs; anything beyond that is skipped and reported as a
STATIC_COVERAGE_LIMITwarning. - Install commands with pipes, command substitution, or heavy quoting that defeat spec extraction produce a
PACKAGE_PARSE_INCOMPLETEwarning instead of silently passing. - Semver ranges and dist-tags resolve against the registry's
latesttag and emit anVERSION_RESOLUTION_APPROXIMATEinfo finding; requested versions missing from registry metadata emitVERSION_NOT_IN_METADATA. - Common wrappers (
sudo,env,nice,timeout,corepack, leadingFOO=barassignments) are stripped before parsing, and npm alias specs (pkg@npm:actual@1.2.3) are resolved to the package npm actually installs.
Cache
GoAudit caches prepared sandbox containers to speed up repeat scans.
$ goaudit cache status$ goaudit cache clean
Use --cache-dir or GOAUDIT_CACHE_DIR to store cache entries elsewhere. --warm-cache prepares the sandbox without running a scan.
Warm containers are reused only when they still match the request:
- A cached container is discarded if its network mode no longer matches the requested policy — offline scans never reuse a network-enabled container and vice versa.
- Between warm scans, mutable state is reset: temp dirs, the sandbox home, and package-manager caches are wiped, and honeypots are re-applied.
- Floating image tags (
latest,current-slim, …) are re-pulled so sandboxes cannot silently run stale images; versioned tags and digest-pinned references are reused.
Demo Output
Reports lead with a verdict and confidence, then group observed behavior, runtime probe results, static warnings, network activity, and a summary. Below is the actual output of a real scan of is-odd, a benign npm package.
Security & Honeypots
Target commands and runtime probes always execute as an unprivileged sandbox user. GoAudit injects realistic decoy credentials such as .ssh/id_rsa, .aws/credentials, Kubernetes configs, .env, .npmrc, and .git-credentials to bait credential theft.
The tracing engine watches for environment theft (/proc/self/environ), process injection (ptrace), fileless execution (memfd_create), privilege escalation ( the setuid / setgid family including setfsuid / setfsgid, and chmod calls that set SUID/SGID bits on system binaries), capability changes (capset), and privilege-relevant kernel operations such as namespace creation (unshare, setns, clone with CLONE_NEW* flags), chroot, keyring ownership changes (keyctl), and BPF program loads (bpf). It also watches mount and unmount attempts, file truncation, ownership changes, and hard/symbolic link manipulation. Data transfers are covered too, including multi-packet sends (sendmmsg) and zero-copy transfers (sendfile / splice), plus unauthorized network listeners.
Noise reduction: redundant network calls are deduplicated, expected registry traffic and common lifecycle noise are suppressed, and benign sandbox setup (for example su/PAM setuid during prep) is ignored before payload execution. Failed privilege attempts count as a single signal regardless of how many syscall families tried, so denied probes alone do not stack into a MALICIOUS verdict.
Limitations
GoAudit provides a risk assessment based on behavior and static indicators. It is not meant to prove absolute maliciousness.
- Package commands are not fully shell-parsed: pipes, substitutions, and complex wrappers may be incomplete. Unparseable installs are flagged (
PACKAGE_PARSE_INCOMPLETE) rather than silently analyzed. - Secret redaction under
--mount-projectcovers common paths, not every possible secret filename. - Private registry auth is not used for host-side static analysis.
- Semver ranges may resolve to registry
latestwith an approximate-version finding. - The runtime probe exercises imports and
bin --helponly; delayed timers, workers, interactive prompts, and arbitrary exported APIs are not covered.
Requirements
- Docker installed and running
- gVisor (
runsc) registered with Docker — required, not optional. GoAudit refuses to run whenrunscis not registered.
gVisor (runsc) Setup
GoAudit requires gVisor and exits with code 1 unless Docker lists runsc in docker info runtimes. Installing the binary is not enough: register it with Docker (for example in /etc/docker/daemon.json):
{"runtimes": {"runsc": {"path": "/usr/local/bin/runsc","runtimeArgs": ["--debug=false", "--platform=ptrace"]}},"default-runtime": "runc"}
Use runsc help platform for valid --platform values. Restart Docker, then verify:
docker info | grep -i runtimes
SELinux: gVisor cannot use Docker's default container SELinux labels. GoAudit sets --security-opt label=disable automatically for runsc containers.
Node sandbox image: when you keep the default --node-image, GoAudit uses ghcr.io/kushalmeghani1644/goaudit-node-sandbox:latest for Node-based scans.
No fallbacks: GoAudit never falls back to runc. If runtime verification or sandbox preparation fails, the scan stops rather than silently weakening isolation.