Skip to content

feat!(detector): route NVD CPE detection to vuls2, keep go-cve-dictionary for non-NVD sources#2575

Draft
shino wants to merge 100 commits into
masterfrom
shino/cpe-detect-nvd
Draft

feat!(detector): route NVD CPE detection to vuls2, keep go-cve-dictionary for non-NVD sources#2575
shino wants to merge 100 commits into
masterfrom
shino/cpe-detect-nvd

Conversation

@shino

@shino shino commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What

Route NVD-based CPE detection through the vuls2 library while keeping go-cve-dictionary for the sources vuls2 does not cover (JVN, Cisco, Paloalto, Fortinet, …). Companion work to MaineK00n/vuls-data-update#827/#836/#841 and MaineK00n/vuls2#382/#384, which this PR pins via go.mod.

Why

The go-cve-dictionary CPE path matches NVD data by version-range comparison only, which cannot decide membership for non-semver version formats (cisco ios 15.1(4)m3, juniper junos 21.4r3, …) and suffers from a go-cpe numeric-prefix over-match (5.15.10 matching a 5.15.103 query). The vuls2 DB carries the NVD feed with cpematch-expanded criteria, fixing both — but it has no JVN/Cisco/Paloalto/Fortinet CPE data, so dropping go-cve-dictionary entirely would lose those sources.

Division of labour after this PR:

Source Path
NVD vuls2 (cpe.Detect, cpematch-expanded criteria)
JVN / Cisco / Paloalto / Fortinet / Vulncheck go-cve-dictionary (inside DetectCpeURIsCves, NVD contribution stripped; EUVD / MITRE contents ride along as enrichment but are never a detection basis)

How

detector API — master-shaped, library-consumer safe

External consumers drive detection as a library through the master-era pair DetectPkgCves()DetectCpeURIsCves(cpes []Cpe). Both entry points keep their master names and calling convention, each with a complete, order-independent responsibility:

  • DetectPkgCves(r, vuls2Conf, noProgress) — unchanged signature. OS-package / Microsoft-KB detection via vuls2.DetectPkgs (family-gated) plus the FixState / ListenPortStats post-processing. Server mode calls this alone, preserving its no-CPE behaviour.
  • DetectCpeURIsCves(r, cpes, cveCnf, logOpts, vuls2Conf, noProgress) — the full CPE pipeline; two parameters added so the change is visible at compile time rather than silently dropping NVD results. Internally: go-cve-dictionary first (non-NVD sources only — each detection's Nvds is nil'ed before use; NVD-only detections disappear and vuls2 re-detects them; JVNDB advisories stay gated on the pre-strip NVD presence, matching classic behaviour), then vuls2.DetectCPEs for NVD over every entry. The Cpe.UseJVN flag passes through to the dictionary lookup unchanged: UseJVN=false excludes JVN only — NVD / Vulncheck / vendor sources still apply, exactly as master. The flag is part of the library API and is set per CPE by the caller (the report flow sets it false for the synthesised Apple CPEs; external consumers set it on their own terms).

detector/vuls2 library layer

  • vuls2.DetectPkgs (OS packages / KB; renamed from Detect to mirror) and vuls2.DetectCPEs (CPE URIs) stay separate entry points by design — when CPE-capable sources beyond NVD (JVN, Vulncheck, …) move into the vuls2 DB, per-source confidence aggregation can live entirely inside the CPE path. DetectCPEs suppresses the OS-package / Microsoft-KB inputs so the two entry points cannot double-detect; the ScannedCves merge dedups CveContents by SourceLink and merges CpeURIs/Exploits/Mitigations. preConvert forwards the CPE list (converted to CPE 2.3 FS) on scanTypes.ScanResult.CPE; walkCriteria handles CriterionTypeCPE, emitting the SCANNED CPE form which postConvert maps back to the user-supplied URI form.
  • CPE confidence design — two-valued for vuls2-migrated sources: ExactVersionMatch (100) when a criterion accepts (cpe / range / cpe_matches hit), VendorProductMatch (10) when no criterion accepts but the detection index matched on part:vendor:product (walkVulnerabilityDetections walks the raw criteria tree and collects scanned CPEs sharing part:vendor:product with a vulnerable=true CPE criterion; vulnerable=false hardware guards are excluded). RoughVersionMatch (80) is intentionally retired — both remaining levels derive from information the schema already carries. Fallback CPEs fill CpeURIs only when the CVE has no exact match.
  • CPE-AND relax (pruneCriteria): for the cpe ecosystem, vulnerable=false subtrees (hardware/environment guards under AND) are skipped — they cannot be confirmed from a CPE-only scan, and ignoring them matches historical go-cve-dictionary behaviour users rely on.
  • Exploit/Mitigation parity: vuls-data-update extractors lift NVD reference tags (Exploit, Mitigation) into content-level slots at extract time; walkVulnerabilityDatas converts them to models.Exploit/models.Mitigation, and mergeVulnInfo now dedup-appends both (it previously dropped them on merge).
  • deps: vuls-data-update@ca09bbc7, vuls2@22e83337 — the branch builds standalone, no go.work overrides.

Testing

  • go test ./... — green. (scanner/TestAnalyzeLibrary_Golden initially failed on this branch: the dependency bump transitively raised packageurl-go v0.1.5 → v0.1.6, whose purl escaping no longer percent-encodes &; the juddiv3 golden was regenerated accordingly. Pinning back to v0.1.5 was not an option since both vuls-data-update and vuls2 require v0.1.6.)
  • E2E with a NVD+JVN go-cve-dictionary sqlite and an NVD-only vuls2 DB, scanning cpe:/a:apache:http_server:2.4.49: 57 CVEs via vuls2 (NvdExactVersionMatch) + 15 via go-cve-dictionary (JvnVendorProductMatch), 0 double-reports; JVNDB advisory IDs land in DistroAdvisories.
  • vuls diff detection over the 69 vulsio/integration scan fixtures (master binary vs this branch, same nightly DB): all Linux families 0 diff; Windows families Removed=0 with small Added counts attributable to the newer vuls2 library's KB-supersession improvements (feat(detect/ospkg/microsoft): include forward superseders in vcm scope MaineK00n/vuls2#370), not to this PR's CPE changes.
  • vuls-compare runs over CPE-bearing scan results (apache typical / cisco non-semver / linux_kernel over-match / juniper non-semver): CVE sets match the classic dictionary path exactly (0 over-detect, 0 missed); remaining diffs are reference-ordering noise.
  • Test_postConvert gains a "cpe vendor:product fallback" case: no accepted criterion, index-level vendor:product match → NvdVendorProductMatch confidence with the scanned CPE restored to URI form; different-product and vulnerable=false criterions do not contribute.

shino and others added 6 commits June 10, 2026 15:50
CPE-URI detection moves from the go-cve-dictionary-backed
DetectCpeURIsCves to the vuls2 library: preConvert forwards the
per-server CPE list (converted to CPE 2.3 FS form) on
scanTypes.ScanResult.CPE, detect() consults cpe.Detect alongside
ospkg.Detect in the same DB session, and walkCriteria maps accepted
CPE indices back to the scanned CPE forms.

vuls2's NVD data carries cpematch-expanded criteria, which both fixes
non-semver version matching (cisco ios "15.1(4)m3" etc.) and picks up
range edge cases the dictionary path missed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NVD configurations frequently use an AND between a vulnerable product
and an environment guard (e.g. "vulnerable iff running on broadcom
hardware"). Existing vuls + go-cve-dictionary users rely on the
historical behaviour of ignoring the env side — they typically scan
with only the OS / app CPE and never list the hardware. To preserve
that behaviour after migrating CPE detection to the vuls2 backend,
relax AND evaluation for ecosystem="cpe":

- pruneCriteria takes the detection ecosystem as an argument.
- For ecosystem=="cpe" inside an AND node, child subtrees / criterions
  whose Vulnerable flag is false (i.e. environment guards) are skipped
  before the standard "every child must be affected" check runs.
- Non-CPE ecosystems are unchanged: AND stays strict.
- Empty AND after stripping → returns empty (correctly treated as
  unaffected by the caller).

Effect on the vuls-compare detection diff (5 non-cisco CPEs):
  vim AND-FP-2 cases       3 → 1 (CVE-2025-66476 & CVE-2022-0319 now hit)
  linux-kernel             CVE-2022-3643 (broadcom AND) now hit
  apple-safari             +139 CVEs migrated from "only gocve" to common
                           (most macOS-conditioned safari vulns)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related cleanups exposed by vuls-compare against the classic gocve
path:

1. AffectedPackages: postConvert assigned an initialised-but-empty
   `models.PackageFixStatuses{}` to CVEs with no affected packages
   (i.e. CPE-only detections). Classic gocve leaves the field nil, so
   the encoded JSON shifted from `null` to `[]`. Drop the make() when
   the source map is empty.

2. CpeURIs: walkCriteria emitted the criterion's CPE field, which is
   the matched-spec form (e.g. `cpe:2.3:a:vim:vim:*:*:*:*:*:*:*:*`
   with the version anonymised to `*`). Use the SCANNED CPE indexed
   by `fcn.Accepts.Version` instead, which preserves the user's
   configured version. postConvert then maps each FS string back to
   the user-supplied URI/FS form via a new fsToOriginalCPE map
   threaded from toFSCPEs → preConvert → postConvert.

Verified with vuls-compare against bash/jq/openssl/wget/vim scans:
both fields now match the classic-path output bit-for-bit.

Signature changes:
- preConvert returns (ScanResult, map[string]string)
- toFSCPEs returns ([]string, map[string]string)
- postConvert takes the map as a third arg
- existing tests pass `nil` for the map (already exercised in fix-path)

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

Bring back the DetectCpeURIsCves call that was dropped when CPE
detection moved to vuls2, but with a twist: the NVD contribution of
every go-cve-dictionary detection is stripped before use.

Division of labour:
- vuls2.Detect — authority for NVD CPE detection. Its DB carries the
  NVD feed with cpematch-expanded criteria (Strategy E), which both
  fixes non-semver version matching and avoids go-cpe over-match
  false positives.
- DetectCpeURIsCves (go-cve-dictionary) — contributes only the
  sources vuls2 does not cover: JVN, Cisco, Paloalto, Fortinet,
  Vulncheck, EUVD, MITRE. detail.Nvds is nil-ed per detection;
  detections that were NVD-only disappear entirely and are re-detected
  by vuls2 from its own NVD data.

Ordering matters: DetectCpeURIsCves runs BEFORE vuls2.Detect, so
vuls2's NVD content lands on a ScannedCves map that never contains
go-cve-dictionary's NVD remnants — the two paths cannot double-report
the same source. (vuls2.Detect merges into existing VulnInfos
per-source via CveContents append, so JVN entries registered first
are preserved.)

Implementation notes:
- The CPE collection loop now builds two parallel views: cpeURIs
  []string for vuls2.Detect and cpes []Cpe for go-cve-dictionary.
  UseJVN mirrors the original pre-vuls2 behaviour: user-supplied and
  OWASP-DC CPEs consult JVN; synthesised Apple CPEs are NVD-only
  (UseJVN=false), so they contribute nothing on the dictionary path
  once NVD is stripped, and are effectively vuls2-only.
- The `!detail.HasNvd() && detail.HasJvn()` advisory branch loses its
  HasNvd guard — NVD is always stripped, the condition was dead.
- getMaxConfidence needs no change: with Nvds nil it simply never
  returns an Nvd* confidence; Cisco/Paloalto/Fortinet/Vulncheck/JVN
  ranks are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0 models

vuls-data-update extractors lift detection-relevant NVD reference tags
("Exploit", "Mitigation") into the vulnerability content's Exploit /
Mitigations slots at extract time (Exploit.Link and
Remediation.Description carry the reference URL). Surface those in the
vuls2-routed path:

- walkVulnerabilityDatas: convert v.Content.Exploit into
  models.Exploit{ExploitType: NVD, URL} and v.Content.Mitigations into
  models.Mitigation{CveContentType, URL}, attached to the built
  VulnInfo. The classic gocve path derives the same entries in
  ConvertNvdToModel, so without this the vuls2 path silently drops
  them.
- mergeVulnInfo: dedup-append Exploits and Mitigations when merging
  per-source VulnInfos — previously the merge dropped both fields
  entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Track the merged upstream work this branch depends on:

- MaineK00n/vuls-data-update@ca09bbc7 — cpecriterion sibling kind
  (#836), NVD v2 feed extractor with cpematch expansion + reference-tag
  Exploit/Mitigation lift (#827), concrete-attribute over-match guard
  (#841).
- MaineK00n/vuls2@22e83337 — CriterionTypeCPE consumption in detect
  (#382) and part:vendor:product CPE detection index (#384).

With these pins the branch builds standalone (no go.work overrides).

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

This PR reroutes NVD-based CPE detection to the vuls2 DB (to leverage cpematch-expanded criteria and avoid version-format pitfalls) while keeping go-cve-dictionary CPE detection for non-NVD sources (JVN, Cisco, Paloalto, Fortinet, Vulncheck, EUVD, MITRE, etc.). It also updates vuls2-path data conversion to preserve user-supplied CPE forms and to carry exploit/mitigation metadata through merges.

Changes:

  • Update the vuls2 detector to run CPE detection (NVD) alongside OS-package detection in one DB session and convert CPEs to/from CPE 2.3 FS.
  • Update the main detector flow to run go-cve-dictionary CPE detection first (stripping NVD) and then run vuls2 detection.
  • Bump dependencies (vuls2, vuls-data-update, and related indirect modules) and update tests/source ID handling.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
detector/detector.go Collect CPEs from scan result snapshot + OWASP + synthesized Apple CPEs; strip NVD results from go-cve-dict CPE detection; run unified vuls2 detection afterwards.
detector/vuls2/vuls2.go Add vuls2 CPE detection path, CPE FS conversion/restoration, CPE-AND relax pruning, exploit/mitigation conversion & merge behavior.
detector/vuls2/vendor.go Update Red Hat VEX source ID handling and sorting/tag comparison logic.
detector/vuls2/vuls2_test.go Update tests for new exported helper signatures and updated criterion/source types.
go.mod Pin updated vuls2 and vuls-data-update versions and bump several direct/indirect deps.
go.sum Module sum updates corresponding to dependency bumps.
Comments suppressed due to low confidence (1)

detector/vuls2/vuls2.go:101

  • When merging vuls2-detected VulnInfo into an existing r.ScannedCves entry (e.g., after DetectCpeURIsCves ran first), the merge currently ignores CpeURIs, Exploits, and Mitigations, so those fields can be silently dropped for CVEs present in both paths. Also, viBase.CveContents may be nil for existing entries, which would panic on assignment.
	vulnInfos, err := postConvert(vuls2Scanned, vuls2Detected, fsToOriginalCPE)
	if err != nil {
		return xerrors.Errorf("Failed to post convert. err: %w", err)
	}


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

Comment thread detector/vuls2/vuls2.go Outdated
Comment thread detector/detector.go Outdated
…hs run

Per Copilot review on #2575:

- detect(): when the same RootID was flagged by both the OS-package and
  CPE paths, addDetection fetched Vulnerability/Advisory data only for
  the FIRST detection's ecosystem/datasources and just appended the
  later detection — dropping the content the other path needs. Collect
  detections first, then fetch once per RootID without
  ecosystem/datasource narrowing (mirrors vuls2's pkg/detect.detect).
- detector.go: format the cpeURIs slice with %v instead of %s in the
  DetectCpeURIsCves error message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ca09bbc7/22e83337 bump was done with `go get` alone; CI's
`go mod tidy && git diff --exit-code` gate caught the residue.

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

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

Comment thread detector/detector.go Outdated
…mode

Per Copilot review on #2575: DetectPkgCves had become post-processing
only, with the actual vuls2 detection moved to the main Detect flow.
But server mode (server/server.go) calls DetectPkgCves directly and
never the main flow — OS-package detection silently disappeared there.

Restore the master-era division of responsibility:

- DetectPkgCves runs vuls2.Detect (OS packages / Microsoft KB) again,
  so every caller — report flow and server mode alike — gets package
  detection, and the FixState / ListenPortStats post-processing inside
  DetectPkgCves once again runs AFTER detection results exist.
- vuls2.Detect drops the cpeURIs parameter; CPE detection moves to a
  new vuls2.DetectCPEs, called from the main Detect flow after the CPE
  list is collected. DetectCPEs suppresses the OS-package /
  Microsoft-KB inputs in the converted scan result so the two entry
  points cannot double-detect packages (vuls2's merge appends
  AffectedPackages, so a double run would duplicate them).
- Both share the session/convert plumbing via detectWith.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `!detail.HasNvd() && detail.HasJvn()` guard means "emit JVNDB
advisories only when NVD does NOT cover this CVE" — the JVN advisory
is redundant when NVD content exists. Rewriting it to an unconditional
HasJvn() check after the NVD strip changed that semantics: CVEs
covered by both NVD and JVN started emitting JVNDB DistroAdvisories
that the classic path suppressed.

Capture hadNvd before nil-ing detail.Nvds and gate the JVN advisory
emission on the pre-strip value.

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

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Comment thread detector/vuls2/vuls2.go Outdated
Comment thread detector/vuls2/vuls2.go Outdated
Per Copilot review on #2575:
- isVulnerableTrue's comment claimed CPE criteria have no Vulnerable
  concept, but the implementation does consult CPE.Vulnerable; restate
  the doc to match (Version and CPE check their flag, the rest default
  to true).
- "vuls/ normalises" → "vuls normalises".

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

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Comment thread detector/vuls2/vuls2.go Outdated
Comment thread detector/vuls2/vuls2.go Outdated
Comment thread detector/detector.go Outdated
The cpecriterionTypes import landed mid-block during the rebase; CI's
golangci-lint (goimports) flagged it.

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

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Comment thread detector/detector.go Outdated
Comment thread detector/vuls2/vuls2.go
Comment thread detector/vuls2/vuls2.go Outdated
…os + dedup FS CPEs

Per Copilot review on #2575:

- Detect's ScannedCves merge loop now dedup-appends Exploits and
  Mitigations when the CVE already exists (e.g. registered first by the
  go-cve-dictionary non-NVD path); previously the vuls2-derived entries
  were silently dropped for mixed-source CVEs.
- toFSCPEs dedups by FS form: the first user-supplied form wins in both
  the detection list and the reverse map, instead of re-detecting the
  same FS string for each duplicate input.

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

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

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

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Comment thread detector/vuls2/vuls2.go
Comment thread detector/detector.go
…points

DetectPkgCves/DetectCpeURIsCves advertise themselves as library entry
points, but a zero-value ScanResult (ScannedCves == nil) panicked on
the first map assignment in mergeIntoScannedCves or the
go-cve-dictionary helper. The report and server flows always
initialize the map, so this only bit direct library consumers — guard
both entry paths and pin it with a merge test case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shino shino requested a review from Copilot June 12, 2026 03:57

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

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

Comment thread detector/vuls2/vuls2.go

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

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

Comment thread detector/detector.go Outdated
The skip gate listed every Has* accessor, but go-cve-dictionary's
GetByCpeURI admits a detail on Nvd/Vulncheck/Jvn/Fortinet/Paloalto/
Cisco matches only — EUVD and MITRE are enrichment contents that ride
along and are no detection basis (getMaxConfidence has no tier for
them either). An NVD-only detection carrying EUVD/MITRE content
therefore survived the NVD strip and registered with a zero-value
confidence. Align the gate with the dictionary's admission sources.

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

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

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

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

shino and others added 7 commits June 15, 2026 12:23
A criterion with version=* and no Range / no CPEMatches states that
every version of the product is affected, so an accepted match is
exact regardless of the scanned version — not vendor:product. The
previous code demoted all version-unrestricted accepts to
VendorProductMatch, which was wrong for NVD (where a bare version=*
is a deliberate "all versions" statement).

Split the accept classification:
- version=* (no Range/CPEMatches): all versions affected -> exact
- version=NA: no version concept -> vendor:product (matches gocve)
- version-restricted + version-less query: cannot compare -> vendor:product
- version-restricted + concrete query: -> exact

JVN, whose every criterion is version=*, must not be inflated to
exact: it carries no version data at all. That demotion already lives
in toVuls0Confidence (JVN maps to JvnVendorProductMatch from whichever
tier it lands in); a comment now states why, and a postConvert case
pins JVN version=* -> JvnVendorProductMatch alongside NVD version=* ->
NvdExactVersionMatch.

cpecriterion.Accept already accepts version=* (no narrowing -> accept
on attribute match), and the NVD extractor emits such criteria, so no
vuls-data-update change is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JVN is not a vuls2 CPE source yet (go-cve-dictionary serves it), so
the exact-tier JVN case in toVuls0Confidence is dead code today.
Reframe the comment from "JVN never reaches exact" (an active-demotion
claim) to "currently unreachable; when JVN migrates a version=* match
is legitimately exact — revisit then (no JvnExactVersionMatch exists)".
Drop the postConvert JVN case that forced this unreachable path and
pinned the placeholder VP behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
walkCPECriteria's recursive walk propagated the child error bare while
its sibling walkPkgCriteria wraps the same recursion with
xerrors.Errorf("Failed to walk criteria. err: %w", err). Match the
convention so a deep CPE-tree error keeps its locality.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous commit wrapped walkCPECriteria's recursion with "Failed
to walk cpe criteria", the same message the walkVulnerabilityDetections
closure already adds — a literal double wrap. walkPkgCriteria's
recursion uses the generic "Failed to walk criteria" with the closure
supplying the "pkg criteria" specificity; mirror that for cpe so the
two paths are symmetric and the dup is gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
master wraps the walkCriteria result with "Failed to walk criteria" at
the walkVulnerabilityDetections call site; the refactor's IIFE
dispatcher left a bare `return err` there, dropping that wrap. Replace
the IIFE with a plain if/else so each branch wraps the walk error
("Failed to walk cpe/pkg criteria") and returns directly — no bare
propagation, no double wrap, and no tuple-returning closure.

The walkCPECriteria / walkPkgCriteria top-level bare returns stay as
is: they propagate an already recursion-wrapped error up to this
caller wrap, which is net-identical to master's recursion-wrap +
caller-wrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
walkCPECriteria / walkPkgCriteria propagated walk(ca)/walk(pruned)
errors bare at their top-level. Wrap them too so no error return in
the CPE detection path is bare — a reader never has to ask "why isn't
this one wrapped?". The message repeats the recursion's "Failed to
walk criteria" by design; consistency beats avoiding the minor
duplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the verified-replaces-unverified special case on Exploits and
match the AppendIfMissing convention of Confidences / DistroAdvisories
/ Mitigations. A lone exploit whose only difference across passes is
the Verified flag would mean the upstream data disagrees with itself —
not something this merge should arbitrate — and a one-off "replace"
rule here was the odd one out that invited "why is this different?".

The merge test now asserts first-wins for a same-key exploit.

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

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

Comment thread detector/vuls2/vuls2.go

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

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

Comment thread models/vulninfos_test.go
Exploits.AppendIfMissing got a test but its sibling Mitigations did
not. Mirror it to lock in the dedup key (CveContentType, URL,
Mitigation): append when missing, first-wins on the same key, and a
differing Mitigation text being a distinct entry.

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

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

Comment thread detector/vuls2/vuls2.go
…t test

The vuls2 CPE path now emits NVD CveContents / Exploits / Mitigations,
but FillCvesWithGoCVEDictionary later appended its own NVD-derived
copies with no dedup, so vuls2-detected NVD CVEs ended up with
duplicated cveContents[nvd], exploit, and mitigation entries.

- Exploits / Mitigations: switch the plain append to AppendIfMissing.
- nvd CveContents: drop any pre-filled nvd entries before appending
  go-cve-dictionary's, since gocve is the NVD content authority during
  the transition. A SourceLink dedup cannot be used here — gocve emits
  one nvd CveContent per CVSS source, all sharing the NVD SourceLink,
  so it would collapse those legitimate per-source entries.

Also gofmt the Mitigations test added in the previous commit (the
goimports gap that failed lint).

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

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

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

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

shino and others added 2 commits June 16, 2026 15:18
vendorProductEligible mirrored go-cve-dictionary's match() by re-evaluating
a range with RPM-style comparison when the semver comparator could not parse
the query (e.g. juniper "21.4r3", safari "1.0.0b1"), reporting in-range hits
at VendorProductMatch. Empirically that fallback is neither necessary nor
sufficient:

- Not necessary: with a well-formed query the matcher already reaches every
  affected version at ExactVersionMatch. Normalising juniper's joined form to
  version=21.4 / update=r3 makes the same wildcard range ("< 22.2") evaluate
  as plain semver; detection is byte-identical with the fallback on or off
  (199 CVEs either way). The fallback only compensates for a malformed query
  representation, which is a detect-side normalizer's responsibility.

- Not sufficient: RPM comparison gives no consistent order for NVD's messy
  pre-release strings. "4.0_beta" > "4.0" but "4beta" < "4.0" (the same
  "4 beta" ordered oppositely by NVD spelling), and "1.0.0b1" > "1.0.0". It
  only lands correct when the leading version digits already dominate; near a
  boundary it mis-orders. Vendors like safari, whose NVD version formats are
  inconsistent, cannot be served by any version-comparison heuristic here.

The fallback only ever produced the retired RoughVersionMatch tier (folded
to VendorProductMatch), so removing it leaves ExactVersionMatch untouched;
it drops only the fuzzy in-range VP guesses for non-semver query versions.
Splitting such version strings belongs in a future detect-side query
normalizer (tractable for regular forms like juniper, a known gap for
irregular ones like safari).

Simplify vendorProductEligible to the version-less cases (query ANY/NA, or
criterion NA), delete rangeVendorProductEligible, and drop the now-unused
go-rpm-version and cpecriterion range/criterion imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iving it

CPE match-quality determination moved upstream into vuls-data-update's
cpecriterion.Match (exact / version-unconfirmed). walkCPECriteria is now a
pure projection: it reads FilteredCriterion.Accepts.CPE.{Exact,
VersionUnconfirmed} and folds the supporting scanned CPEs up the AND/OR tree
into vuls0's exact / vendor:product tiers — no more raw CPE re-judgement.

Removed from vuls0: vendorProductEligible, the inline pvpEqual, the
accept-empty vendor:product fallback, and the scanned-version re-derivation
(go-cpe WFN matching, range compare, and the retired RPM fallback are gone
from this layer). The version=NA "all versions" detection the fallback used
to supply now comes through cpecriterion.Match at VersionUnconfirmed, so
NVD/cisco/linux detection is byte-identical (vuls-compare gate).

walkCPECriteria takes a sourceID and demotes JVN matches (JVNFeedRSS /
JVNFeedDetail) from exact to vendor:product — JVN carries no version data, a
source-semantics call kept in vuls0 rather than the source-agnostic matcher.
toVuls0Confidence's JVN exact branch is now documented as unreachable for that
reason. RoughVersionMatch stays retired.

Co-Authored-By: Claude Opus 4.8 <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