lockwarden audit
Execution-surface audit of the resolved dependency tree.
Synopsis
Section titled “Synopsis”lockwarden audit [--diff <base-ref> | --deep] [--verbose] [--baseline <path> | --no-baseline] [--write-baseline]Usage: lockwarden audit [options]
execution-surface audit of the resolved dependency tree
Options: --diff <base-ref> delta-score only packages whose resolved version changed vs a git ref --deep full-tree delta scan (fetches previous version of every dep — slow) (default: false) --verbose include Low findings in SARIF output (default: false) --baseline <path> baseline file (default: <dir>/.lockwarden-baseline.json) --no-baseline ignore any baseline file --write-baseline create/update the baseline from current findings (default: false) -h, --help display help for commandaudit enumerates every execution vector in the resolved tree — npm lifecycle scripts,
binding.gyp / node-gyp build hooks, AI-agent hooks and MCP manifests, IDE task files,
prebuilt .node binaries and their fetcher toolchains, phantom dependencies, obfuscation
markers, and file-size anomalies — and grades each package A–F. See
Scoring for the full signal table.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--diff <base-ref> | string | — | Delta-score only packages whose resolved version changed vs a git ref |
--deep | boolean | false | Full-tree delta scan — fetches the previous version of every dependency (slow) |
--verbose | boolean | false | Include Low findings in SARIF output |
--baseline <path> | string | <dir>/.lockwarden-baseline.json | Baseline file location; an explicit path must exist (exit 2 otherwise) |
--no-baseline | boolean | false | Ignore any baseline file |
--write-baseline | boolean | false | Create/update the baseline from current findings, then exit 0 |
--diff and --deep are mutually exclusive (combining them exits 2). All
global flags apply. audit analyzes one project per
run — with multiple --dir values, the first is used and the rest are ignored with a
warning (run once per directory for monorepos; see
CI recipes → monorepos).
Default — absolute scan, zero network
Section titled “Default — absolute scan, zero network”Scores what exists in the tree today using absolute weights only. Analyzes package
contents from node_modules (packages not installed are skipped with a warning — Layer-2
known-bad matching still covers every resolved version). Runs entirely offline.
--diff <base-ref> — the PR flow
Section titled “--diff <base-ref> — the PR flow”Compares the working lockfile against the one committed at <base-ref> and delta-scores
only the packages whose resolved version changed. For each changed package it fetches
the previous version’s tarball (SRI-verified against the base lockfile’s integrity
hash, cached in ~/.lockwarden/cache) and scores what the new version introduced —
the signal every 2026 attack exhibited. These fetches are the
only network calls in the tool.
--deep — full-tree delta
Section titled “--deep — full-tree delta”Fetches the previous version of every dependency and delta-scores the whole tree. Explicitly slow; intended for periodic scheduled runs, not PRs.
Baseline
Section titled “Baseline”Real trees carry accepted execution surface — esbuild’s install script, sharp’s native
binaries. Without a baseline, adopting --threshold med or low in CI means failing on
findings you have already reviewed. A baseline file records those accepted findings so
CI fails only on new ones — the delta-over-absolute
rule applied to adoption.
# Review current findings, then accept them:lockwarden audit --write-baseline
# Commit the file; from now on only NEW findings fail the run:lockwarden audit --ci --threshold med--write-baseline writes .lockwarden-baseline.json next to the lockfile (or to
--baseline <path>):
{ "version": 1, "generatedAt": "2026-07-05", "tool": "lockwarden@0.4.0", "entries": [ { "code": "LW002-BINDING-GYP", "package": "with-gyp", "version": "1.0.0", "addedAt": "2026-07-05" }, { "code": "LW001-LIFECYCLE", "package": "with-post", "version": "1.0.0", "addedAt": "2026-07-05" } ]}Entries are cleartext and diff-reviewable. code + package are the match key; add a
"reason" (shown as the SARIF suppression justification) and an optional "expires"
ISO date (on/after it, the entry stops suppressing and a warning is printed). version
and addedAt are audit trail only.
Matching is version-independent. An accepted LW001-LIFECYCLE on esbuild stays
accepted when esbuild bumps 0.21.5 → 0.21.6 — the surface that persists across versions
is exactly what a baseline is for. What changes between versions is caught by the
delta analyzers (--diff) and the Layer-2 known-bad overlay, which a baseline can never
mute:
- Layer-2 findings (known-bad packages) are never suppressible.
- Critical findings are never suppressible.
- Delta findings on a grade-F package are never suppressible (protects corpus-elevated compound Criticals).
--write-baseline skips these with a printed note; hand-edited entries matching them are
ignored with a warning. Suppressed findings stay visible: dimmed [suppressed] lines in
human output, a suppressed array per package in --json,
and SARIF results carrying the standard suppressions property (GitHub code scanning
shows them as suppressed instead of open).
Re-running --write-baseline preserves the addedAt/reason/expires of surviving
entries and prunes entries that no longer match anything.
Example 1 — absolute scan
Section titled “Example 1 — absolute scan”npx lockwarden auditgrade C — 2 packages flagged of 2 analyzedmed 1 · low 1lockfile: package-lock.json (npm) — mode: absoluteadvisories: OSV 2026-07-03 · newest incident 2026-06-09 (2 days old)
with-post@1.0.0 — grade C [med] LW001-LIFECYCLE package.json — lifecycle script "postinstall" runs automatically on install
with-gyp@1.0.0 — grade B [low] LW002-BINDING-GYP binding.gyp — native build hook: binding.gyp present (binding.gyp)Exit 0 — a postinstall and a binding.gyp existing is Med/Low, below the default
high threshold. Absolute findings are inventory, not alarm: legitimate native packages
carry binding.gyp forever. (This output also feeds the
ignore-scripts allowlist workflow.)
Example 2 — --diff: what did this bump introduce?
Section titled “Example 2 — --diff: what did this bump introduce?”npx lockwarden audit --diff maingrade F — 2 packages flagged of 2 analyzedcritical 2 · med 2lockfile: package-lock.json (npm) — mode: diff
dep-a@1.0.1 — grade F [critical] LW001D-LIFECYCLE-INTRODUCED package.json — lifecycle script "postinstall" is NEW in 1.0.1 (absent in 1.0.0) [med] LW001-LIFECYCLE package.json — lifecycle script "postinstall" runs automatically on install [med] LW008-PHANTOM package.json — declared dependency "dep-b" (^1.0.0) is never imported in 2 JS/TS files (plain-crypto-js pattern)
dep-b@1.0.0 — grade F [critical] LW006D-PATCH-DEP-INTRODUCED — new transitive dependency dep-b@1.0.0 entered the tree alongside patch bump(s): dep-a 1.0.0 → 1.0.1Exit 1. The same script that is med for existing is Critical for being new
(LW001D), and the new transitive dep arriving under a patch bump is Critical on its own
(LW006D) — the axios/plain-crypto-js shape. Interpreting each delta code:
dependency review.
Example 3 — CI variants
Section titled “Example 3 — CI variants”npx lockwarden audit --diff main --ci # PR gate: exit code onlynpx lockwarden audit --diff HEAD~1 --sarif # what did the last commit change?npx lockwarden audit --offline # airgapped: exit 2 if any fetch attemptednpx lockwarden audit --diff main --offline # works when the tarball cache is warmnpx lockwarden audit --threshold critical # only Critical findings fail the runThe --offline failure mode, verbatim:
lockwarden: --offline is set but a network call to https://registry.npmjs.org/nested-lib/-/nested-lib-3.0.2.tgz was attempted hint: Remove --offline, or avoid flags that require tarball fetches (--diff/--deep).Exit 2. With a warm cache the same command succeeds — cache hits never touch the
network. See the warm-cache pattern.
--json output
Section titled “--json output”Stable, snapshot-tested shape (trimmed to one finding here):
{ "command": "audit", "mode": "diff", "lockfile": { "path": "/work/app/package-lock.json", "type": "npm" }, "packages": [ { "name": "dep-a", "version": "1.0.1", "key": "dep-a@1.0.1", "grade": "F", "findings": [ { "layer": 1, "signal": { "analyzer": "lifecycle-scripts", "code": "LW001D-LIFECYCLE-INTRODUCED", "kind": "delta", "package": { "name": "dep-a", "version": "1.0.1" }, "evidence": { "file": "package.json", "excerpt": "\"postinstall\": \"node install.js\"", "detail": "lifecycle script \"postinstall\" is NEW in 1.0.1 (absent in 1.0.0)" }, "metrics": { "introduced": 1, "changed": 0 } }, "severity": "critical" } ] } ], "rollup": { "grade": "F", "packagesAnalyzed": 2, "packagesFlagged": 2, "counts": { "none": 0, "low": 0, "med": 2, "high": 0, "critical": 2 } }, "warnings": [], "advisories": { "osvGeneratedAt": "2026-07-03", "newestIncident": "2026-06-09" }}Only flagged packages appear in packages. When a baseline is applied,
three additive fields appear: a top-level baseline object
({ path, entries, matched, expired }), rollup.suppressedCounts, and a per-package
suppressed array (each element is a finding plus a
suppression: { reason?, addedAt?, expires? } object) — packages whose findings are all
suppressed stay in packages with re-derived grades. Full field tables:
JSON output → audit.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 | No findings at or above --threshold (default: high) |
1 | Findings at or above --threshold |
2 | Execution error — unparseable lockfile, lockfile missing at the --diff ref, --diff combined with --deep, invalid --threshold, a malformed or missing explicit --baseline file, --write-baseline combined with --json/--sarif/--no-baseline, or a network call attempted under --offline |
Output modes
Section titled “Output modes”--json: the stable report above.--sarif: SARIF 2.1.0 mapped Critical→error, High→warning, Med→note; Low is suppressed unless--verbose. Uploadable straight to the GitHub Security tab (the GitHub Action does this for you).
See also
Section titled “See also”- Scoring — weights, grades, elevations, the corpus gate.
- Dependency review — the review workflow around
--diff. drift— the companion lockfile-tampering check.scan— the same analysis applied to shipped artifacts.