Skip to content

feat(security): cve-lite 1.28 + override hygiene scanning (0.21.0) - #133

Merged
alamb-hex merged 16 commits into
mainfrom
feat/cve-lite-1.28-integration
Aug 10, 2026
Merged

feat(security): cve-lite 1.28 + override hygiene scanning (0.21.0)#133
alamb-hex merged 16 commits into
mainfrom
feat/cve-lite-1.28-integration

Conversation

@alamb-hex

Copy link
Copy Markdown
Collaborator

Upgrades cve-lite-cli 1.24.0 → 1.28.0, absorbs two correctness changes in data HexOps already rendered, and integrates cve-lite's override-hygiene audit (OA001–OA009) as a first-class surface.

Stacked on #132. Base is chore/patch-level-updates-2026-07-03 so this diff shows only the cve-lite work. GitHub will retarget to main automatically once #132 merges — provided #132 is merged (not squashed).

Why this isn't just a version bump

Two things in 1.28 changed data already on screen:

  • recommendedParentUpgrade.confidence was renamed from exact-direct-child/best-effort to verified/unverified. The badge colour map still keyed on the old strings, so after the bump every confidence badge silently degraded to gray. 1.28 also now only recommends a parent upgrade it has proven resolves the vulnerable package — unverified suggestions are marked, and are now rendered distinctly instead of as equally trustworthy advice.
  • Partial scans became detectable. 1.28 reports unresolved advisories per finding and skipped dependencies at the top level. Previously a scan that couldn't check everything rendered identically to a clean one.

What's new

  • OverrideHygieneSource — a fifth ScanSource running cve-lite <path> overrides --json, surfacing override-configuration defects as config findings on the fleet view.
  • Override hygiene panel on the project security view: rule id, severity, package, file > jsonPath location, runnable fix command.
  • GET /api/security/overrides/[id] (cached, ?force) and POST .../fix, the latter gated by a new OVERRIDE_HYGIENE_FIX_ENABLED flag that ships disabled, is enforced server-side with a 409 before any project lookup, and wraps the install in the dev-server guard (Guard patch apply when the target project's dev server is running (Turbopack node_modules churn) #109).
  • Partial-scan reportingscanCompleteness() drives a warning banner naming what couldn't be checked, plus a degraded source state on the fleet cards.
  • ScanSource.scan widened to { findings, warning? }, wiring the previously declared-but-dead SourceResult.warning end to end.

Deliberate decisions worth reviewing

  • PD001/PD002 are filtered out. cve-lite's phantom-dep rules duplicate DependencyHealthSource (Scanner: detect phantom dependencies (source imports of undeclared/override-only packages) #125). They also false-positive on monorepo roots: on ptolemy cve-lite reports 11 phantom deps that are declared directly in apps/web/package.json, while DependencyHealthSource correctly reports 0. Without the filter, /security would show 11 fabricated findings on that project alone.
  • The jsonPath is folded into the finding path. config findings dedup on config:<path>|<title>, and two OA009 findings on this repo carry byte-identical message text (ip-address, ws) — keying on file + message alone silently merged them into one.
  • json-cache.ts is now the shared cache, with cve-lite-cache.ts a thin wrapper preserving its API and on-disk filenames. Cache entries record the resolved cve-lite version, so a bump invalidates immediately rather than aging out — including on the stale-fallback path, which could otherwise resurrect a pre-bump report during a scan failure.

Fleet impact

Audited all 29 lockfile-bearing registered projects. 26 raw findings, 18 of them PD001/PD002 (filtered) → 5 visible findings across 3 projects; 17 projects clean. No flood.

The rules found real problems: cordero-group had a stale postcss override in the wrong package-manager section (OA008 critical + OA006/OA003 high), since fixed; hexaxia-media has two dead overrides (OA001 high).

Verification

292 tests / 43 files, tsc --noEmit clean, pnpm build exit 0. Every task was independently reviewed; a whole-branch review returned four findings living in the seams between tasks — a banner fed a filtered report, the read endpoint returning unfiltered findings, a cache test that passed without exercising its target, and a missing type guard before a regex on a shell-bound value — all fixed and re-reviewed.

Known gap: the flag-enabled fix path has no automated coverage. That is a hard gate before OVERRIDE_HYGIENE_FIX_ENABLED is ever flipped; read-only use is fully built and tested.

Records the resolved cve-lite version in each cache entry and treats a
mismatch as a miss, so a bump invalidates stale reports immediately rather
than letting them age out over the 1h TTL. Also closes the ignoreTtl
stale-fallback path, which could otherwise resurrect a 1.24 report when a
1.28 scan fails.

Adds recorded 1.28 scan + overrides fixtures for parser tests.
1.28 (#899) replaced the 'exact-direct-child' | 'best-effort' confidence
values with 'verified' | 'unverified' and now only recommends a parent
upgrade it has proven resolves the vulnerable package. The findings badge
still keyed on the old strings, so every confidence badge degraded to the
gray fallback after the bump.

Adds FindingRow.parentVerified so unverified suggestions can be told apart
from proven ones instead of being presented as equally trustworthy.
Adds scanCompleteness(), which tells a clean scan apart from one that could
not check everything. 1.28 surfaces unresolvedAdvisoryIds per finding and
skippedDependencies at the top level; both are aggregated here.

skippedDependencies is counted, never destructured — the array was empty in
the recorded capture so its element shape is unverified.
SourceResult.warning was declared but never set or rendered. Widens
ScanSource.scan to return { findings, warning } so a source can report that
it succeeded without covering everything, wires it through the runner, and
renders it on the source card.

Without this a degraded scan is indistinguishable from an all-clear.
A scan that could not resolve some advisories or skipped dependencies now
surfaces a partial-scan warning on the source card instead of rendering as a
green all-clear on the fleet view.
Wraps 'cve-lite <path> overrides --json' (OA001-OA009). Like the main scan,
the subcommand writes a timestamped file into cwd rather than stdout, so the
runner executes in a temp dir and reads the file back; the file's presence,
not the exit code, signals success.

Adds a namespaced JSON cache shared with future callers rather than copying
cve-lite-cache.ts.
Surfaces cve-lite's override-hygiene audit (OA001-OA009) as config findings
on the fleet security view, alongside the existing three-source stack.

PD001/PD002 are filtered out so DependencyHealthSource remains the single
phantom-dep authority; config findings dedup on title+path, so letting both
report would double-count every phantom dep.

The jsonPath is folded into the finding path because two findings of the
same rule on different override entries carry identical message text and
would otherwise collapse into one.
GET /api/security/overrides/:id returns the cached override audit (?force
bypasses) as both raw cve-lite findings and HexOps Finding rows.
POST /api/security/overrides/:id/fix runs 'cve-lite overrides --fix',
optionally scoped to a single OA rule, behind a new default-off
OVERRIDE_HYGIENE_FIX_ENABLED kill switch enforced server-side so a stale tab
cannot bypass it (#96/#97).

The fix runs an install, so it is wrapped in runWithDevServerGuard (#109) to
stop HexOps patching itself out from under its own dev server.
Renders OA rule findings with rule id, severity, package, file > jsonPath
location, and the runnable fix command. The Fix all control appears only when
OVERRIDE_HYGIENE_FIX_ENABLED is on, and is confirm-gated; the server enforces
the switch independently.
Names the advisories that could not be resolved and the dependencies that
were skipped, so a partial scan is not mistaken for an all-clear on the
project view. Complements the degraded source state on the fleet cards.
…ompleteness

Four Important findings from the final branch review, each a real correctness
bug that only showed up once tasks were viewed together:

- CompletenessBanner was fed the "imported only" FILTERED report. Toggling
  that filter could drop the one finding carrying an unresolved advisory id,
  making a provably partial scan render as clean — the exact false all-clear
  the banner exists to prevent. Now fed the unfiltered report; completeness
  is a property of the scan, not the view.

- GET /api/security/overrides/[id] returned report.findings unfiltered while
  parseOverrideAuditJson (used for `rows`) excludes PD001/PD002 so
  DependencyHealthSource stays the single phantom-dep authority. The panel
  renders `findings` directly, so a real phantom dep showed up twice.
  EXCLUDED_RULES is now exported and applied to both arrays.

- The cve-lite-cache TTL-expiry test wrote an entry with no toolVersion, so
  the version gate returned null before the TTL comparison ever ran —
  verified by temporarily deleting the TTL branch and confirming only that
  test failed (see final-fix-report.md for the exact command output). Fixed
  by giving the entry a matching toolVersion.

- POST /api/security/overrides/[id]/fix ran RULE_RE.test(body.rule) without
  a typeof guard first; a non-string body.rule like ["OA009"] coerces via
  toString() and passes the anchored regex, reaching JSON.stringify as an
  array. Not currently exploitable (the regex confines reachable bytes to
  inert shell characters) but the safety proof depends entirely on the regex
  staying anchored/alternation-free with no test to catch a future
  regression. Added the typeof guard plus a second test file
  (route.rule-validation.test.ts) that mocks OVERRIDE_HYGIENE_FIX_ENABLED
  true in an isolated module registry, since the existing test file mocks it
  false and 409s before validation is ever reached. The shipped default in
  auto-apply-flag.ts stays false.

Full suite: 272 passed (42 files), up from the 265/41 baseline.
Two Minor findings in parseOverrideAuditJson:

- The dedup key path fell back to bare `file` when jsonPath was absent, with
  no package component. Two same-rule findings on different packages that
  share message text and omit jsonPath would collide on the merger's
  `config:<path>|<title>` key and silently lose one. Falls back to
  `${file}#${packageName}` instead of bare `file` now, keeping the existing
  jsonPath-present path unchanged (the fixture's dedup test still passes
  as-is).

- A finding emitted without a ruleId defaulted to 'OA000', which isn't in
  EXCLUDED_RULES, so an unlabelled phantom-dep finding could slip past the
  PD001/PD002 filter and duplicate DependencyHealthSource. Now dropped
  outright when ruleId is missing rather than guessed into an OA-shaped
  default — no new rule taxonomy invented.

(F5's devServerGuard fix and F8's reworded 409 message shipped in the prior
commit, since both live in overrides/[id]/fix/route.ts alongside F4.)

Full suite: 272 passed (42 files).
…he a thin wrapper

cve-lite-cache.ts and json-cache.ts were near-duplicate implementations
instead of the extracted-and-shared module the design called for
(§4.1). json-cache.ts is now the single source of truth: it gains an
ignoreTtl option, a closed/self-validating namespace registry to make
namespace-separator filename collisions structurally impossible, and a
fix for corrupt cachedAt values being treated as fresh (NaN age).
cve-lite-cache.ts is now a thin typed wrapper that preserves its public
API and on-disk filename/format exactly, including reading entries
written under the pre-refactor `report` field name.
The pairwise collision check ran on every import over a fixed two-element
registry already guarded by TypeScript's closed JsonCacheNamespace union
and the runtime assertRegisteredNamespace check. This redundant check
converted any helper-function bug into an app-wide import-time crash
instead of a caught test failure.

Unit tests for _namespacesCollide (in json-cache.test.ts) provide the
proper CI-gated safety for any future namespace addition — that is the
right enforcement point. Module-load execution removed; the helper and
all its tests remain.
Covers the override-hygiene integration and the next/postcss security fixes
that ride with it.

Also gitignores .playwright-mcp/, which browser-driven verification runs leave
in the working tree.
Base automatically changed from chore/patch-level-updates-2026-07-03 to main August 10, 2026 22:23
@alamb-hex
alamb-hex merged commit 82ada43 into main Aug 10, 2026
1 check passed
@alamb-hex
alamb-hex deleted the feat/cve-lite-1.28-integration branch August 10, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant