@@ -593,6 +593,14 @@ type sourceData struct {
593593 // confidence instead of ExactVersionMatch — the vuls2 replacement for
594594 // go-cve-dictionary's fuzzy CPE reporting.
595595 vpCpes []string
596+ // exactCpesByCVE / vpCpesByCVE hold the per-CVE matched CPEs for SUPPRESSED
597+ // CPE sources (VulnCheck / JVN). Those sources are walked once per
598+ // constituent CVE with that CVE's own verified-product set, so a verified
599+ // product for one CVE never suppresses a sibling and the AND/OR criteria
600+ // satisfaction stays correct per CVE. Non-suppressed sources keep using the
601+ // per-root exactCpes / vpCpes above.
602+ exactCpesByCVE map [string ][]string
603+ vpCpesByCVE map [string ][]string
596604}
597605
598606type rootTag struct {
@@ -632,7 +640,7 @@ func appendMissing(dst, src []string) []string {
632640// VulnInfo.CpeURIs so the report shows the user-supplied CPE rather than
633641// the internal FS-with-wildcards form. Nil / missing keys fall back to
634642// the FS string as-is.
635- func postConvert (scanned scanTypes.ScanResult , detected detectTypes.DetectResult , fsToOriginalCPE map [string ][]string , noJVNCPEs map [string ]struct {}, verifiedProductsByRoot map [dataTypes.RootID ]map [string ]struct {}) (models.VulnInfos , error ) {
643+ func postConvert (scanned scanTypes.ScanResult , detected detectTypes.DetectResult , fsToOriginalCPE map [string ][]string , noJVNCPEs map [string ]struct {}, verifiedProductsByRoot map [dataTypes.RootID ]map [string ]map [ string ] struct {}) (models.VulnInfos , error ) {
636644 m := make (map [source ]sourceData )
637645
638646 if err := walkVulnerabilityDetections (m , scanned , detected .Detected , noJVNCPEs , verifiedProductsByRoot ); err != nil {
@@ -716,14 +724,24 @@ func postConvert(scanned scanTypes.ScanResult, detected detectTypes.DetectResult
716724 }
717725 }
718726
727+ // Suppressed CPE sources (VulnCheck / JVN) were walked per CVE, so
728+ // pick this vulninfo's own matched CPEs rather than the per-root
729+ // union — a verified product for a sibling CVE must not surface or
730+ // gate this one.
731+ exactForCVE , vpForCVE := vd .exactCpes , vd .vpCpes
732+ if isSuppressedCPESource (src .SourceID ) {
733+ exactForCVE = vd .exactCpesByCVE [vi .CveID ]
734+ vpForCVE = vd .vpCpesByCVE [vi .CveID ]
735+ }
736+
719737 base .kbIDs = appendMissing (base .kbIDs , vd .kbIDs )
720- base .exactCpes = appendMissing (base .exactCpes , vd . exactCpes )
721- base .vpCpes = appendMissing (base .vpCpes , vd . vpCpes )
738+ base .exactCpes = appendMissing (base .exactCpes , exactForCVE )
739+ base .vpCpes = appendMissing (base .vpCpes , vpForCVE )
722740
723741 // A KB/CPE detection signal marks this CVE emittable. packStatuses
724742 // are deliberately excluded — packages are gated separately through
725743 // comparePack's per-source winner selection.
726- if len (vd .kbIDs ) > 0 || len (vd . exactCpes ) > 0 || len (vd . vpCpes ) > 0 {
744+ if len (vd .kbIDs ) > 0 || len (exactForCVE ) > 0 || len (vpForCVE ) > 0 {
727745 if ! slices .Contains (vd .detectableCveIDs , vi .CveID ) {
728746 vd .detectableCveIDs = append (vd .detectableCveIDs , vi .CveID )
729747 }
@@ -830,30 +848,49 @@ func postConvert(scanned scanTypes.ScanResult, detected detectTypes.DetectResult
830848 return vim , nil
831849}
832850
833- func walkVulnerabilityDetections (m map [source ]sourceData , scanned scanTypes.ScanResult , vs []detectTypes.VulnerabilityData , noJVNCPEs map [string ]struct {}, verifiedProductsByRoot map [dataTypes.RootID ]map [string ]struct {}) error {
851+ func walkVulnerabilityDetections (m map [source ]sourceData , scanned scanTypes.ScanResult , vs []detectTypes.VulnerabilityData , noJVNCPEs map [string ]struct {}, verifiedProductsByRoot map [dataTypes.RootID ]map [string ]map [ string ] struct {}) error {
834852 for _ , v := range vs {
835- // The part:vendor:product set DEFINED by the verified CPE sources
836- // (NVD / Fortinet / Cisco / PaloAlto) for this CVE — used to suppress
837- // overlapping VulnCheck / JVN matches, mirroring go-cve-dictionary's
838- // isCpeURIAlsoDefinedInVerifiedDataSource. Nil when no verified source
839- // defines anything for the CVE.
840- verifiedProducts := verifiedProductsByRoot [v .ID ]
841853 for _ , d := range v .Detections {
842854 for sourceID , fconds := range d .Contents {
843855 for _ , fcond := range fconds {
844856 var (
845- statuses []packStatus
846- kbIDs []string
847- exactCpes []string
848- vpCpes []string
857+ statuses []packStatus
858+ kbIDs []string
859+ exactCpes []string
860+ vpCpes []string
861+ exactByCVE map [string ][]string
862+ vpByCVE map [string ][]string
849863 )
850- if d .Ecosystem == ecosystemTypes .EcosystemTypeCPE {
851- exact , vp , err := walkCPECriteria (sourceID , fcond .Criteria , scanned , noJVNCPEs , verifiedProducts )
864+ switch {
865+ case d .Ecosystem == ecosystemTypes .EcosystemTypeCPE && isSuppressedCPESource (sourceID ):
866+ // Suppressed sources (VulnCheck / JVN) are walked ONCE
867+ // PER constituent CVE with that CVE's own verified
868+ // product set — the part:vendor:product the verified CPE
869+ // sources (NVD / Fortinet / Cisco / PaloAlto) DEFINE for
870+ // that CVE, mirroring go-cve-dictionary's
871+ // isCpeURIAlsoDefinedInVerifiedDataSource. Per-CVE keeps a
872+ // verified product for one CVE from suppressing a sibling
873+ // and keeps AND/OR satisfaction correct per CVE.
874+ exactByCVE , vpByCVE = map [string ][]string {}, map [string ][]string {}
875+ for _ , cveID := range cveIDsOf (v ) {
876+ exact , vp , err := walkCPECriteria (sourceID , fcond .Criteria , scanned , noJVNCPEs , verifiedProductsByRoot [v.ID ][cveID ])
877+ if err != nil {
878+ return xerrors .Errorf ("Failed to walk cpe criteria. err: %w" , err )
879+ }
880+ if len (exact ) > 0 {
881+ exactByCVE [cveID ] = append (exactByCVE [cveID ], exact ... )
882+ }
883+ if len (vp ) > 0 {
884+ vpByCVE [cveID ] = append (vpByCVE [cveID ], vp ... )
885+ }
886+ }
887+ case d .Ecosystem == ecosystemTypes .EcosystemTypeCPE :
888+ exact , vp , err := walkCPECriteria (sourceID , fcond .Criteria , scanned , noJVNCPEs , nil )
852889 if err != nil {
853890 return xerrors .Errorf ("Failed to walk cpe criteria. err: %w" , err )
854891 }
855892 exactCpes , vpCpes = exact , vp
856- } else {
893+ default :
857894 s , k , err := walkPkgCriteria (d .Ecosystem , sourceID , fcond .Criteria , fcond .Tag , scanned )
858895 if err != nil {
859896 return xerrors .Errorf ("Failed to walk pkg criteria. err: %w" , err )
@@ -865,8 +902,9 @@ func walkVulnerabilityDetections(m map[source]sourceData, scanned scanTypes.Scan
865902 // WITHOUT registering it in m: the downstream walk
866903 // treats presence in m as "this source detected
867904 // something", and an empty entry would emit contents
868- // for an undetected CVE.
869- if len (statuses ) == 0 && len (kbIDs ) == 0 && len (exactCpes ) == 0 && len (vpCpes ) == 0 {
905+ // for an undetected CVE. A suppressed source's per-CVE
906+ // result counts as a signal here too.
907+ if len (statuses ) == 0 && len (kbIDs ) == 0 && len (exactCpes ) == 0 && len (vpCpes ) == 0 && len (exactByCVE ) == 0 && len (vpByCVE ) == 0 {
870908 continue
871909 }
872910
@@ -885,6 +923,18 @@ func walkVulnerabilityDetections(m map[source]sourceData, scanned scanTypes.Scan
885923 base .kbIDs = append (base .kbIDs , kbIDs ... )
886924 base .exactCpes = append (base .exactCpes , exactCpes ... )
887925 base .vpCpes = append (base .vpCpes , vpCpes ... )
926+ for cveID , cpes := range exactByCVE {
927+ if base .exactCpesByCVE == nil {
928+ base .exactCpesByCVE = map [string ][]string {}
929+ }
930+ base .exactCpesByCVE [cveID ] = append (base .exactCpesByCVE [cveID ], cpes ... )
931+ }
932+ for cveID , cpes := range vpByCVE {
933+ if base .vpCpesByCVE == nil {
934+ base .vpCpesByCVE = map [string ][]string {}
935+ }
936+ base .vpCpesByCVE [cveID ] = append (base .vpCpesByCVE [cveID ], cpes ... )
937+ }
888938 m [src ] = base
889939 }
890940 }
@@ -1161,9 +1211,10 @@ func hasSuppressedCPESource(v detectTypes.VulnerabilityData) bool {
11611211// while NVD is under the CVE root), so the verified data is gathered across all
11621212// roots that alias the CVE via GetVulnerabilityDataByVulnerabilityID. Results
11631213// are cached per CVE so VulnCheck and JVN detections of the same CVE share one
1164- // bolt read. The returned map is keyed by the suppressed source's RootID for a
1165- // direct lookup in walkVulnerabilityDetections.
1166- func collectVerifiedProducts (sesh * session.Session , detected detectTypes.DetectResult ) (map [dataTypes.RootID ]map [string ]struct {}, error ) {
1214+ // bolt read. The returned map is keyed by the suppressed source's RootID, then
1215+ // by constituent CVE ID, so a multi-CVE root keeps each CVE's verified product
1216+ // set separate (a verified product for one CVE must not suppress a sibling).
1217+ func collectVerifiedProducts (sesh * session.Session , detected detectTypes.DetectResult ) (map [dataTypes.RootID ]map [string ]map [string ]struct {}, error ) {
11671218 cache := make (map [string ]map [string ]struct {})
11681219 productsFor := func (cveID string ) (map [string ]struct {}, error ) {
11691220 if set , ok := cache [cveID ]; ok {
@@ -1193,23 +1244,23 @@ func collectVerifiedProducts(sesh *session.Session, detected detectTypes.DetectR
11931244 return set , nil
11941245 }
11951246
1196- out := make (map [dataTypes.RootID ]map [string ]struct {})
1247+ out := make (map [dataTypes.RootID ]map [string ]map [ string ] struct {})
11971248 for _ , v := range detected .Detected {
11981249 if ! hasSuppressedCPESource (v ) {
11991250 continue
12001251 }
1201- merged := make (map [string ]struct {})
1252+ byCVE := make (map [ string ] map [string ]struct {})
12021253 for _ , cveID := range cveIDsOf (v ) {
12031254 set , err := productsFor (cveID )
12041255 if err != nil {
12051256 return nil , xerrors .Errorf ("Failed to get verified products. CVE-ID: %s, err: %w" , cveID , err )
12061257 }
1207- for k := range set {
1208- merged [ k ] = struct {}{}
1258+ if len ( set ) > 0 {
1259+ byCVE [ cveID ] = set
12091260 }
12101261 }
1211- if len (merged ) > 0 {
1212- out [v .ID ] = merged
1262+ if len (byCVE ) > 0 {
1263+ out [v .ID ] = byCVE
12131264 }
12141265 }
12151266 return out , nil
0 commit comments