Skip to content

Add fixed-version remediation UX: FIXED IN column and --format fixplan - #4

Merged
cwayne18 merged 4 commits into
mainfrom
cwayne18-fixed-version-ux
Aug 5, 2026
Merged

Add fixed-version remediation UX: FIXED IN column and --format fixplan#4
cwayne18 merged 4 commits into
mainfrom
cwayne18-fixed-version-ux

Conversation

@cwayne18

@cwayne18 cwayne18 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

What

Parse the fixed ranges vexscan already receives from OSV but never read, and turn them into the report's one actionable field: what to upgrade to.

Two things ship here, sharing the same OSV-parsing work:

1. FIXED IN column + remediation summary

  • Extract release-scoped fixed versions from each OSV record's affected ranges (a bookworm scan reports the bookworm fix, never the sid one), joined onto findings by source package.
  • New FIXED IN column that earns its place like EPSS/KEV — it appears only when a section has at least one published fix.
  • New summary line: N affected: N unique advisories, N fixable, N with no fix yet. The fixable clause is dropped entirely when nothing is fixable, so it never prints 0 fixable.
  • no fix is kept deliberately distinct from a blank cell: one is data (acknowledged, unpatched), the other is missing data.

2. --format fixplan

A remediation-first view that reorganizes affected findings by the action that clears them — one row per upgrade instead of one per advisory:

  138 of 292 affected findings have a fix.
  upgrading 28 packages clears 86 advisories; 154 with no fix yet.

UPGRADE (28) - apply these to clear the fixable findings
PACKAGE       CURRENT           FIXED IN            CLEARS  SEVERITY
libgnutls30   3.7.9-2           3.7.9-2+deb12u7     27      CRITICAL
libc6         2.36-9+deb12u1    2.36-9+deb12u14     24      HIGH
...
NO FIX YET (154) - affected, but no patch has shipped
...

A package with many advisories fixed across different point releases collapses to one row targeting the newest version, since distro point releases are cumulative. That needs version ordering, which the tool otherwise never does (OSV decides affectedness server-side), so it's scoped to ecosystems it can order confidently:

  • New internal/debver — a correct reimplementation of dpkg's verrevcmp (epochs, ~, numeric-vs-lexical runs), unit-tested against known orderings.
  • Debian/Ubuntu collapse to the newest fix; other ecosystems stay split by published fix rather than risk naming a wrong "newest" (a semver pre-release sorts opposite to a deb revision).
  • It's a view, not a filter: every un-fixable finding is still listed under NO FIX YET, so the plan can't read as complete when it isn't. Rows are sorted worst-first (KEV → severity → most cleared).

Verification

  • New tests for internal/debver, the fixed-version overlay, OSV parsing/scoping, the FIXED IN column, the remediation summary, and fixplan grouping/collapse/fallback.
  • Full suite, go vet, gofmt all clean.
  • Real runs: stale debian:bookworm-20230919 (28 clean upgrades, libc6 → deb12u14 clears 24) and current debian:12 (0 fixable, everything under NO FIX YET — correct).

Files

fixplan.go, internal/debver/ (new); internal/osv/osv.go, internal/analyze/analyze.go, internal/ecosystem/ecosystem.go, report.go, main.go, README.md (changed); plus tests.

Parse the fixed ranges vexscan already receives from OSV but never read,
and surface them as the report's one actionable field.

- Extract release-scoped fixed versions from each OSV record's affected
  ranges, joined onto findings by source package.
- Add a FIXED IN column (earns its place like EPSS/KEV) and a remediation
  summary line (N unique advisories, N fixable, N with no fix yet). "no fix"
  is kept distinct from a blank cell.
- Add --format fixplan: a remediation-first view that groups affected
  findings by the upgrade that clears them. Debian/Ubuntu packages collapse
  to the single newest fix (cumulative point releases) via a new
  internal/debver dpkg comparator; other ecosystems stay split rather than
  guess an order. Un-fixable findings are still listed under NO FIX YET.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds remediation-focused reporting by extracting OSV “fixed” version events and surfacing them as actionable upgrade targets in both the standard text report and a new --format fixplan output.

Changes:

  • Parse OSV affected ranges to extract release-scoped fixed versions and overlay them onto findings.
  • Extend the text report with an optional FIXED IN column plus a remediation summary line.
  • Introduce --format fixplan, including Debian/Ubuntu version collapsing via a new internal/debver comparator, with tests and README updates.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
report.go Adds shared empty-report messaging, remediation summary, and optional FIXED IN column/detail output.
report_test.go Adds coverage for FIXED IN column behavior and remediation summary output rules.
README.md Documents FIXED IN semantics and the new --format fixplan output.
main.go Wires --format fixplan into CLI validation and rendering.
internal/osv/osv.go Extracts fixed versions from OSV affected ranges with release scoping.
internal/osv/osv_test.go Tests fixed-version extraction, latest-event selection, and release scoping.
internal/ecosystem/ecosystem.go Extends the finding schema with fixed_version.
internal/analyze/analyze.go Adds resolver plumbing + overlay to populate FixedVersion on findings.
internal/analyze/fixed_test.go Tests overlay join logic (by package/component) and empty-map behavior.
internal/debver/debver.go Implements Debian version ordering (dpkg verrevcmp) for safe collapse-to-newest.
internal/debver/debver_test.go Validates ordering against known dpkg comparison outcomes.
fixplan.go Implements remediation-first “upgrade plan” rendering, grouping, collapsing, and sorting.
fixplan_test.go Tests collapse behavior, no-fix separation, and non-orderable ecosystem behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread report.go
cwayne18 and others added 3 commits August 5, 2026 15:13
The dpkg verrevcmp algorithm weights a digit as 0 in the non-digit
comparison run, so a digit sorts before any letter and after '~'. The
first cut of order() omitted the digit case, letting a digit fall through
to c+256 and sort after letters instead. This only triggers when two
versions' runs misalign (a digit on one side against a letter or
punctuation on the other), which real distro point-release strings avoid,
but it inverted those comparisons for a general dpkg comparator and could
pick a non-newest fix target. Add the digit case and regression tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The remediation line ("N affected: ...") repeats in the footer like every
other summary line, so the footer is now four summary lines rather than
three and the test's hard-coded last-3-lines window no longer caught the
ecosystem header. Assert the invariant directly instead: the section index
is the final line (nothing prints beneath the footer), and the summary
markers appear in the footer block.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@cwayne18
cwayne18 merged commit 2a8623e into main Aug 5, 2026
1 check passed
cwayne18 added a commit that referenced this pull request Aug 5, 2026
A review of the fixed-version work in #4 turned up one behavioural bug and
six places where the code and its own documentation had drifted apart.

An OSV record routinely carries more than one `fixed` event, because a vendor
maintaining several branches patches them all: GO-2022-0623 fixed Vault in
1.5.9, 1.6.5 and 1.7.2, and 60 of the 131 records returned for that module
read the same way. The last event won, on the reading that OSV lists them
ascending so the last is newest -- true, and irrelevant, because the newest of
several maintained branches is not the upgrade target for someone on the
oldest of them. A 1.5.4 install was told to move to 1.8.5 to close
CVE-2021-43998, when 1.7.6 closes it.

Advisory.Fixed becomes a list, Advisory.Ecosystem travels with it, and the
overlay picks the lowest fix that is actually an upgrade. Picking needs a
version order, so it is scoped to what this tool can order with confidence:
Debian and Ubuntu through internal/debver, Go and npm through semver. PyPI is
deliberately out, since PEP 440 sorts 1.0rc1 before 1.0 and semver sorts it
after; so are the RPM distros, since rpmvercmp is not verrevcmp. Everything
else keeps the old last-wins target, which overshoots rather than naming a
version that may not contain the fix -- and --details now names the branches
it did not choose either way, so the overshoot is visible.

The rest, all from the same review:

- Two doc comments lost their first line when the new code was spliced in
  above them. `go doc` was rendering writePriority's as "severity spread above
  it counts." and aliases's as "It exists for the VEX overlay."
- The fix plan borrowed renderText's footer, which repeats the main report's
  section index -- naming AFFECTED and RULED OUT under a document whose only
  headings are UPGRADE and NO FIX YET. It gets its own footer: its own summary,
  and the caveats, which are about the scan and not about the view.
- "clears 86 advisories; 154 with no fix yet" mixed units in one sentence. The
  86 are advisories and the 154 are findings; both now say so.
- The fix plan silently dropped findings a vendor VEX statement had answered
  and findings that were undetermined. A remediation view that omits rows is
  the one report whose shortness reads as good news, so they are counted.
- writeRemediation's doc comment claimed the fix clause is skipped when
  nothing is fixable. An autofix commit had already changed that, correctly --
  a fully-patched image is the case a reader most wants confirmed. The comment,
  the README and the test name now say what the code does.
- fixed_version had omitempty, which erased the difference between "no patch
  has shipped" and a scan run before the field existed. The text report goes
  to the trouble of printing "no fix" rather than a blank for exactly this
  reason.
- The --help example for --format fixplan used debian:12, which is fully
  patched and renders the degenerate case.

Verified against debian:12.0: 138 of 292 fixable, 28 upgrade rows, 86
advisories cleared -- unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants