Skip to content

feat: the version-aware scan, from the database to the dashboard (PR-01) - #64

Merged
maksimtech merged 8 commits into
mainfrom
feat/the-web-ui-shows-the-verdict
Oct 17, 2026
Merged

maksimtech merged 8 commits into
mainfrom
feat/the-web-ui-shows-the-verdict

Conversation

@maksimtech

@maksimtech maksimtech commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Audit finding PR-01 (critical), the rest of it after #58: the version-aware scan, from the database to the dashboard (docs/DESIGN.md §5). This PR replaces the stack #59 → #63, which is closed: one review, one merge, no conflicts from squash merges. The seven commits are kept so they can be read one at a time:

Commit What it does Was
f912b3a The verdict kept per product in cve_software with the version it was given for. The API takes version/cpe and judges with the CLI's function (affected.mark). status gets a Verdict column. #59
284d5de perf: the current-verdict check moved into the join; /api/watchlist in one query (CodSpeed on #59) #59
231c0e0 A CVE from KEV or MSRC alone is judged through NVD's record of it (nvd.records_for, cveId=, paced); a failure stays "not evaluable" and is said #60
e57a387 KEV facts and CVSS v4 exploit maturity stored. status and /api/cves ranked by priority.sql_order, built from the same constants; an index makes the full listing faster than the old date order (1.0 ms vs 1.45 ms) #61
3ab79ac fix: vendor/product_id are the vendor source's, as inventory.py meant. #58 had filled them with the CPE's fields, a misreading in my reconstruction of the design. No release carried it. #62
45ef35c patchradar import --accept "<name>" writes the products a person confirmed: only fully identified ones (Chrome, Edge), with version and vendor source. Anything else is refused with the survey's reason. #63
64de848 The web UI: installed version in the watchlist, version and CPE in the add form, Priority and Verdict columns, the not-affected CVEs counted and one click away, the split in the scan message #64

Tests

Strict TDD throughout. Each commit's tests were written first and seen failing, against recordings (NVD, KEV, the Notepad++ and 7-Zip payloads) or derived variants that say what they changed. Mutants killed per step:

Step Mutants killed
verdict stored 15/15
through NVD 9/9
priority 15/16 (the survivor is equivalent and explained in the commit)
vendor/product_id fix all
import all
web UI 10/10

Full suite on the merged result: 2239 passed; ruff and mypy clean.

For you to decide

CodSpeed showed the listings filtered by product ~10–15% slower than main. That is the cost of a join and four more fields per row, the price of reading the verdict. The full listing is faster than main once the priority index is in. The remainder is yours to acknowledge on CodSpeed.

Clean-up

feat/the-verdict-is-kept on the remote is the pre-rebase copy of the first commit and can be deleted. So can the branches of #59–#63 once this is merged.

🤖 Generated with Claude Code

maksimtech and others added 7 commits October 17, 2026 17:02
PR-01a judged each CVE against the installed version in memory; the next
`status`, /api/cves and the web UI went back to CVEs by name, and a scan
over HTTP did not judge at all, so the two scans disagreed.

- cve_software gains verdict, verdict_reason, verdict_fixed_version and
  judged_version, migrated into existing databases: per product, since
  one CVE can affect one watched product and not another. The latest
  scan decides it.
- get_cves shows a verdict only while the installed version is still the
  one it was given for: a stale "affected" sends someone to patch what
  is patched, a stale "not affected" is worse.
- affected.mark is the one place both scans judge.
- The API takes version and cpe on POST /api/watchlist/{name} with the
  CLI's checks, lists entries on GET /api/watchlist, and returns the
  split per product from POST /api/scan. `status` gets a Verdict column.

Tests first, seen failing; 15 of 15 mutants killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e watchlist

CodSpeed on #59: /api/watchlist 24% slower, get_cves and the /api/cves
listings 14-17%.

- /api/watchlist read the names and the entries in two queries; the names
  now come from the entries.
- get_cves picked the current verdict with a CASE per column; the version
  check is now part of the join, so a stale link simply does not match.
  Measured locally on the benchmark's 500 seeded CVEs: 1.68 ms -> 1.61 ms
  (main 1.45 ms). The rest is the join and four more fields per row, which
  is what reading the verdict costs: the same query with NULL columns was
  already 1.57 ms.

The full listing's stale-verdict case had no test of its own: added, and
it kills the mutant that drops the version check from that join.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
KEV and MSRC carry no cpeMatch, so for an entry with an installed version
a CVE that reached the scan only through them was "not evaluable" - and
KEV's are the ones being exploited.

- nvd.records_for asks NVD by id (cveId=), paced like every request to
  NVD through the pacer fetch_cves now shares.
- with_records_for_the_rest merges NVD's record into each such row,
  configurations and score with it; the CLI and the API call it for an
  entry they can judge, and only then.
- When NVD cannot be asked, the CVE stays not evaluable and the scan
  says so; the API lists it in `errors`.
- The NIS2 document's NVD row says the CVE ids that are sent.

Tests first, against the recorded cveId answer for CVE-2026-48092 and a
KEV entry derived in KEV's shape; seen failing; 9 of 9 mutants killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CISA's exploitation facts and the CVSS v4.0 exploit maturity travelled
in the scan's records and were not columns: the next `status` listed by
publication date, and an exploited CVE from 2023 sat under every new
5.3. test_priority_wiring held that boundary to say "now rank the
database paths too" the day the columns arrived.

- cves gains known_exploited, kev_ransomware, kev_date_added,
  kev_due_date and cvss_exploit_maturity, migrated in place and
  refreshed by every scan that brings them: CISA lists a CVE long after
  NVD publishes it. CISA's "Unknown" is stored as no.
- get_cves orders by priority.sql_order, built from the constants
  priority() decides with; an index on that order makes the full
  listing 1.0 ms on the benchmark's 500 CVEs, against 1.45 ms by date.
- /api/cves rows carry priority, priority_reason and the KEV fields;
  status has a Priority column and lists most urgent first.
- test_priority_wiring now holds the other side of the boundary.

Tests first, seen failing; the SQL order is checked against
sort_by_priority on rows covering every rank and edge. 15 of 16 mutants
killed; the survivor drops the "scored" branch, which COALESCE on the
score already orders the same way (equivalent).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…entory.py meant

inventory.py named the watchlist columns before they existed: vendor,
product_id, installed_version, channel, cpe. There a vendor source is a
feed that answers by version (Chrome's, Edge's, MSRC, Mozilla's) and
product_id is the product it needs; NVD by CPE is the other route. #58
filled vendor and product_id with the CPE's fields instead - a misreading
of the design it rebuilt - which would have had the inventory import
write one over the other. No release carried it.

- The CPE's vendor and product are read from `cpe` (WatchEntry.cpe_vendor,
  cpe_product, installed()), not stored.
- set_source records the vendor source, product and channel a person
  confirmed, for the import to call; it never adds a name.
- DESIGN.md section 5.1 says which column is which, and why.

Tests first, seen failing; the mutants that swap source and product or
drop the version from installed() are killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`import` proposes and a person confirms, and what gets remembered is the
confirmation (inventory.py). Until the watchlist had the columns the
confirmation had nowhere to go and the command wrote nothing.

- `--accept "<name>"`, once per product, any of its registry entries:
  a product the survey identified completely (Chrome, Edge) is watched
  with the registry's version and its vendor source in vendor and
  product_id (set_source).
- A source without its product, a component, a program nothing answers
  for: refused with the survey's own reason, exit 1; what was confirmed
  beside it is still written.
- Without --accept nothing is written, and the closing line says how to
  accept instead of naming columns this release no longer lacks.
- README, the NIS2 document and DESIGN 5.6 say what import writes.

Tests first, seen failing; the mutants for each refusal, the version,
the exit code and the lookup by any registry entry are killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CLI and the API learnt the installed version, the verdict on each
CVE and the stored priority; the dashboard still listed every CVE found
by name, ranked as if each were a job, and could not say what is
installed.

- The watchlist shows the version beside each name; the add form takes
  a version and a CPE, sent to POST /api/watchlist/{name} with the CLI's
  checks, and a refusal shows the server's detail (NVD's form of a CPE).
- The table gains Priority (its reason on hover) and Verdict columns;
  a product matched by name reads "not checked" rather than blank.
- The CVEs that do not affect the installed version are counted and
  hidden behind one button, as `scan` does without --all; with none
  hidden there is no button.
- A scan's message gives the split per judged product.

Tests first, the page's real script run in Node by the existing
harness; seen failing (8 of 9); 10 of 10 mutants killed. One test's
routes were reordered after the first run, as test_ui_errors.py orders
them: the harness takes the first matching prefix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@maksimtech maksimtech changed the title feat(ui): the dashboard shows what is installed and the verdict feat: the version-aware scan, from the database to the dashboard (PR-01) Oct 17, 2026
@maksimtech
maksimtech changed the base branch from feat/import-writes-what-a-person-confirms to main October 17, 2026 20:03
@maksimtech maksimtech closed this Oct 17, 2026
@maksimtech maksimtech reopened this Oct 17, 2026
Comment thread src/patchradar/db/database.py Fixed
Comment thread src/patchradar/db/database.py Fixed
@codecov

codecov Bot commented Oct 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.95288% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/patchradar/collectors/nvd.py 95.55% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 10.47%

⚠️ 11 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 5 regressed benchmarks
✅ 11 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_sanitize_cve_batch 8.6 ms 13 ms -33.61%
❌ ⚠️ test_api_cves_filtered 49.7 ms 73.1 ms -31.97%
❌ ⚠️ test_api_cves 33.6 ms 40.5 ms -16.94%
❌ test_print_cves_table 301.9 ms 355.1 ms -14.96%
❌ ⚠️ test_api_watchlist 5.9 ms 6.6 ms -10.36%
⚡ ⚠️ test_get_cves 11.4 ms 6.3 ms +80.06%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/the-web-ui-shows-the-verdict (44401b0) with main (2e2fdc8)

Open in CodSpeed

Bandit's B608 flagged three: the watchlist UPDATE whose column list was
joined at run time (#58, open on main as alert 61) and the two listings
of get_cves whose select list and ORDER BY were interpolated (#64,
alerts 64 and 65). Every interpolated piece was a module constant, so no
input reached them - but the project takes no `# nosec`, and a statement
written out leaves nothing to vouch for.

- set_installed / set_source run fixed UPDATE statements, one per field.
- get_cves' two listings and the priority index are written out; their
  ORDER BY is priority.sql_order() verbatim, and the new test holds the
  text to it, so the one-source rule stands without building from it.
  EXPLAIN still shows the listing scanning the priority index.

Test first and seen failing: no SELECT/INSERT/UPDATE/DELETE passed to
execute() may be an f-string or a concatenation. test_priority_wiring
now reads the written-out ORDER BY. Bandit B608: no issues.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@maksimtech
maksimtech merged commit 5cc8908 into main Oct 17, 2026
24 of 25 checks passed
@maksimtech
maksimtech deleted the feat/the-web-ui-shows-the-verdict branch October 17, 2026 20:22
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