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.

Important: GoAudit is not meant for proving absolute maliciousness. It provides a risk assessment based on observed behavior and static indicators.

Installation

Install the latest version with Homebrew (macOS and Linux):

$brew install --cask KushalMeghani1644/tap/goaudit

Also 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:

FlagPurpose
--ciJSON output for CI
--verboseLive findings during the scan
--offlineSkip host-side npm registry requests
--networkSandbox network policy: auto, on, or off (see Network Policy)
--skip-probeSkip the post-install runtime probe
--fail-onExit non-zero on malicious, inconclusive, or both (default: never)
--warm-cachescan only: prepare the sandbox without running a scan
--no-cacheDo not store a warm container after this run
--timeoutMax time for the install command
--probe-timeoutMax time for the runtime probe (default: 30s)
--mount-cwdscan only: allow mounting CWD for multi-local package installs

Package Manager Support

Managerscanscan-project
npmYesYes
pnpmYesYes
bunYesYes
pipNoNo
yarnNoNo
Arbitrary shell commandsNoNo

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):

ProfileSandbox network
npm, pnpm, bunOn

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

ModeBehavior
refresh-lockRemove lockfile and reinstall (default)
ncuRun npm-check-updates, then install (bun uses bun update)
updateRun the package manager's update command

Project-only flags

FlagPurpose
--upgrade-moderefresh-lock, ncu, or update
--managerForce npm, pnpm, or bun
--include-transitiveAlso registry-check packages listed in the manager's lockfile (package-lock.json, pnpm-lock.yaml, or bun.lock)
--probe-allProbe all direct dependencies at runtime, not only suspicious ones
--mount-projectStage 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:

CommandDefault probe scope
scanPackages named in the install command
scan-projectPackages with suspicious registry findings only
  • It does not exercise delayed timers, workers, interactive prompts, or arbitrary exported APIs. Only imports and --help invocations are performed, and the report says so.
  • Disable with --skip-probe.
  • For projects, --probe-all probes 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-project for a full-tree stage with known secret paths redacted.
  • Multi-local scan installs refuse mounting the working directory unless --mount-cwd is 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_LIMIT warning.
  • Install commands with pipes, command substitution, or heavy quoting that defeat spec extraction produce a PACKAGE_PARSE_INCOMPLETE warning instead of silently passing.
  • Semver ranges and dist-tags resolve against the registry's latest tag and emit an VERSION_RESOLUTION_APPROXIMATE info finding; requested versions missing from registry metadata emit VERSION_NOT_IN_METADATA.
  • Common wrappers (sudo, env, nice, timeout, corepack, leading FOO=bar assignments) 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.

$ goaudit scan "npm install is-odd"
GoAudit Report ──────────────────────────────────────────────────────────────── Command: npm install is-odd Verdict: SUSPICIOUS (confidence: 55) Sandbox: gVisor (runsc) What GoAudit Observed 1. GoAudit did not collect enough clear behavioral evidence to describe the run. Runtime Probe - Runtime import probe completed without suspicious behavior - No credential access, suspicious writes, or unknown exfiltration detected during import Static Warnings ──────────────────────────────────────────────────────────────── 1. [WARNING] LIFECYCLE SCRIPTS: npm install is-odd Details: npm install runs lifecycle scripts (preinstall/postinstall) — this is common Network Activity (expected) - 1 connection(s) to registry.npmjs.org (registry) - 1 connection(s) to 1 host(s) Summary: 0 critical (0 install-time, 0 probe, 0 static), 1 warnings, 13 informational Use --ci for full JSON output.

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-project covers common paths, not every possible secret filename.
  • Private registry auth is not used for host-side static analysis.
  • Semver ranges may resolve to registry latest with an approximate-version finding.
  • The runtime probe exercises imports and bin --help only; 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 when runsc is 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.