Skip to content

fix(statusline): overlay ruflo's fabricated CVE counter with the real scan - #26

Merged
pacphi merged 1 commit into
mainfrom
fix/statusline-fabricated-cve-counter
Jul 16, 2026
Merged

pacphi merged 1 commit into
mainfrom
fix/statusline-fabricated-cve-counter

Conversation

@pacphi

@pacphi pacphi commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Why

The statusline told this repo — and every clean repo — that it had 3 CVEs:

Swarm ● 1/15 · Hooks 29/29 · 🧠 33% · 💾 20MB · 🛡 scan pending · ⚠ 3 CVEs
⚠ 3 CVEs pending — Run ruflo security scan --depth full

Meanwhile pnpm audit reported 0 vulnerabilities across 176 deps and ruflo security scan --depth full reported Critical: 0 High: 0 Medium: 0 Low: 0. Both auditors disagreed with the statusline.

It's an upstream defect, not ours. getSecurityStatus() in @claude-flow/cli/dist/src/funnel/local-signals.js (3.32.0, latest):

let cvesFixed = 0;
const totalCves = 3;                              // hardcoded
const scans = fs.readdirSync(scanResultsPath).filter(f => f.endsWith('.json'));
cvesFixed = Math.min(totalCves, scans.length);    // counts FILES, not findings
  • totalCves = 3 describes ruflo's own v3 roadmap — per their v3-security-architect.md, CVE-1/2/3 are an outdated @anthropic-ai/claude-code dep, SHA-256 hashing in their api/auth-service.ts:580, and hardcoded creds in their api/auth-service.ts:602. Not public CVE IDs; nothing to do with the rendered project.
  • cvesFixed counts .json files. A scan reporting "findings": [] counts the same as one reporting a critical RCE.

So the alarm clears itself: ⚠ 3 CVEs → "run the scan" → the scan writes a file → the counter drops. Verified — two cp commands take a never-meaningfully-scanned directory to CLEAN:

$ ruflo hooks statusline --json | jq .security
{ "status": "PENDING", "cvesFixed": 0, "totalCves": 3 }     # pristine dir
$ ruflo security scan --depth full                          # "No security issues found!"
{ "status": "IN_PROGRESS", "cvesFixed": 1, "totalCves": 3 }  # a "CVE" was "fixed"
$ for f in a b; do cp .claude/security-scans/*.json .claude/security-scans/$f.json; done
{ "status": "CLEAN", "cvesFixed": 3, "totalCves": 3 }        # CLEAN by copying files

Reported upstream: ruvnet/ruflo#2694. Latest is 3.32.0 and unfixed, so we patch until it lands.

Confirmed our own customizations are not implicated: the defect reproduces on a pristine statusline.cjs with zero kit fingerprints, fixStatusline never touches security/cvesFixed/totalCves, and the kit never writes to ruflo's CLI internals.

What

Overlay the counter with the real scan. totalCves/cvesFixed are pinned to 0 so the ⚠ N CVEs branch can never fire; real state rides in status, which ruflo's renderer prints verbatim:

scan state renders
never scanned 🛡 scan pending (honest unknown, not a false green)
clean, fresh 🛡 ✓
clean, >7d old 🛡 scan stale
N real findings 🛡 n issues (red)

Two implementation notes worth a reviewer's attention:

  1. Wraps getStatuslineData, not applyLocalOverlays. The obvious fix — completing applyLocalOverlays, which already re-derives adrs/tests/hooks but skips security — silently does nothing. The fresh-cache early return (if (cache.fresh && cache.promoFresh) return overlayMemoPromo(cache.data)) bypasses it, so during the 60s TTL the patched function is never called. Wrapping the single entry point covers all four return paths.
  2. The count reaches the screen twice. funnel/insights.js:45 re-derives pending = totalCves - cvesFixed CLI-side and ships a finished sentence, so line 3 still read ⚠ 1 CVE pending with security already corrected. rufloHonestInsight rebuilds that one line from the real scan, matching on text because promo.js drops the insight id. Non-CVE insights pass through by identity, so the funnel rotation is untouched.

Self-retiring. Gated on positively detecting the defect in the installed CLI rather than a pinned version — same approach as improvement-eval's --cli-check (#2222). When upstream fixes getSecurityStatus, the next sync strips the stopgap. There's a test for exactly that.

Also here

status: detect statusline CONTENT drift, not just marker presence. Drift is now "would a sync change this file?" — which fixStatusline's dry run already answers. The marker test reported ok on a stale injected block, and since sync builds its plan from rows carrying a fix, re-injection never ran. This bit during development: an updated overlay silently failed to land. Beyond this PR, it means any kit upgrade revising the footer would not have re-injected it.

statusline: aidefence segment is now alarm-only. It was a permanent green 🛡 aidefence on — the only pure binary badge in the footer (SONA/QE counts move; a constant "on" says nothing after the first glance) — and its shield collided with ruflo's line-2 scan shield. They are different concerns: security scan audits your source; security defend/AIMDS screens prompts for injection, jailbreak, PII. Per issue #8's rule (already law for the proof segment: "no static green badge"), the healthy state is now silent and only the failure surfaces, with no shield glyph in the alarm.

Kept rather than deleted because it's load-bearing: @claude-flow/aidefence is still not a declared dependency of ruflo or @claude-flow/cli on 3.32.0 while security defend imports it (ruvnet/ruflo#2670) — it exists only because healAidefence installs it. A plain npm i -g ruflo can silently remove injection defense.

The probe is three-state, not boolean. Inverting a signal inverts its failure mode: a probe miss (custom npm prefix, statusline running under a different node) would have rendered "your defense is off" when it isn't — the same fabricated-alarm crime this PR fixes. "off" requires positive evidence; anything unverifiable is "unknown" and stays silent.

Verification

pnpm run check → typecheck ✓  lint ✓  markdownlint ✓  build ✓
82 kit tests, 43 statusline tests, 0 failures

Before → after on this repo (clean scans on disk):

- Swarm ◉ 1/15 · Hooks 29/29 · 🧠 33% · 💾 20MB · 🛡 scan pending · ⚠ 3 CVEs
- ⚠ 3 CVEs pending — Run ruflo security scan --depth full
- 🛡 aidefence on
+ Swarm ◉ 1/15 · Hooks 21/21 · 🧠 29% · 💾 20MB · 🛡 ✓

New coverage: overlay states (pending/clean/stale/N-issues), CVE counters pinned at 0 in every state, newest-scan-wins, malformed-JSON tolerance, insight rebuild + non-CVE pass-through, aidefence three-state polarity end-to-end, injection idempotency, and self-retirement on upstream fix.

Nothing in .claude/ is committed — it's generated and gitignored.

🤖 Generated with Claude Code

… scan

The statusline told every clean repo it had 3 CVEs. Upstream's
getSecurityStatus (@claude-flow/cli funnel/local-signals.js) hardcodes
`const totalCves = 3` — ruflo's OWN v3 roadmap items (CVE-1/2/3 in their
v3-security-architect.md: an outdated dep + SHA-256 hashing + hardcoded
creds in THEIR api/auth-service.ts), not the rendered project's risk — and
derives cvesFixed from scans.length, a FILE count. The alarm therefore
cleared itself: run the suggested scan, it writes a JSON file, the counter
drops. Three files reach CLEAN with nothing scanned, let alone fixed, while
a real finding never registers. Reported upstream: ruvnet/ruflo#2694.

The overlay reports what the newest scan actually found and never invents a
CVE: totalCves/cvesFixed are pinned to 0 so the "N CVEs" branch cannot fire,
and real state rides in `status`, which ruflo's renderer prints verbatim —
PENDING when never scanned (an honest unknown, not a false green), "N
ISSUES" when the scan found things, CLEAN, or STALE past 7 days.

Wired by wrapping getStatuslineData rather than patching applyLocalOverlays:
the fresh-cache early return (`if (cache.fresh && cache.promoFresh) return
overlayMemoPromo(cache.data)`) bypasses applyLocalOverlays entirely, so a
patch there is never called during the 60s TTL and the fabricated count
renders anyway. Wrapping the one entry point covers all four return paths.
The count also reaches the screen a second way — funnel/insights.js computes
`pending = totalCves - cvesFixed` CLI-side and ships a finished sentence —
so rufloHonestInsight rebuilds that one line from the real scan, matching on
text because promo.js discards the insight id.

Gated on positively detecting the defect in the installed CLI rather than a
pinned version, mirroring improvement-eval's --cli-check stopgap: the patch
retires itself on the first sync after upstream fixes this. Covered by a
test.

Also in this change:

- status: detect statusline CONTENT drift, not just marker presence. Drift
  is now "would a sync change this file?", which fixStatusline's dry run
  already answers. A marker test reported 'ok' on a stale injected block, and
  since sync builds its plan from rows carrying a `fix`, re-injection never
  ran. This bit us live: an updated overlay silently failed to land. It also
  means any kit upgrade revising the footer would not have re-injected.

- statusline: make the aidefence segment alarm-only. It was a permanent green
  "aidefence on" — the one pure binary badge in the footer (SONA/QE counts
  move; a constant "on" says nothing after the first glance) — and its shield
  glyph collided with ruflo's line-2 scan shield, a different concern
  entirely: `security scan` audits your source, `security defend`/AIMDS
  screens prompts for injection, jailbreak and PII. Per issue #8's rule,
  already law for the proof segment, the expected state is now silent and
  only the failure surfaces, with no shield glyph in the alarm.

  Kept rather than deleted because it is load-bearing: @claude-flow/aidefence
  is still not a declared dependency of ruflo or @claude-flow/cli on 3.32.0
  while `security defend` imports it (ruvnet/ruflo#2670), so it is present
  only because healAidefence installs it. A plain `npm i -g ruflo` can
  silently remove injection defense.

  The probe is three-state, not boolean: inverting a signal inverts its
  failure mode, and a probe miss (custom npm prefix, statusline running under
  a different node) would otherwise fail loud and WRONG. "off" requires
  positive evidence — a located ruflo install lacking aidefence; anything
  unverifiable stays "unknown" and silent.
@pacphi
pacphi merged commit 3fb26e8 into main Jul 16, 2026
11 checks passed
@pacphi
pacphi deleted the fix/statusline-fabricated-cve-counter branch July 16, 2026 16:34
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