The postcss That Would Not Die, and How CVE Lite Ended My Override Grind

A Next.js dependency footgun sent me down a year of hand-managing npm overrides. Here is how a lockfile scanner from the OWASP Incubator became the source of truth that fixed it in HexOps.

I am Aaron Lamb. I run Hexaxia, and at any given time I have a lot of projects running at once: client sites, internal tools, AI products, all in different states of build. Keeping that many repos healthy by hand is its own job.

So I built a tool to do it. HexOps is a local development dashboard. It manages multiple projects from one interface: start and stop dev servers, watch logs, check git status, run security scans, push to Vercel. It runs on my machine, not in a pipeline, because the work I care about happens before anything reaches a pipeline. I built it to take recurring maintenance off my plate, so I could glance at the health of thirty projects instead of hand-tending each one.

This is the story of one feature inside HexOps, and the multi-month grind that forced me to build it: dependency security scanning, and specifically the part where you have to fix a vulnerability buried three levels deep in a dependency you never chose.

The postcss That Would Not Die

It started with postcss.

If you run Next.js, you have a vulnerable copy of postcss in your tree right now, and there is a good chance you do not know it. Here is why. next declares "postcss": "8.4.31" in its own package.json, as an exact version, not a range. So your package manager installs postcss@8.4.31 nested under node_modules/next/, every time, no matter what. That version is below 8.5.10, which means it is flagged for GHSA-qx2v-qp2m-jg93. A moderate CVE, sitting in every Next project I own.

You cannot fix it by bumping a top-level dependency. The nested copy is pinned by its parent. No amount of npm update touches it. And npm audit fix --force has an opinion about how to resolve it: downgrade next to 9.3.3. That is not a fix, that is vandalism. Never accept it.

The only thing that actually works is a flat override. You add a "postcss" entry to overrides (npm), pnpm.overrides (pnpm), or resolutions (yarn), pinning it to a safe version. That forces the package manager to collapse every nested copy to the version you specified. One line, correctly placed, and the CVE is gone.

“Correctly placed” is where I lost a year of my life in small increments.

Managing Overrides by Hand Did Not Scale

The override field is package-manager-specific, and getting it wrong fails silently. npm reads overrides. pnpm reads pnpm.overrides. yarn reads resolutions. Put the right pin in the wrong field and nothing happens. No error. No warning. The pin you think is protecting you does absolutely nothing, and audit keeps passing because in some projects the scan was not even looking in the nested path.

I found this live on one of my own sites. It had overrides: { postcss: 8.5.15 } sitting in its package.json, looking responsible. The project is pnpm. pnpm never read it. The nested postcss@8.4.31 under next was still there, still vulnerable, the whole time. The override had been a no-op since the day it was written, because the repo started as an npm export and got switched to pnpm later, and the override field came along for the ride pointed at the wrong package manager.

That is the kind of bug that does not announce itself. You only catch it if you go looking, and you only go looking if something has taught you not to trust the green checkmark.

So I started building override management into HexOps. The first version was override-aware patching: detect the transitive vuln, write the flat override, reinstall. Simple in theory. In practice it grew its own backlog of bugs, and each one taught me another way this breaks:

  • Writing an override for a package that is also a direct dependency crashes npm with EOVERRIDE. You have to remove it from devDependencies first, then add it to overrides. Order matters.
  • Writing the override is not enough. You have to verify it actually landed in node_modules after the install, because sometimes it does not.
  • The patch would report success while the nested copy was still vulnerable. Post-patch audit verification was missing entirely.
  • Overrides go stale. Once next finally ships a patched postcss range, your pin is dead weight and you need to know to prune it. Except for postcss-under-Next specifically, which you should basically never prune, because Next keeps re-pinning it.

I was also, in parallel, doing a lot of this by hand across repos, sometimes with an AI assistant generating the override syntax for me. Both had the same fundamental flaw: no ground truth. The AI did not know which package manager the repo actually used. I did not always remember. Neither of us was reading the lockfile every time. We were both guessing at the safe version and hoping the pin took.

I had built a machine for applying overrides. What I did not have was anything trustworthy telling me which override to apply, and whether it had worked.

And here is the part that actually bothered me. I built HexOps to kill exactly this kind of work. The whole reason a dashboard watches every project at once is so recurring maintenance becomes something I glance at, not something I grind through. Instead I had rebuilt the grind inside the tool that was supposed to end it. Every new override bug was another afternoon of manual patching, or another round of pointing an agent at a repo and praying the diff was right. That is the precise toil HexOps exists to delete. A maintenance tool that needs this much hand-maintenance is not finished, and I knew it.

Finding CVE Lite

CVE Lite CLI is an OWASP Incubator Project maintained by Sonu Kapoor. It is a lockfile scanner. It is backed by the OSV database, it can run fully offline against a synced advisory database, and it distinguishes direct dependencies from transitive ones. The output is a list of findings with severity, CVE identifiers, and a validated safe version pulled from OSV.

I already had scanning covered with pnpm audit and Grype. What CVE Lite added was the analysis I had been doing by hand and getting wrong. It reads the lockfile, tells me whether a finding is direct or transitive, and validates the actual fixed version against OSV instead of leaving me to guess. For a direct dependency it hands you a copy-and-run upgrade command. For a transitive one it tells you the parent-update path: which parent to bump so the vulnerable child re-resolves. Their remediation strategy guide lays out exactly how it chooses between a direct upgrade, a parent bump, and an npm update, and the comparison docs are honest about where it sits next to npm audit, OSV-Scanner, Snyk, and Socket.

What it deliberately does not do is write your overrides. Its auto-fix mode applies direct upgrades only and stops short of touching transitive overrides, because doing that safely depends on your project. That is the right call for a general-purpose CLI. It is also exactly the seam where HexOps was already strong: I had the override machine. What I was missing was something trustworthy to drive it.

CVE Lite was that source of truth. Small, focused, local, honest about what it does. The repo is at github.com/OWASP/cve-lite-cli, and the documentation is worth reading on its own.

What sold me was not the feature list. It was that the project’s whole philosophy matched the reason HexOps exists in the first place. Its docs are blunt about it: “vulnerability scanning belongs at the developer’s terminal, not at the end of a pipeline,” and “detection without remediation creates work without resolution.” That is the same argument HexOps makes about everything it does. Run the check locally, on the machine where the work actually happens, and put the fix right next to the finding. A tool that already thought that way was never going to fight my dashboard. It was going to slot into it.

So I did not bolt it on as a third scanner and move on. I built the Security section of HexOps around it. The dedicated CVE Lite dashboard, the merged fleet view that folds pnpm-audit and Grype in beside it, the per-finding Apply pipeline, the SBOM and SARIF exports: that whole surface grew out of deciding to take CVE Lite seriously. It is what turned a couple of utilitarian scan sources into a real, first-class security section in the app.

Wiring It Into HexOps

HexOps runs every scanner through a common ScanSource interface. Each source implements scan(), returns a normalized array of findings, and feeds into a mergeFindings() pass that groups findings sharing an advisory ID (a CVE or GHSA), so the same vulnerability reported by two scanners collapses into one row with both sources credited. pnpm-audit and Grype were already there. Adding CVE Lite was a matter of wrapping its output in the same shape.

The wrapper calls the CLI with --json, parses the output, and maps each finding to the internal Finding type the other sources already use: severity, CVE ID, package, current version, validated fix version, and the direct-or-transitive relationship. The fields lined up well enough that the mapping was straightforward.

The two problems worth telling came after the mapping.

The first was the offline behavior, and it was the same false-all-clear ghost that had been haunting this whole project. CVE Lite queries the live OSV API by default. When that query fails, on a network timeout or in a restricted-egress environment, the CLI writes no output file. My first pass read that as zero findings. An empty result from a security scanner looks like a clean bill of health, and that is the worst possible failure mode, because you trust it. So I rewrote the runner: try online first for the freshest advisories, and if no output comes back, fall back to a scan against the synced local advisory database with --offline-db (their offline guide clocks a full sync of roughly 217,000 advisory records in under nine seconds, so keeping that database current is cheap). If neither attempt produces output, throw, so the caller surfaces a real error instead of silence. The presence of the output file, not the CLI exit code, is what tells success from failure, because CVE Lite also exits non-zero when it finds real vulnerabilities. That bug had been quietly masking a live qs CVE, CVE-2026-8723, the whole time it was in place.

The second was a regression that proved the loop works. A qs override I had shipped got silently dropped when a pull request was squash-merged, and main quietly regressed to the vulnerable version. I did not catch it by reading the diff. CVE Lite caught it on the next scan, flagged qs again, and I re-applied the fix, this time as a >=6.15.2 version floor rather than an exact pin. That is exactly the feedback loop I had been trying to build for a year: a scan I can trust to tell me when a fix has quietly come undone.

What It Solved

Once CVE Lite findings were flowing through the dashboard, the per-finding Apply button finally had something solid underneath it. Each Apply routes through the same patch pipeline:

  • Direct dependencies run pnpm add pkg@<fix-version> through the update route.
  • Transitive dependencies inject a flat pnpm.overrides or npm overrides entry, pinned to the validated safe version, in the field that matches the project’s actual package manager.

That last clause is the entire point. The thing I kept getting wrong by hand, the wrong-field no-op that left a site of mine exposed for months, is now mechanical. HexOps detects npm versus pnpm, writes the override into the correct field, and uses the version CVE Lite validated against OSV, not one I guessed. A rescan runs automatically after, so the green checkmark actually means something. The Apply and Fix-all actions are gated behind an environment flag, because automatically rewriting a dependency tree is not something a tool should do without you opting in.

The postcss problem that started all of this is now a single click, applied correctly, verified by a rescan. So is every transitive vuln behind it. The grind is gone, and the tool finally does what I built it to do, which is hand me back the afternoons I was spending on this.

Stale Overrides Are Their Own Vulnerability

There is a second-order risk here that took me too long to take seriously. An override is not a fix you set and forget. It is a standing instruction to pin a package to a version, and that instruction outlives the reason you wrote it.

Pin a package to an exact version to clear a CVE and you have frozen it. When that frozen version later picks up its own advisory, the override is doing the opposite of its job: holding you on a vulnerable version and overriding the resolver’s ability to climb to a safe one. The line you added to fix a vulnerability has become the vulnerability. Across thirty repos and a couple of years, that is a pile of pins, each one a quiet bet that the version you froze is still safe, none of them re-examined.

Nested copies make it worse, because the dangerous version is not even in your package.json. It is three levels down, held there by a parent or by your own stale pin, where reading the manifest will never show it to you.

You cannot audit this by eye. A stale override looks exactly as responsible as a live one. The only way to catch it is to scan the resolved tree against current advisories, repeatedly, and treat a pinned-but-now-vulnerable package as a finding like any other.

That is the part CVE Lite quietly fixed for me. It reads the resolved versions, not the manifest’s good intentions, and checks them against OSV on every scan, so an override that has gone stale-vulnerable surfaces the day its advisory does. And because it hands me a validated safe version, HexOps writes a floor (>= the lowest known-good version) rather than an exact pin wherever it can, leaving the resolver free to climb on its own. An override stops being write-once and becomes something re-checked every scan. That is the difference between a fix and a fix I can still trust six months later.

If You Run Next.js

You do not need HexOps to fix the specific bug that started this. If you run Next.js, go add a postcss override right now, pinned to a current version, in the field your package manager actually reads: overrides for npm, pnpm.overrides for pnpm, resolutions for yarn. Then run your audit again and confirm the count actually dropped. The pin is worthless if it is in the wrong field, and nothing will warn you. That one check is the whole lesson of this post, compressed.

Where HexOps Is

HexOps is open source, and I cut its releases in public. The security stack landed in v0.20.0 and the current release is v0.20.1: pnpm-audit, Grype, and CVE Lite running together, their findings merged into one deduplicated view, presented as a merged fleet dashboard plus a dedicated CVE Lite dashboard that labels each finding direct or transitive, with severity filters, SBOM and SARIF export, and the per-finding Apply action. CVE Lite is the newest piece and still early access, so its scan options and fix workflows are still moving. I use the whole thing daily across every Hexaxia project.

One note for anyone who wanders into the HexOps repo from this post: I wear a lot of hats across a lot of companies, and HexOps is honestly a work in progress. What you are looking at is a tool I build for my own daily use and keep building on a regular basis as I need more from it. Treat it as a living project, because that is exactly what it is.

If you write JavaScript or TypeScript for a living, I recommend CVE Lite without reservation. Install it, point it at your project, and read what it tells you. Their own line says it best: “Most tools tell you what’s wrong. CVE Lite CLI tells you what to run.” In a landscape where dependency risk is mostly transitive and mostly invisible, it is the tool I want every developer I work with to be running. Adopt it, and keep it in your workflow going forward.

Thanks to Sonu Kapoor and the OWASP community for building CVE Lite in the open.

Aaron Lamb
Co-Founder, Hexaxia Technologies