DEV Community

neve7r
neve7r

Posted on

"Why I stopped trusting npm audit (and built my own)"

Generate a CycloneDX SBOM and deterministic, audit-ready risk report from your package-lock.json.

You run npm audit. It says “47 vulnerabilities.”

Cool.

Which ones actually matter?

The one in your production bundle?
The dev-only Jest dependency?
The transitive package you didn’t even know existed?

You don’t know.

So you either:

Ignore everything → ship anyway
Or block everything → break your team

Either way, you lose signal.

The real problem isn’t vulnerabilities — it’s decision-making

Most tools answer:

“What is wrong?”

They don’t answer:

“What should I do about it?”
“Can I prove that decision later?”

That last one is the real problem.

Enter: audit-ready

Instead of scores, it gives you decisions.

Deterministic. Reproducible. Auditable.

🔑 reasonCode replaces CVSS

Every dependency gets exactly one label:

DEV_DEPENDENCY_ONLY
OPTIONAL_DEPENDENCY
TRANSITIVE_NO_EXPLOIT
DIRECT_UNPATCHED
NO_KNOWN_VULNERABILITY
EXEMPTED

No interpretation required.

CI becomes trivial
npx audit-ready scan --fail-on DIRECT_UNPATCHED
Exit 0 → safe
Exit 1 → actionable issue

Not “7 high vulnerabilities.”
👉 A clear, enforceable rule

🧠 The constraint that shapes everything

Same package-lock.json → identical output. Always.

How that’s enforced

Core logic has hard constraints:

no Date
no Math.random()
no process.env
no I/O

And yes, it’s enforced by test:

const banned = ['Date', 'Date.now()', 'Math.random()', 'process.env'];
expect(found).toHaveLength(0);

If determinism breaks → build fails.

⚙️ The engine is intentionally simple
for (const rule of rules) {
if (rule.match(node)) {
return { ...node, reasonCode: rule.reasonCode }
}
}

No scoring. No heuristics.

👉 First match wins

Priority Rule
1 NO_KNOWN_VULNERABILITY
2 DEV_DEPENDENCY_ONLY
3 OPTIONAL_DEPENDENCY
4 TRANSITIVE_NO_EXPLOIT
5 DIRECT_UNPATCHED

Order = logic.

🧾 Output you can actually use
CycloneDX 1.5 SBOM
Human-readable report
SARIF (GitHub Security)

Everything tied to reasonCode.

🔐 Security: this tool audits itself

If you’re generating audit artifacts, your tool has to be trustworthy.

Here’s what that means in practice:

No environment access

The core engine literally cannot read:

environment variables
system time
runtime context

👉 Output depends only on input + tool version

Deterministic PURL generation

Standard encoders (encodeURIComponent, URL) can differ across Node versions.

So PURLs are built manually.

👉 Same package → same PURL → always

Schema validation (input + output)
.audit-policy.json → validated before scan
SBOM → validated before write

If validation fails:

👉 Nothing is written

Immutable output
All models are readonly
Everything is Object.freeze()d

👉 No silent mutation
👉 No post-processing surprises

Exceptions cannot live forever

Every exception requires:

a reason
an expiration date

Expired?

audit-ready audit-exceptions

👉 exit 1

No silent ignores.

Network safety by design

Only one external call:

👉 OSV API with PURLs

If it fails:

SBOM still generated
tool exits with code 2
no retries, no stale cache
Self-audit (this is rare)
audit-ready audit-self

The tool runs its own pipeline on itself.

Same code. Same rules.

👉 If it lies, it exposes itself.

⚠️ What this tool does NOT do
No AI explanations
No “smart” guessing
No monorepo support (yet)
No caching (Phase 3)
npm only

If a case isn’t covered:

👉 it fails loudly

Why this matters

This isn’t about better scanning.

It’s about:

reproducible decisions
CI you can trust
audit trails you can defend
🧪 Try it
npx audit-ready@beta scan --dry-run
🧭 Status
Phase 1: SBOM + triage ✅
Phase 2: policy + exceptions ✅
Phase 3: caching + performance 🚧

Production release planned after Phase 3.

💬 Looking for feedback
weird dependency graphs
incorrect classifications
CI edge cases

👉 https://github.com/neve7er/audit-ready

Final thought

Most tools try to be smart.

This one tries to be predictable.

Because in security:

predictability beats intelligence.

Top comments (0)