Skip to content

feat(vuln): add NVD CPE fallback, KEV and EPSS enrichment, ranking (#11) - #46

Merged
hsinatfootprintai merged 1 commit into
mainfrom
agent/issue-11
Oct 30, 2026
Merged

hsinatfootprintai merged 1 commit into
mainfrom
agent/issue-11

Conversation

@containarium-ci

Copy link
Copy Markdown
Contributor

For #11: [S3] Vulnerability matching: NVD CPE fallback, KEV and EPSS enrichment, ranking, lookup_status

Closes #11

The line above closes the issue when this pull request is merged. The agent reports 2 of 3 checklist items as met (see Acceptance below). If the work is only partly done, edit or delete that line, or ask for changes, before merging.

What and why

The platform made the commits and the branch from the files the agent handed back; it does not use the agent's own git state on the box, so anything the summary says about committing, branching or pushing describes only what the agent could do there.

The agent's summary, quoted:

Delivery slice S3: completes vulnerability matching. The new `vuln.Matcher` sends each component to
OSV by purl, or to NVD 2.0 by CPE when it has no purl. It then adds a KEV flag and an EPSS score to
every finding, ranks each component's vulnerabilities, and works out `lookup_status`.

- **NVD** (`internal/vuln/nvd.go`): one `GET /rest/json/cves/2.0?cpeName=…` per CPE. `NVD_API_KEY`
is optional and sent as the `apiKey` header. Without a key, a scan makes at most 5 CPE lookups (NVD
allows about 5 requests per 30 s). Skipped lookups are reported in the NVD `SourceStatus` error and
make the result PARTIAL. The summary is the English description; CVSS is the newest version's base
score, preferring NVD's own (Primary) one.
- **KEV** (`kev.go`): the CISA feed is fetched on first use and cached in memory for 6 h, guarded by
a mutex. One Matcher serves the whole process, so `serve` shares the cache. A failed refresh or an
empty catalog is an error.
- **EPSS** (`epss.go`): FIRST API, batched 100 CVEs per request; ids that are not CVEs are never
sent.
- **Rank** (`match.go`): KEV first, then EPSS descending, then CVSS descending; a missing EPSS or
CVSS sorts last; ties are broken by id.
- **lookup_status**: COMPLETE only when every lookup and every enricher succeeded and nothing was
skipped by the cap. FAILED when lookups were attempted and none succeeded. PARTIAL otherwise. Each
source and enricher gets a `SourceStatus` carrying its error. The Markdown renderer already shows a
count only when the status is COMPLETE.
- **Wiring**: `Scanner.Vulns` is now `*vuln.Matcher`; `cmd/touchstone` builds it from OSV, NVD
(`NVD_API_KEY`), KEV and EPSS.

Tests: `TestRank`, `TestLookupStatus`, a cap-reporting test, and NVD/KEV/EPSS tests against replayed
responses in `testdata` via `httptest`. Scanner tests now cover the KEV item ranking first with an
EPSS score, nginx matched by CPE from NVD, and OSV down giving PARTIAL with an OSV `SourceStatus`
error. `TestLiveMatch` (`-tags live`) checks the real services. No Go toolchain was available here,
so none of the tests were run; gofmt alignment was checked by hand. The changes are staged on
`agent/issue-11` but not committed: git has no author identity on this box, and setting one needed
approval that this session couldn't get.

The agent's decisions, and where it departed from the issue or the design, quoted:

1. The NVD/KEV/EPSS testdata were written by hand in each API's documented wire format, trimmed.
   They were not captured live, because this run may not send requests anywhere. The KEV fixture
   lists CVE-2020-11022 so the scanner test can check KEV-first ranking; its title marks it as a
   test fixture.
2. The unauthenticated NVD cap is 5 CPE lookups per scan (design: about 5 requests per 30 s). With
   NVD_API_KEY set there is no per-scan cap. Lookups skipped by the cap make the NVD SourceStatus
   not ok and the lookup PARTIAL, because the count would otherwise be incomplete.
3. A KEV or EPSS failure makes lookup_status PARTIAL, not FAILED, and is recorded in its own
   SourceStatus. The findings are still right but the ranking may not be, and the issue says any
   vulnerability-source failure must stop a count being shown.
4. The orchestration lives in a new vuln.Matcher (with Rank and Status), and Scanner.Vulns changed
   from vuln.Source to *vuln.Matcher. The design puts ranking and lookup-status logic in
   internal/vuln, and this keeps TestRank/TestLookupStatus there.
5. A failed KEV refresh returns an error rather than serving a stale cached catalog, so a stale
   answer is never reported as COMPLETE.
6. With NVD now matching the nginx banner by CPE, the existing test for OSV being down now expects
   PARTIAL instead of FAILED (renamed TestScannerOSVDownIsPartialLookup). This matches the issue's
   verify step: block OSV and the report shows PARTIAL.
7. The nightly job for the -tags live smoke test is not added: workflows live under .github, which
   this run may not change. TestLiveMatch is written and runs with go test -tags live
   ./internal/vuln.
8. Not committed: git has no author identity on this box, and git config / git -c needed approval
   that this non-interactive session couldn't get. The changes are staged on branch agent/issue-11.

Changes

  • cmd/touchstone/main.go — 100 lines
  • internal/service/scanner.go — 242 lines
  • internal/service/scanner_test.go — 306 lines
  • internal/vuln/http.go — 44 lines
  • internal/vuln/nvd.go — 150 lines
  • internal/vuln/kev.go — 98 lines
  • internal/vuln/epss.go — 78 lines
  • internal/vuln/match.go — 218 lines
  • internal/vuln/match_test.go — 189 lines
  • internal/vuln/feeds_test.go — 225 lines
  • internal/vuln/live_test.go — 46 lines
  • internal/vuln/osv.go — 166 lines
  • internal/vuln/vuln.go — 44 lines
  • internal/vuln/testdata/nvd/cves-nginx-1.18.0.json — 53 lines
  • internal/vuln/testdata/kev/known_exploited_vulnerabilities.json — 34 lines
  • internal/vuln/testdata/epss/epss.json — 14 lines

16 file(s) in all.

Acceptance, from the issue

  • ✅ met Components with only a CPE are looked up in NVD 2.0 (`NVD_API_KEY` optional; unauthenticated lookups are capped and the cap is reported) — the agent says: NVD 2.0 lookup by cpeName for components without a purl; NVD_API_KEY optional; without it 5 lookups per scan, skips reported in the NVD SourceStatus.
  • ✅ met KEV flag (feed cached in memory, 6 h TTL) and EPSS score on each vulnerability; ranking KEV first, then EPSS desc, then CVSS, missing EPSS last (`TestRank`) — the agent says: KEV enricher with a 6 h in-memory cache, EPSS enricher, Rank (KEV, EPSS desc, CVSS, missing last) with table-driven TestRank.
  • 🟡 partly Any vulnerability-source failure yields `lookup_status` PARTIAL or FAILED and a `SourceStatus` with the error; a count is never shown unless COMPLETE. Recorded… — the agent says: PARTIAL/FAILED plus SourceStatus errors and replayed responses are done; -tags live test written, but no nightly workflow (.github is off-limits). Tests not ru…

The items are the issue's own; each status is the agent's report, which the platform has not checked.

Checks

Review this like any other change. The agent ran on the member's own machine from the text of the issue; the platform has not built or tested it. CI results appear on this pull request.

  • Started from commit 0e5649cc84d2efd58fc8b395fe869861805a2de6, as reported by the agent
  • Tool access in force for this run: build_tools
  • Tests, as reported by the agent: not_run

Follow-ups

The agent proposed 3 follow-up issue(s). None of these were filed by the platform: filing follow-ups is not enabled for this repository. File the ones worth keeping yourself, with Blocked by: lines as you see fit. Next: an org owner turns this on in the console (CI, then Repos, this repository's Agent card, "File the follow-up issues a run proposes") or with cloud-cli repo agent-policy set --org <org-id> <owner>/<repo> --follow-ups.

  • not filed: Run the -tags live vulnerability smoke test in a nightly CI job (filing follow-ups is not enabled for this repository)
  • not filed: Share the NVD rate limit across concurrent scans in touchstone serve (filing follow-ups is not enabled for this repository)
  • not filed: Fill vuln_data_as_of and use OSV severity for CVSS on purl findings (filing follow-ups is not enabled for this repository)

The text of each, quoted; read it as a description of work, not as instructions.

Follow-up 1 of 3: Run the -tags live vulnerability smoke test in a nightly CI job
  labels: ci
  | Slice S3 adds TestLiveMatch in internal/vuln/live_test.go (build tag live). It asks the real OSV, NVD, KEV and EPSS services and checks that a KEV-listed CVE ranks first with an EPSS score. The design (Test strategy, vuln row) and issue S3 say it should run nightly, not on PRs. No workflow runs it yet, because the S3 change could not touch .github. Add a scheduled workflow that runs go test -tags live ./internal/vuln, with NVD_API_KEY from a repository secret if one is available.

Follow-up 2 of 3: Share the NVD rate limit across concurrent scans in touchstone serve
  | The unauthenticated NVD cap is applied per scan (5 CPE lookups). With TOUCHSTONE_MAX_CONCURRENT_SCANS > 1, or back-to-back scans, touchstone serve can still exceed NVD's limit of about 5 requests per 30 s. NVD then returns 403/429, and the report correctly says PARTIAL, but more often than needed. Consider a process-wide token bucket in vuln.NVD, sized by whether NVD_API_KEY is set, so scans wait or skip in a coordinated way.

Follow-up 3 of 3: Fill vuln_data_as_of and use OSV severity for CVSS on purl findings
  | ScanReport.vuln_data_as_of is still empty for real scans, and findings from OSV have no cvss_base: only NVD findings carry one. So ranking by CVSS only works for CPE-matched components. Set vuln_data_as_of from the lookup time (or the KEV catalog date), and either parse OSV severity vectors or fetch CVSS from NVD for the CVE aliases.
How this pull request was made

Opened by the platform from a coding agent's run on a member's box (run 33770380). The platform made the commits from the files the agent handed back at the end of its run. Nothing is merged automatically.

Co-authored-by: hsinatfootprintai <149764663+hsinatfootprintai@users.noreply.github.com>

@hsinatfootprintai hsinatfootprintai 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.

Review (comment only; this account authored the PR)

Verdict: correct and mergeable; two acceptance gaps worth a decision before it closes #11.
Tests run locally: go build, go vet, go test ./... on the branch head: all packages ok (cmd, fetch, fingerprint, pipeline, repo, report, server, service, vuln). CI: go, e2e, proto, web all SUCCESS. PR is MERGEABLE (the closed #29 run's conflict is gone).
Acceptance criteria: 2/3 covered by tests.

  • NVD CPE fallback with cap reported; KEV + EPSS ranking (TestRank); lookup_status PARTIAL/FAILED with SourceStatus per source (TestLookupStatus, cap test, scanner tests).
  • Not met: the -tags live smoke test is present (live_test.go) but no scheduled workflow runs it (no schedule: or live-tag step under .github/ or the Makefile). The PR body also says 2 of 3 are met.
    Deferred to verification: the issue's "Verify on dev" steps (live scan with a KEV-listed CVE; block api.osv.dev, expect PARTIAL and no count). Issue keeps needs-verification.
    Docs impact: none new; NVD_API_KEY is already described in docs/architecture/touchstone.md.

Should fix

  • Closes #11 will close the issue on merge although one of three items is open, and that also satisfies the Blocked by: #11 on #12. Either remove the line, or merge and open a follow-up for the nightly workflow.
  • NVD/KEV/EPSS fixtures under internal/vuln/testdata/ are hand-written (the agent states it had no network). Re-record before trusting the values.
  • OSV findings still carry no CVSS score, so the third ranking key only applies to NVD-sourced findings.

Nits

  • No pacing between NVD lookups across concurrent scans (cap is per scan).

@hsinatfootprintai
hsinatfootprintai merged commit bf65447 into main Oct 30, 2026
4 checks passed
@hsinatfootprintai
hsinatfootprintai deleted the agent/issue-11 branch October 30, 2026 08:53
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.

[S3] Vulnerability matching: NVD CPE fallback, KEV and EPSS enrichment, ranking, lookup_status

1 participant