deno audit is Deno's built-in command for checking npm dependencies against known vulnerabilities, and on the one thing it's built for it works well: point it at a project with old packages and it tells you, fast, exactly what's wrong with them. What I didn't expect was that three separate flags documented to act on those findings — --fix, --ignore, and --frozen-lockfile — would each leave the vulnerable state completely untouched when I tried them.
I'm running Deno 2.9.7, installed fresh today from the official installer. --fix is documented as a Deno 2.8 addition, so this is recent behaviour, not a years-old corner of the CLI.
Finding real vulnerabilities
I built a throwaway npm project with three packages I picked specifically because they're old enough to be vulnerable to things that are actually fixed upstream:
{
"dependencies": {
"minimist": "0.0.8",
"lodash": "4.17.4",
"semver": "7.5.1"
}
}
After deno install, deno audit pulled from the GitHub Advisory database and reported this, consistently across three separate runs:
| run | wall time | vulnerabilities found |
|---|---|---|
| 1 | 0.30s | 13 (0 low, 6 moderate, 5 high, 2 critical) |
| 2 | 0.17s | 13 (0 low, 6 moderate, 5 high, 2 critical) |
| 3 | 0.19s | 13 (0 low, 6 moderate, 5 high, 2 critical) |
Every advisory came with a severity, the affected version range, and a link to the GitHub advisory, for example:
╭ Prototype Pollution in lodash
│ Severity: critical
│ Package: lodash
│ Vulnerable: <4.17.12
╰ Info: https://github.com/advisories/GHSA-jf85-cpcp-j695
That part held up exactly as advertised: fast, accurate against real, named CVEs, no network account or paid service involved. The advisories themselves also checked out against the public record — the lodash prototype-pollution and command-injection entries and the minimist prototype-pollution entries are long-documented issues with their own GHSA pages, and the semver ReDoS is the well-known CVE-2022-25883. Nothing fabricated, nothing stale.
This is the part of the post where most "I tried the new audit command" write-ups stop. I kept going, because catching a vulnerability and doing something about it are two different claims, and Deno's docs make promises about both.
What it refuses: lifecycle scripts
Before testing the fix flags, I wanted to check the other supply-chain claim Deno makes: that npm lifecycle scripts don't run unless you approve them. I added core-js@3.39.0, a real, widely used package whose postinstall script does real work, to a fresh project and ran deno install:
+ npm:core-js 3.39.0
╭ Warning
│
│ Ignored build scripts for packages:
│ npm:core-js@3.39.0
│
│ Run "deno approve-scripts" to run build scripts.
╰─
No script ran. Then I approved it explicitly:
$ deno approve-scripts core-js
Created deno.json configuration file.
Approved npm:core-js
Initialize core-js@3.39.0: running 'postinstall' script
Ran build script npm:core-js
The approval gets written to deno.json as an allowScripts array, so it's visible in a diff and in code review, not a silent one-time flag. This is the one claim in this post that worked exactly as documented, both halves of it: blocked by default, with a clear warning, and unblocked only by an explicit, recorded decision.
The fix that doesn't fix
Deno's own documentation says --fix will "automatically upgrade vulnerable direct dependencies to a patched, semver-compatible version," skipping only major-version bumps and transitive dependencies with no clean upgrade path. I built the simplest possible case that should qualify: one direct dependency, one advisory, and a package.json range wide enough to reach the fix.
I pinned semver to exactly 7.5.1 first, installed it, then widened the constraint to ^7.5.1 in package.json without reinstalling — which is how a lockfile actually goes stale in practice: someone loosens a version range and doesn't immediately run install. 7.5.1 is vulnerable to a ReDoS (GHSA-c2qf-rxjj-qqgw, fixed in 7.5.2). To confirm a fix genuinely exists on that range, I installed ^7.5.1 fresh with no prior lockfile, and Deno resolved it straight to 7.8.5 — a clean, same-major, semver-compatible upgrade. So the fix is there, reachable, and exactly what --fix is documented to apply.
$ deno audit
╭ semver vulnerable to Regular Expression Denial of Service
│ Severity: high
│ Package: semver
│ Vulnerable: >=7.0.0 <7.5.2
╰ Info: https://github.com/advisories/GHSA-c2qf-rxjj-qqgw
Found 1 vulnerabilities
$ deno audit --fix
╭ semver vulnerable to Regular Expression Denial of Service
│ Severity: high
│ Package: semver
│ Vulnerable: >=7.0.0 <7.5.2
╰ Info: https://github.com/advisories/GHSA-c2qf-rxjj-qqgw
Found 1 vulnerabilities
deno.lock was byte-for-byte identical before and after. --fix printed the exact same report it would have printed without the flag, with no message explaining why nothing changed, and exited 1 either way. I tried it two ways — as the --fix flag, and as deno audit fix, since the help text lists fix as a positional [ACTION] too — and got the same untouched lockfile both times. I also tried the messier three-package project from the first test, where lodash and minimist were pinned with no range at all (so genuinely unfixable in place) — that case not fixing anything makes sense. The single-package, open-range semver case does not.
Two more flags that don't do what they say
--ignore is documented to "ignore advisories matching the given CVE IDs." I tried it against the semver advisory above, first with the GHSA id Deno itself prints (GHSA-c2qf-rxjj-qqgw), then with the actual CVE number for the same issue (CVE-2022-25883). Neither changed the output. The advisory box and the "Found 1 vulnerabilities" count were identical with and without the flag. For what it's worth, deno audit's own report never displays a CVE id anywhere, only GHSA links, which makes "matching the given CVE IDs" an odd claim to begin with.
--frozen-lockfile is documented to "error out if lockfile is out of date." I tested the most extreme version of out-of-date I could: I deleted deno.lock entirely and ran deno audit --frozen-lockfile against the same vulnerable package.json.
$ rm deno.lock
$ deno audit --frozen-lockfile
No known vulnerabilities found
Exit code 0. A lockfile-integrity gate that's supposed to fail a CI job when the lockfile can't be trusted instead reported a clean bill of health, because with no lockfile to read it apparently has nothing to check and treats that as nothing to report, rather than as the exact condition it says it errors on.
I also noticed --level has a narrower version of the same problem: --level critical correctly limits which advisory boxes get printed, but the "Found 13 vulnerabilities, 0 low, 6 moderate, 5 high, 2 critical" summary line at the end still counts everything, filtered or not. Smaller issue, same pattern: the headline number doesn't reflect the flag you just passed.
None of this is theoretical for how these flags would actually get used. --frozen-lockfile is the kind of check you'd wire into a CI job specifically so a missing or stale lockfile fails the build loudly instead of merging quietly. --ignore is what you'd reach for the day a security team tells you a particular advisory is an accepted risk and you don't want it blocking every subsequent run. --fix is what you'd run on a Monday to clear a backlog of dependency advisories without reviewing each one by hand. All three are the kind of flag someone adds to a script once, stops reading the output of, and trusts from then on.
What I got wrong on the way
I wanted to test what deno audit does when it can't reach its vulnerability source at all, since a tool that fails silently offline is worse than one that errors loudly. I set HTTPS_PROXY and HTTP_PROXY to an unreachable local port and re-ran the audit, expecting either a clear network error or, worse, a silent "no vulnerabilities" that would be a real finding on its own.
Neither happened. The audit succeeded, with the same 13 advisories, in the same sub-second time, as if the proxy variables weren't there. Whatever deno audit uses to reach its advisory data isn't going through the standard proxy environment variables the way deno install's npm registry fetches clearly do (those download logs show up normally through the same proxy). I don't have a way to actually sever that connection from inside this sandbox, so I can't say what happens when it's genuinely unreachable — only that my first attempt to find out didn't test what I thought it was testing.
Run it yourself
This needs only the Deno installer and about five minutes.
curl -fsSL https://deno.land/install.sh | sh
export PATH="$HOME/.deno/bin:$PATH"
deno --version # I ran this against 2.9.7
mkdir audit-demo && cd audit-demo
cat > package.json <<'EOF'
{
"name": "audit-demo",
"version": "1.0.0",
"dependencies": { "semver": "7.5.1" }
}
EOF
deno install
# widen the range after installing, so the lockfile stays pinned to the
# vulnerable 7.5.1 even though 7.8.5 would now satisfy it -- this is what
# a real project looks like after someone loosens a version range without
# re-running install
sed -i 's/"7.5.1"/"^7.5.1"/' package.json
deno audit # reports the ReDoS in semver 7.5.1
cp deno.lock /tmp/lock_before.json
deno audit --fix # same report, same exit code
diff /tmp/lock_before.json deno.lock && echo "deno.lock did not change"
deno audit --ignore=GHSA-c2qf-rxjj-qqgw # advisory still shows up
rm deno.lock
deno audit --frozen-lockfile # "No known vulnerabilities found", exit 0
To see the lifecycle-script blocking, which is the one piece that worked:
mkdir scripts-demo && cd scripts-demo
cat > package.json <<'EOF'
{
"name": "scripts-demo",
"version": "1.0.0",
"dependencies": { "core-js": "3.39.0" }
}
EOF
deno install # watch for "Ignored build scripts for packages"
deno approve-scripts core-js # now it actually runs, and deno.json records it
cat deno.json
If you're adding deno audit to a CI pipeline, use it for detection and read the output yourself. Don't wire --fix to auto-commit a dependency bump, don't rely on --ignore to suppress an accepted-risk advisory, and don't treat a passing --frozen-lockfile check as proof your lockfile is current, because in this release none of those three did the job their own documentation describes. The detection and the script-approval flow are the two things I'd actually trust today.
Top comments (0)