Skip to content

fix: skip NVD entries whose target_sw names another language runtime - #8834

Closed
Akhil-1527 wants to merge 1 commit into
dependency-check:mainfrom
Akhil-1527:fix-7139-target-sw-ecosystem
Closed

Akhil-1527 wants to merge 1 commit into
dependency-check:mainfrom
Akhil-1527:fix-7139-target-sw-ecosystem

Conversation

@Akhil-1527

Copy link
Copy Markdown

Description of Change

NvdCveAnalyzer.ecosystemMatchesTargetSoftware only looked at target_sw for Node.js dependencies and let everything else match. So a CPE match on a Java dependency kept CVEs whose only entries target another runtime, like CVE-2019-7617 (cpe:2.3:a:elastic:apm_agent:*:*:*:*:*:python:*:*) on the Java APM agent (#3876) and CVE-2020-36448 (cpe:2.3:a:cache_project:cache:*:*:*:*:*:rust:*:*) on cache4k-jvm (#6851).

Changes:

  • The target_sw switch in CveItemOperator.extractEcosystem is now CveItemOperator.ecosystemOfTargetSw, used by both the database build and the analyzer. rust was missing and is added.
  • The analyzer drops an entry when its target_sw names a language runtime (.NET, Go, Java, Perl, PHP, Python, Ruby, Rust) and the dependency is from a different one. Other targets keep matching: wildcards, native ones like linux or c/c++, iOS, ColdFusion, unmapped values, and JavaScript and Node.js, since JavaScript libraries ship inside webjars, NuGet packages and gems. Dependencies outside those runtimes (no ecosystem, native, iOS, JavaScript) are not filtered, and Node.js dependencies keep their current rules.
  • filterEcosystem no longer edits its input. CveDB.getVulnerabilities caches the list per CPE and hands the same list and Vulnerability objects to every dependency with that CPE. With more ecosystems filtering, a Python dependency could remove a CVE that a Java dependency with the same CPE still needs. It now returns a new list. Since a Vulnerability has no copy, it also stops removing the non-matching software entries from a kept vulnerability. That pruning only happened for Node.js dependencies, so their reports may now list a CVE's other entries next to the matched one. Happy to keep the pruning with a copy instead if you prefer.

Removing the bundled suppressions this makes redundant can be a follow-up PR.

Related issues

Have test cases been added to cover the new functionality?

Yes. A new NvdCveAnalyzerTest covers the runtime matching, the Node.js rules and that the cached list is left alone, and CveItemOperatorTest covers the mapping. mvn -pl core -am test passes locally (3492 tests in core).


@Test
void testTargetSoftwareForTheSameRuntimeMatches() {
assertTrue(ecosystemMatchesTargetSoftware(Ecosystem.JAVA, "java"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a question to maintainers and others. Why does it have to match a lower case string and is there a guarantee that the string passed to this function is always lowercase ?

i.e, if i pass

ecosystemMatchesTargetSoftware(Ecosystem.JAVA, "Java") This still asserts true because there is no runtime match to "Java" right ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Keeping in mind I had no idea we even had any of this logic here prior to this PR (otherwise I'd have submitted a PR myself ages ago), my understanding is

  • technically speaking,for target_sw within the CPE naming spec, the values could be of any capitalization but the comparison internally between different CPEs MUST be case sensitive, i.e Java == java when comparing.
  • since the comparison is defined as case insensitive, generally most things are thus lower case in the official CPE directory and affected versions
  • Ecosystem is ODC's own calculated/derived concept based on the CPE fields, but worth noting that it predates the enumeration of ecosystems from PURL so is different to PURL's concept.
  • So when matching/converting to our own ecosystem it is our choice how to do that matching, but doing it case insensitively is both sensible and consistent with CPE<>CPE literal string comparisons. So we can normalise to whatever we like, and a case insensitive match like is currently done is fine, since we define that all ecosystems are lowercase, and we decide what they mean.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would it be better to convert these magic strings into an ENUM ? @Akhil-1527 @chadlwilson

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah, the whole thing is a bit of a mess IMHO, however to some extent classes like Ecosystem form part of a public API of ODC which may be used by others within code, so my understanding is that we generally couldn't just convert the type to an Enum or value type, unless we are pretty confident it's unlikely to be used directly by outside code/plugins/integrations.

Regardless of this, per my other comments, some of the code here is in the wrong place that will make it just as difficult to maintain correctly as the current situation.

In some sense, the reason this has become broken seems to be that the filtering hasn't been properly maintained over time, probably because no-one realised a bunch of filtering hacks were buried inside NvdCveAnalyzer and CveItemOperator, rather than closer to the CPE, the VulnerableSoftware, the CpeAnalyzer, the Ecosystem .... or other relevant domain matching logic.

@neilscallywag neilscallywag Oct 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think making any kind of "proper" fix where you consolidate logic from NvdCveAnalyzer and CveItemOperator would only result in breaking changes to these external plugins relying on them (which I think exist primarily to override these custom hacks).

Injecting an orchestrator like class to handle these cases could be one possible way but I am unsure if that will create more problems than it resolves.

I don't have sufficient historical context or domain expertise to propose another viable solution.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There's a difference to changing behaviour to the interface. And the analyser logic is clearly internal code, despite its lax API (due to the constraints of the java package system for defining API intention).

Its generally only risky for domain types where people might have written custom analyzers that work with them, and where it is used to bind to things like hints. Much of the current logic is in private methods so we can do what we want.

Generally speaking the behaviour of stuff other than the Engine is not a contract, to my knowledge.

@chadlwilson chadlwilson left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am not yet an expert on this logic, however I am less confident that the overall approach here is correct, because there is a lot of logic around that I was not aware of, such as cpeEcosystemCache in the database etc, and I have no idea how the existing logic is/was intended to work and its limits.

At the moment from my perspective this PR has the normal "LLM PR" problems if not clearly articulating which problems it fixes vs. does not fix, and the design reasoning for where the changes are made and how it related to other logic. As usual, too much focus on the "what" small picture stuff, not enough on the "why" big picture design. Y'know, the bit LLMs make a mess of if not directed properly.

We should at least be able to answer questions like

  • what happens if the CPE value is - or NA? (and what should happen)
  • what happens if the CPE value is * or ANY? (and what should happen)
  • what is happening for a suite of real vulnerabilities? with real upstream analyzers?

In my opinion, we probably shouldnt really have hacks for webjars, because that just opens the door to needing hacks for everything (rubygems distributed via maven central, java extensions inside a ruby gem, all sorts of things). The logic here is in the "wrong" place; only analyzers should really know much about the nature of artifacts they discover are; not the

Javascript and webjars aren't that special; and in most cases when embedded in other artifacts they should be detected independently of the wrapping jar, as shaded - so appear as an embedded npm: or javascript: dependency independently of the wrapping jar/gem/whatever in most cases.

Perhaps it needs to be looked at separately if it's a "real" problem that these thing are false negatives without the hacks here, however to me this is essentially a modelling problem that should be solved earlier in the process, where analyzers populate EvidenceType.ECOSYSTEM or something like that and the whole matching is done earlier in the CpeAnalyzer.

I'd also like to see some analysis/count of the values of target_sw from affected software expressions across a real dump of vulnerabilities so we have some notion of what coverage we java, and can map things appropriately. Right now this mainly assumes that the values are the same as our existing ecosystems, which I am not sure is necessarily true.

For example, most golang vulns for such mult-lang libraries I have anecdotally seen as cpe:2.3:a:grpc:grpc:*:*:*:*:*:go:*:* i.e target_sw = go like https://nvd.nist.gov/vuln/detail/cve-2026-33186. This change will not fix that it seems, because it expects golang, and go is not a known value when normalizing eocsystems, making it incomplete to resolve #7139 because it fails to actually check real data to determine that modelling and mapping of these values is incomplete.

Comment on lines +48 to +56
/**
* Ecosystems of language runtimes. Target software that names one of these
* does not apply to a dependency from another one. JavaScript is left out
* because JavaScript libraries are often bundled in other packages, such as
* webjars, NuGet packages and gems.
*/
private static final Set<String> RUNTIME_ECOSYSTEMS = Set.of(Ecosystem.DOTNET, Ecosystem.GOLANG,
Ecosystem.JAVA, Ecosystem.PERL, Ecosystem.PHP, Ecosystem.PYTHON, Ecosystem.RUBY, Ecosystem.RUST);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I do not like the naming, nor maintaining this enumeration here. In my opinion it should be maintained in the Ecosystem class or CveItemOperator where the rest of the logic is.

With current setup it's not clear

  • what have language runtimes got to do with it?
  • when it is appropriate to add a new value to this set?
  • how/where to handle differences in the real world about how NVD names target_sw vs our Ecosystem concept? do I have to add more hacks like for nodejs?

Comment on lines +153 to +155
* the target software from the NVD. A vulnerability is kept if any of its
* vulnerable software matches. CveDB caches the list per CPE and shares it,
* and its vulnerabilities, with other dependencies, so neither is changed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* the target software from the NVD. A vulnerability is kept if any of its
* vulnerable software matches. CveDB caches the list per CPE and shares it,
* and its vulnerabilities, with other dependencies, so neither is changed.
* the target software from the NVD.

Public javadoc comments shouldn't contain unnecessary implementation details. Normal LLM slop, unfortunately, models trained mostly on ancient coding practices.

@neilscallywag

neilscallywag commented Oct 13, 2026 •

Copy link
Copy Markdown

Ill open a discussion so that we can discuss how to implement this change correctly. Currently, there are no test cases for these "hacks" also which makes impact/blast radius of any change hard to understand.

@chadlwilson @Akhil-1527

@chadlwilson

Copy link
Copy Markdown
Collaborator

@neilscallywag feel free to do so, or we can discuss here or on the linked issue. There are basically no regular contributors outside myself (and I have looked at limited areas) right now so PRs are sometimes a reasonable place to discuss since Jeremy checks them periodically - probably mostly ignores issues etc (since there is so much noise relative to the # of actual code contributors).

This weakness is by far the biggest source of ongoing false positives for NVD CVEs so I'm motivated to get it sorted, since I do basically all of the issue & FP triage & suppressions at the moment. Getting this right would clean up a lot.

If we're confident this change doesn't make things worse I am happy to continue, but I'd be more confident if the requester engaged. At the moment this looks like the typical drive-by AI PR where the raiser has never used the software in question, which is discouraging - but let's see. If no engagement we can always pick to a new branch and open an alternative PR we can collaborate on.

@neilscallywag

neilscallywag commented Oct 13, 2026 •

Copy link
Copy Markdown

Ill just type here.

I supposed the first step would be to have some kind of regression test cases to capture existing behavior first for these hacks so that any change we make does not break anything or introduce something new. These are things listed in the issue #7139

We can also categorise these "hacks" as ingestion time or scan time. Since they roughly happen before creating the db and then during scan.

For ingestion time:
2. Then we can remove all the custom code from CveItemOperator. For this, we probably need to normalise the strings. We also should discuss if cases of "-" and "_" should be normalised in another manner or if that will create any issue eg ("curses_project".equals(vendor) && "curses".equals(product)) or ("node-opencv_project".equals(vendor) && "node-opencv".equals(product)).
3. CpeEcosystemCache also contains some custom logic to collapse conflicting ecosystem classifications collapse to MULTIPLE or null. There are test cases that handle it in CpeEcosystemCacheTest. Do we want to consolidate this logic too ?
4. The parses cve method has a hardcoded CVE too cpe:2.3:a:apache:apache:*:*:*:*:*:*:*:* in CveDB class. Should cases like this be consolidated?

For scan time:

  1. CPEAnalyzer class has a lot of language/vendor specific custom logic being embedded in different functions. I cant think of a good way to consolidate it because the sources are very different.

I'm not what the best way would be to collapse these into one module as they pertain different stages of the model, maybe it would make more sense to first inventory the actual target_sw values in a recent NVD dataset and classify them?

I suppose this PR can be closed for now or should be changed to just add test cases for behavioural coverage instead of code changes. Subsequent smaller PRs can do the clean up once we have decided on a high level design. These are just my 2 cents, I am not an expert in this domain.

Edit: I noticed there is a False Positive analyser . But no changes have been made to it for 4 years.

@mjatin2007

Copy link
Copy Markdown
Contributor

Relevant to @neilscallywag's inventory suggestion, I'm sharing results from a previously completed, bounded, AI-assisted contributor-side inventory of the official NVD JSON 2.0 annual 2026 feed. This covers the raw inventory and a narrow literal comparison, not ecosystem classification or a review verdict on @Akhil-1527's PR.

The feed's embedded timestamp was 2026-10-12T03:00:01.646738. I pinned the payload, and its decompressed SHA-256 matched the feed's official .meta value: fba321b28b85d631f02933d04bbbc79df135b13847057cf32b972fede36fb275.

The inventory found:

  • 70,590 CVE records, of which 33,026 contained configuration matches
  • 208,515 CPE-match occurrences and 85 distinct raw target_sw values. Those values include * and -; excluding those two leaves 9,426 occurrences across 83 raw values
  • 0 tokenizer errors in the bounded extraction (the tokenizer was not a full CPE validator)

A second traversal/tokenizer, implemented separately on the contributor side, reproduced those headline counts. Both checks are ours, so this is a self-consistency check, not independent third-party validation. Occurrences can repeat within a CVE, and the 2026 annual feed is neither all NVD history nor a count of scanner false positives, installations, or affected users.

For the Go case @chadlwilson raised in review, raw target_sw=go appeared 321 times across 189 distinct CVEs; all 321 were vulnerable=true application (part=a) matches. golang appeared 0 times in this snapshot. Although GOLANG is in the PR's runtime-ecosystem set, neither go nor golang appears among the pinned target_sw-to-ecosystem switch cases at head 94442bca.

As one data example, the record @chadlwilson cited—CVE-2026-33186—appears in the pinned feed with cpe:2.3:a:grpc:grpc:*:*:*:*:*:go:*:* and versionEndExcluding: 1.79.3. I did not run a Dependency-Check scan against that record; it is not a reproduced Java-scan false positive.

No uppercase letters appeared in the 85 raw values in this snapshot. The list below is raw inventory, not effective scanner classification: parsing/unescaping, intentional wildcard/native handling, and later analyzer logic can change the result. For example, the feed contains escaped raw values such as joomla\! and c\#; I did not treat raw literal nonmatches as defects.

All 85 raw values (occurrences / distinct CVEs, sorted by occurrence count)
*                              197092 / 29908
node.js                          5068 / 1622
-                                1997 / 513
macos                             1040 / 344
rust                               937 / 215
python                             341 / 300
windows                            322 / 146
go                                 321 / 189
drupal                             223 / 99
android                            123 / 99
ruby                               106 / 62
linux                               93 / 41
joomla\!                            65 / 50
perl                                65 / 65
jenkins                             61 / 54
nginx                               52 / 12
mongodb                             51 / 36
iphone_os                           48 / 36
craft_cms                           47 / 18
kubernetes                          39 / 25
postgresql                          35 / 35
wordpress                           34 / 34
mediawiki                           27 / 12
.net                                24 / 12
c\#                                  24 / 12
suse_linux_enterprise               22 / 1
visual_studio_code                  20 / 20
command_centre                      16 / 2
chrome                              16 / 12
commerce                            14 / 14
swift                               11 / 10
docker                              10 / 10
sap                                 10 / 1
nextcloud                            9 / 5
rapid7                               9 / 9
mattermost                           8 / 6
unix                                 8 / 5
azure                                7 / 7
typo3                                7 / 3
linux_kernel                         7 / 3
c\+\+                                 7 / 7
openfga                              6 / 6
eclipse                              6 / 6
pimcore                              4 / 2
mariadb                              4 / 1
openshift                            4 / 4
magento                              4 / 4
elixir                               4 / 4
cpanel                               4 / 1
pretix                               3 / 2
mysql                                3 / 1
plone                                3 / 1
rancher                              3 / 1
universal_windows_platform           3 / 1
claude_code                          3 / 3
intel_gaudi                          3 / 1
jetbrains                            2 / 2
azure_cli                            2 / 2
glpi                                 2 / 2
django                               2 / 2
multiplatforms                       2 / 1
microsoft_edge                       2 / 2
freebsd                              2 / 2
aws                                  2 / 2
beautiful_soup                       2 / 2
cursor                               2 / 2
theia                                2 / 2
vmware_vsphere                       2 / 2
fastify                              2 / 2
java                                 1 / 1
s.t.a.l.k.e.r._anomaly               1 / 1
gradle                               1 / 1
proxmox_virtual_environment          1 / 1
sap_hana                             1 / 1
humhub                               1 / 1
traefik                              1 / 1
kafka                                1 / 1
laravel                              1 / 1
lenovo_vantage                       1 / 1
gstreamer                            1 / 1
home_assistant                       1 / 1
kubernates                           1 / 1
php                                  1 / 1
directadmin                          1 / 1
plesk                                1 / 1

This gives the design discussion a concrete Go mapping/test case. It does not show that any Dependency-Check scan produced a false positive for these CVEs, establish that adding one literal mapping is sufficient, or settle whether handling belongs at ingestion or scan time. I can share the rerunnable extraction method and per-value vulnerable/application breakdowns if useful.

@Akhil-1527

Copy link
Copy Markdown
Author

Thanks for the detailed review, and sorry for the slow reply.

You're right about go. I kept the values the existing switch already mapped, added rust, and never checked them against real feed data, so GOLANG in the new set can't match anything. Thanks @mjatin2007 for the 2026 counts, they show the same gap.

On */ANY and -/NA: NVD data is all 2.3 strings and the cpe parser returns ANY and NA as * and -, so the literal ANY/NA never reach this method. Both keep matching, same as before this PR. * says the entry applies to any target and - says target_sw doesn't apply, so neither gives us anything to filter on.

On where it lives: cpeEcosystemCache stores one ecosystem per vendor/product and collapses mixed ones to MULTIPLE, so CPEAnalyzer can't tell that one CVE under a product targets python while another doesn't. target_sw is per vulnerable software entry, so the check has to run once we have a CVE's entries for the matched CPE. That's why it went into NvdCveAnalyzer next to the existing Node.js check. I agree the knowledge of which target_sw values name which ecosystem shouldn't be split between CveItemOperator and a second set in the analyzer. I'll move it next to the mapping.

I take the point on webjars. If embedded JS is supposed to be found as its own dependency, the exception just papers over that, so I'll drop it.

What I'd like to do next, in this order:

  1. Tests that pin down current behavior first (the CveItemOperator mapping, the cpeEcosystemCache collapse and the NvdCveAnalyzer filter), as neilscallywag suggested, before changing anything.
  2. Count target_sw values across every NVD year, not just 2026, and post the mapping I'd propose for each value that names a language.
  3. Run the CLI built from main and from this branch over real artifacts (the APM agent jar from False Positive on elastic-apm-agent #3876, cache4k-jvm from [FP]: Rust vulnerability CVE-2020-36448 has resurfaced, this time on dependency cache4k-jvm, even with latest (10.0.3) Dependency Check version #6851, a grpc jar, a couple of webjars, plus Python, Ruby, Go and .NET manifests) and post the diff of reported CVEs, so we can see what drops out and whether anything real goes with it.

I'll also take the javadoc suggestion. Happy to hold the code changes until the data is posted if that's easier to review.

Adds tests for what the code does today, before changing it for dependency-check#7139.

CveItemOperatorTest covers the ecosystem that extractEcosystem stores for
each target_sw value, the vendor and product checks that run first, and
the fallback to the base ecosystem.

NvdCveAnalyzerTest covers which CVEs and vulnerable software entries a
dependency keeps for a matched CPE. Node.js dependencies only keep
Node.js, * and - entries, and every other dependency keeps everything,
including the CVEs from dependency-check#3876 and dependency-check#6851 that only target Python or Rust.
@Akhil-1527
Akhil-1527 force-pushed the fix-7139-target-sw-ecosystem branch from 94442bc to f5e570b Compare October 14, 2026 16:54
@Akhil-1527

Copy link
Copy Markdown
Author

Step 1 is pushed, so the PR is tests only for now (f5e570b). The cpeEcosystemCache collapse already has tests in CpeEcosystemCacheTest, so I didn't add more there.

Here's step 2, target_sw across every NVD year.

I used all 25 annual JSON 2.0 feeds (2002 to 2026), downloaded 2026-10-14 and checked against the sha256 in each .meta file. For every cpeMatch with vulnerable: true I split criteria on unescaped colons and unescaped the values the way the cpe parser does, so joomla\! counts as joomla!. All numbers are for part=a entries. Happy to share the script if anyone wants to rerun it.

  • There are 1,361,498 vulnerable application entries with 598 distinct target_sw values. * and - account for 1,300,587 of them.
  • 209,341 of the 244,083 CVEs with application entries (about 86%) only have * or -, so target_sw can't help with those. 31,539 name a specific target in every entry and 3,203 mix the two.
  • The switch in CveItemOperator maps 40,440 of the 60,911 entries that name a target, and every value in it shows up in the data.
  • rust (653 CVEs) and go (417) are the biggest language values with no mapping, same as you and @mjatin2007 found. golang is in 2 CVEs. The extra spellings the Node.js check in NvdCveAnalyzer accepts (node_js, node-js, npm) don't appear at all.

This is what I'd add to the switch (CVE counts across all years):

ecosystem in the switch today (CVEs) proposed additions (CVEs)
PHP wordpress 16,255, drupal 603, joomla! 353, typo3 133, mediawiki 75, craft_cms 35, mybb 31, joomla 13, moodle 8, php 8, simplesamlphp 4, buddypress 1, openpne 1, phpcms 1, symfony 1, vbulletin3 1, vbulletin4 1 prestashop 195, pimcore 23, nextcloud 22, glpi 16, laravel 16, magento 11, vbulletin 9, xoops 8, opencart 6, silverstripe 6, mantisbt 5, woocommerce 5
NODEJS node.js 3,768, nodejs 2 fastify 5
JAVA android 2,763, java 23 maven 4, spring 1
NATIVE linux 613, unix 98, linux_kernel 35, redhat_enterprise_linux 22, c/c++ 21, suse_linux 13, debian 3, borland_c++ 1, gnu_c++ 1, visual_c++ 1 c++ 12, c 1
IOS iphone_os 691, ios 13, ipad 2, iphone 1 ipados 37, swift 19
PYTHON python 640 pypi 24, django 11, flask 1
RUST rust 653
GOLANG go 417, golang 2
RUBY ruby 405 rails 11, ruby_on_rails 3
PERL perl 143
DOTNET .net 35, c# 14, asp.net 10, dotnetnuke 1 .net_framework 2, dnn 2, nuget 2
JAVASCRIPT jquery 11
ELIXIR elixir 6
DART dart 3
COLDFUSION coldfusion 1

Language names, package registries and frameworks always map. Host apps follow the PHP entries the switch already has (wordpress, drupal and so on): a host maps when the plugins built for it are written in one language, and I only added hosts with at least 5 CVEs. With these additions, 26,739 of the 31,539 CVEs that name specific targets have every value mapped, up from 25,322.

javascript (3 CVEs) stays unmapped. NVD uses it for the JavaScript version of SDKs that come in several languages, like microsoft:azure_identity_sdk, and those ship on npm, so mapping it to JAVASCRIPT would be wrong for them.

Most of what's left says where the software runs rather than what it's written in: windows (2,679 CVEs), macos (1,179), vmware_vsphere (816), chrome (402), edge (354). I left those alone, and didn't touch the platforms the switch already maps (android, iphone_os, linux).

The biggest unmapped value that does imply a language is jenkins: 1,575 CVEs, 1,550 of them with no other target, nearly all jenkins:<plugin>. Jenkins plugins are Java, so it could map to JAVA the way wordpress maps to PHP. jira (39) and confluence (25) are the same for Java, and discourse (15) for Ruby. Should hosts for other languages follow the PHP precedent? I've left them out for now.

Two things in the data change how I'd build this:

  1. The switch already drives CPE matching. CveDB stores what extractEcosystem returns for each vendor/product in cpeEcosystemCache, and CPEAnalyzer.filterEcosystem drops CPEs whose ecosystem doesn't match the dependency's. So once go is in the switch, a product whose CVEs all target Go stops matching Java or Python dependencies at the CPE step (same for Rust). That happens at ingestion though, so an existing database only picks up new values as its CVEs get modified or the database is rebuilt. Is there a usual way to handle that, or is that fine?
  2. When a product's CVEs target more than one ecosystem, the cache stores MULTIPLE and the CPE filter keeps the product for every dependency. Going by target_sw alone, 32 vendor/product pairs have entries in two or more language ecosystems (181 CVEs), for example grpc:grpc, opentelemetry:opentelemetry, snowflake:snowflake_connector and elastic:apm_agent (java, python and go agents, the False Positive on elastic-apm-agent #3876 case). That's where the check in NvdCveAnalyzer is needed.

So for the code I'd put the new values in the switch and have the NvdCveAnalyzer check run each entry through that same mapping, using the same rule as CPEAnalyzer.filterEcosystem: an entry stays when its ecosystem is null, matches the dependency's, or is NATIVE for an iOS dependency, and everything stays when the dependency has no ecosystem. That drops the runtime set from my first version, and adding a value to the switch becomes the only change needed.

That changes two things for Node.js dependencies. They'd keep entries with unmapped targets like windows, which the current check drops, and they'd keep the 5 CVEs that only target fastify (all fastify:fastify-multipart), which the current check drops too. The scans should show whether the first one matters.

I'll start on the scans next.

All 162 target_sw values with at least 5 CVEs (598 in total)
target_sw entries CVEs CVEs with only this target today proposed
* 1,297,226 212,242 208,843
wordpress 19,639 16,255 16,203 PHP PHP
node.js 9,182 3,768 3,609 NODEJS NODEJS
windows 6,007 2,679 1,260
android 3,416 2,763 2,355 JAVA JAVA
- 3,361 1,029 262
macos 2,584 1,179 301
drupal 2,199 603 596 PHP PHP
jenkins 1,965 1,575 1,550
rust 1,747 653 578 RUST
linux 1,356 613 148 NATIVE NATIVE
vmware_vsphere 1,034 816 12
iphone_os 983 691 377 IOS IOS
ruby 777 405 371 RUBY RUBY
python 749 640 602 PYTHON PYTHON
go 607 417 394 GOLANG
joomla! 417 353 344 PHP PHP
chrome 407 402 31
edge 354 354 3
typo3 327 133 131 PHP PHP
internet_explorer 307 303 5
asp.net 289 10 6 DOTNET DOTNET
perl 254 143 140 PERL PERL
vsphere 246 180 8
prestashop 235 195 192 PHP
pivotal_cloud_foundry 225 11 2
unix 204 98 5 NATIVE NATIVE
irfanview 197 106 104
oracle 172 170 2
sap 172 170 0
mac_os_x 101 69 19
mediawiki 100 75 73 PHP PHP
desktop 99 8 8
kubernetes 96 58 45
ach_services 93 19 4
web_services_proxy 92 92 0
mongodb 92 51 48
check_services 88 16 2
cps_services 84 18 1
.net 74 35 27 DOTNET DOTNET
hp-ux 73 26 0
visual_studio_code 72 71 66
linux_kernel 70 35 11 NATIVE NATIVE
windows_server_2003 68 22 0
windows_server_2008 68 22 0
postgresql 68 68 67
vmware 67 20 9
7-mode 66 65 1
craft_cms 65 35 35 PHP PHP
redhat_enterprise_linux 61 22 0 NATIVE NATIVE
puppet 59 11 11
laravel 58 16 15 PHP
nginx 56 16 4
confluence 55 25 17
vbulletin 53 9 9 PHP
suse_linux 52 13 0 NATIVE NATIVE
internet_explorer_11 51 51 0
apache 47 7 5
mac_os 44 34 5
jira 43 39 32
mac 39 27 7
vmware_vcenter 38 36 5
nextcloud 37 22 22 PHP
ipados 37 37 0 IOS
docker 36 30 21
x64 35 35 4
solaris 35 21 3
x86 34 22 1
c# 34 14 13 DOTNET DOTNET
mybb 31 31 31 PHP PHP
easy_digital_downloads 31 31 0
activematrix_bpm 31 24 0
vcenter 31 31 2
foxit_reader 31 20 0
phantompdf 31 20 0
aws 30 25 5
java 28 23 12 JAVA JAVA
freebsd 28 16 2
clustered_data_ontap 27 24 1
sunos 27 14 0
exchange_server 27 15 1
aws_marketplace 27 18 0
pypi 27 24 24 PYTHON
pimcore 27 23 23 PHP
aix 25 10 2
ios 22 13 0 IOS IOS
c/c++ 21 21 13 NATIVE NATIVE
vmax 20 6 3
azure 20 17 9
silver_fabric 20 11 0
swift 20 19 18 IOS
firefox 19 7 3
hyper-v 19 14 0
windows_server 18 11 0
freepbx 18 5 4
glpi 18 16 16 PHP
mattermost 18 16 14
ipad_os 17 17 0
xoops 15 8 8 PHP
minecraft 15 8 8
discourse 15 15 15
domino 14 10 0
ec-cube 14 8 8
bitbucket 14 9 2
commerce 14 14 0
z/os 13 7 2
joomla 13 13 13 PHP PHP
visual_studio 13 11 8
openeuler 13 8 8
chromium 12 12 12
magento 12 11 11 PHP
e-business_suite 12 5 2
c++ 12 12 7 NATIVE
jquery 11 11 6 JAVASCRIPT JAVASCRIPT
django 11 11 11 PYTHON
sharepoint_server 11 8 0
lenovo_vantage 11 5 4
saas 11 11 1
rails 11 11 10 RUBY
microsoft_exchange 10 9 5
aws_with_multi-tenancy 10 10 0
silverstripe 10 6 5 PHP
microsoft_azure 10 7 0
eclipse 10 10 4
pro 9 9 8
rapid7 9 9 9
php 8 8 6 PHP PHP
moodle 8 8 8 PHP PHP
azure_internet_of_things 8 5 4
finance_and_operations 8 6 6
swift_services 8 8 8
universal 8 8 0
line 8 8 8
openfga 8 8 0
netware 7 7 0
enterprise 7 6 3
basercms 7 7 5
mantisbt 7 5 5 PHP
digital_payments 7 5 1
modoboa 7 7 7
shoplazza 7 7 7
atak 7 7 7
ubuntu 6 6 3
windows_7 6 5 2
grafana 6 6 6
middleware 6 6 0
opencart 6 6 6 PHP
emc 6 6 0
beckhoff_cx 6 6 0
fastify 6 5 5 NODEJS
elixir 6 6 6 ELIXIR
xwiki 6 6 5
checkmk 6 6 6
woocommerce 5 5 2 PHP
jetbrains 5 5 5
munkireport 5 5 5
isagraf6_workbench 5 5 0
openshift 5 5 3
luckyframe 5 5 5
gnu_emacs 5 5 1
fadc 5 5 0
fadci 5 5 0

@Akhil-1527

Copy link
Copy Markdown
Author

Here's step 3, the scans. Short version: everything that drops is a CVE for another language, the branch picks up 10 real findings that main misses, and it loses 3 real ones in two ways I go into below.

I built the CLI from main (b801bff) and from this branch with the reworked change from my last comment (not pushed yet). Each build loaded the same NVD 2.0 feeds into its own database, then both scanned the same files offline with the same options: experimental analyzers on, and off for OSS Index, Central, the npm, yarn and pnpm audits, KEV, RetireJS, bundle audit and mix audit, plus the .NET assembly and Go module analyzers since there's no dotnet or go on this machine.

What I scanned:

I picked most of the manifest packages because their products have CVEs for more than one language, so this hits the affected products much more often than a normal project would.

NVD findings across all samples:

main branch
default suppressions (base file plus the hosted snapshot in the jar) 828 819
no suppressions 2,869 2,857

For the second row I swapped both packaged suppression files for a single rule that never matches.

Dropped. I checked each one against its NVD entries, and each matched only through an entry for another language.

dependency CVE target_sw it matched on
grpc-core and grpc-netty-shaded 1.71.0 CVE-2026-33186 go
avro-ipc 1.5.0, Apache.Avro 1.10.2 (NuGet) CVE-2023-37475 (hamba/avro) go
msal4j 1.23.1 CVE-2024-35255 node.js and .net (the java entry ends before 1.15.1)
opcua 0.98.12 (PyPI) CVE-2022-25888, CVE-2022-25903 rust
snowflake-connector-python 2.7.0 CVE-2025-24788, CVE-2025-46326 .net
mechanize 0.4.5 (PyPI) CVE-2022-31033 ruby
i18n 0.7.0 (gem) CVE-2020-7791 asp.net
jmespath 1.6.0 (gem) CVE-2026-54133 php
OpenTelemetry 1.5.0 (NuGet) CVE-2026-39882, CVE-2026-41178 go
CVE-2026-44967 c++
CVE-2026-54285 node.js
apm-agent-attach 1.28.1 (the jar, its shaded apm-agent-common and the embedded elastic-apm-agent.jar) CVE-2019-7617 python

The APM one only shows with suppressions removed. By default the base suppression added for #4168 hides it on both builds. Its note says it's there because target_sw isn't supported, so it can go once this is in. I'll leave it alone unless you want it removed here.

Added, all real:

  • fastify-multipart 5.3.0 and @fastify/multipart 7.4.0 (npm): CVE-2021-23597, CVE-2023-25576, CVE-2026-18549 and CVE-2026-19474, 7 findings. NVD uses fastify as the target for these, and the Node.js check on main only keeps node.js spellings.
  • avro-ipc 1.5.0: CVE-2023-39410, CVE-2024-47561 and CVE-2025-33042, all for the Avro Java SDK. Main never puts apache:avro on the jar. The Java CVEs have - targets and CVE-2021-43045 (the .NET SDK) has .net, so the cache labels the whole product DOTNET and CPEAnalyzer.filterEcosystem hides it from Java. On the branch the three Rust SDK CVEs make it MULTIPLE, so it comes back, but only by luck.

Lost, both real:

  1. ckeditor/ckeditor 4.14.0 from a composer.lock: CVE-2021-32808 and CVE-2021-32809. NVD only has node.js entries for them, and the Composer package ships the same JavaScript, so they apply. This is the wrapped artifact case from your review, @chadlwilson. I think the answer there is detecting the JavaScript inside the package rather than loosening this check, so I've left it.
  2. pubnub 7.2.0 (PyPI): CVE-2023-26154. NVD puts the pubnub SDKs for several languages under pubnub:pubnub, with one go entry and the rest -. Once go maps to GOLANG, the cache labels the whole product GOLANG and the CPE filter drops it for everything else (the npm pubnub 7.3.0 in the samples loses the CPE too, though it has no CVE in range). It's the same thing that hides apache:avro on main.

Of the 512 products that get a label for the first time, 23 have CVEs with a * or - entry for the same product (65 CVEs). Most are Go or Rust projects where the wildcard entry is just less specific (canonical:juju, navidrome:navidrome, parity:frontier, golang:net, golang:crypto, gofiber:fiber), so the label is right for them. pubnub:pubnub is the clear exception. microsoft:azure_uamqp may be another, since its c entry labels it NATIVE and the Python uamqp package builds that C library.

I looked at counting * and - entries as any language when building the cache, which would fix both pubnub and avro. But on main 15,576 of the 29,198 products with a single ecosystem also have wildcard entries, so it would undo a lot of filtering that works today, and I'd rather not fold that into this PR. Are you OK leaving these two cases as they are for now, or would you rather I handle them here?

Everything else:

  • No Node.js dependency picked up anything through windows or another unmapped target, so that part of the change didn't show up here.
  • The webjars come out the same on both builds, so dropping the webjar exception costs nothing here. That's weak evidence though, since only 2 of the 43 get a CPE at all.
  • Go: only the Gopkg.lock path ran. Its CPE versions keep the leading v (yaml_project:yaml:v2.2.7), so nothing matched on either build. That's unrelated to this change.
  • cache4k-jvm ([FP]: Rust vulnerability CVE-2020-36448 has resurfaced, this time on dependency cache4k-jvm, even with latest (10.0.3) Dependency Check version #6851): main's database already labels cache_project:cache RUST, so neither build puts it on the jar. With suppressions removed it still gets 4 GitHub Enterprise CVEs through a github:github CPE, and the APM jar still gets CVE-2023-46669 and CVE-2024-52976 through elastic:elastic_agent. Both builds report those the same way, and the default suppressions hide them.
  • With suppressions removed, a few CPEs come or go without changing any finding. yaml_project:yaml goes from NODEJS to MULTIPLE, so it now also lands on snakeyaml 2.2, PyYAML 3.12 and gopkg.in/yaml.v2. protobuf:protobuf, whose entries target go, no longer lands on protobuf-java 2.5.0, and web_project:web no longer lands on System.Text.Encodings.Web 4.5.0.
NVD findings by sample
sample main branch main, no suppressions branch, no suppressions
24 jars 199 198 209 208
apm-agent-attach 1.28.1 0 0 9 6
cache4k-jvm 0.13.0 0 0 4 4
43 webjars 0 0 1 1
npm 64 71 64 71
PyPI 29 23 102 96
gems 70 68 70 68
Go 0 0 0 0
NuGet and .NET 19 14 43 38
Composer 446 444 2,366 2,364
Swift 1 1 1 1
total 828 819 2,869 2,857

I can share the manifests and the diff script if anyone wants to rerun this. If the above looks fine, I'll push the code next and rewrite the PR description.

@mjatin2007

Copy link
Copy Markdown
Contributor

Thanks for the detailed scans, @Akhil-1527. Tagging @chadlwilson and
@neilscallywag because this bears on the cache/runtime split discussed above.
The results surface two different false-negative mechanisms. My view is that the
three findings you identify as real losses should not be treated as acceptable final
behavior yet, although the scope and merge trade-off are ultimately the
maintainers' call.

A feed-level check against a saved NVD annual-feed set found vulnerable
application entries for all 26 CVEs named in your comment. Their CPE criteria,
target_sw values, and version ranges are consistent with the table in your
comment. The default and no-suppression totals also reconcile arithmetically with
the listed additions and removals. Those are consistency checks, not a
reproduction of either scan or the artifact-level classifications.

The two mechanisms seem to need different treatment:

  • The reported PubNub and Avro behavior points to the vendor/product cache.
    pubnub:pubnub mixes one go range with several - ranges in the same CVE,
    so collapsing the pair to GOLANG can exclude the other SDKs. In the Avro case,
    your results show the Java findings missing on main while the pair is DOTNET
    and returning only after the Rust entries make it MULTIPLE, not because the
    - entries are recognized as applicable.
  • CKEditor is a later applicability problem. In the case you report, the
    Composer package ships the affected JavaScript even though NVD identifies
    node.js as the operating environment. A mapped target differing from the
    package ecosystem is therefore not, by itself, enough evidence to discard a
    finding.

A narrower middle path for this PR could be to leave the persisted
vendor/product classification unchanged and use any new target mappings only in
a separate, non-mutating analyzer-stage decision. I would drop a candidate only
when there is affirmative evidence that every version- and configuration-relevant
application alternative is incompatible with the actual artifact/runtime.
Inferring runtime only from the package ecosystem would not count as that
evidence. Wildcard, -, unmapped, ambiguous, and wrapper/container cases should
remain fail-open. With the existing cache classification preserved, this should
avoid introducing the reported PubNub loss and two CKEditor losses if the pushed
code follows those rules. It would not recover Avro-style candidates already
excluded by the cache; that needs separate cache-state and rebuild/migration
work.

For the narrow path, the highest-value regression questions seem to be whether
the Fastify additions survive, whether PubNub PyPI and CKEditor Composer remain
retained, whether at least one justified cross-language rejection still occurs,
and whether both dependency orders leave shared cached vulnerability state
unchanged. Avro Java should document the remaining early-cache limitation rather
than be credited as a stable gain from MULTIPLE.

Because you note that the scanned implementation is not pushed yet, could you
identify the exact baseline and scanned commit SHAs when you push it, and share
manifests or minimal fixtures with resolved versions, the scan/diff/inventory
commands or scripts and relevant configuration (including both suppression modes
and the starting cache state), and the NVD snapshot identity/checksums? That
would provide a basis for independently reproducing the reported outcomes. I
would leave the earlier count discrepancy aside for now because it does not
decide the safety question; the inventory script can align the definitions
later.

So I would not leave the reported losses as accepted behavior, but I also would
not fold the full cache redesign into this PR. The runtime-only scope above seems
like the safer middle path if the pushed code and regression tests support it.

@neilscallywag

Copy link
Copy Markdown

I would prefer to talk to humans. The agents you two are using seem to be missing the point

@chadlwilson

Copy link
Copy Markdown
Collaborator

I would prefer to talk to humans. The agents you two are using seem to be missing the point

Welcome to my world. 🤦‍♂️

@chadlwilson

Copy link
Copy Markdown
Collaborator

Not that the above isn't helpful (and an impressive amount of token commitment!) but it is overwhelming text vomit, and is indeed missing some of the wider points summarised at an appropriate level of abstraction before drilling down into immense detail.

@neilscallywag

Copy link
Copy Markdown

@chadlwilson it might be worth adding some form of agents.md or equivalent

@chadlwilson

Copy link
Copy Markdown
Collaborator

@chadlwilson it might be worth adding some form of agents.md or equivalent

I'm not a maintainer (just a contributor & triager) so I don't make these calls. Otherwise I'd have a very different code of conduct, personally 😁

@neilscallywag

neilscallywag commented Oct 15, 2026 •

Copy link
Copy Markdown

Anyways, I wanted to check if this is an intentional behaviour that I noticed during some testing

Let's say some java dependency has a cpe X

We call,

CveDB.getVulnerabilities(cpe)

the first time vulnerabilities are looked up for this CPE in the DB, we get something like

[A,B,C]

CveDB then stores this result in its in memory cache

CveDB.vulnerabilitiesForCpeCache

vulnerabilitiesForCpeCache[X] = [A,B,C]

So when
NvdCveAnalyzer.analyzeDependency() calls CveDB.getVulnerabilities(), the cache returns the exact object stored which is passed directly into NvdCveAnalyzer.filterEcosystem() which then mutates it. So, filtering one dependency could affect the vulnerability data seen by another java dependency that uses the same CPE, which a simple test seem to confirm. It does not seem to be intended and in general I don't think mutating lists retrieved from other sources is a good practice.

@chadlwilson

chadlwilson commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Im pretty sure that mutation is unintentional as its logically inxorrect. I read that above in the initial PR when reviewing.

There are many such problems in ODC with intentionally mutable and unintentionally mutable collections, alas.

I'll probably cherry pick, amend and submit a bugfix PR independent of this, as theres no need to conflate these.

@chadlwilson

Copy link
Copy Markdown
Collaborator

This is a mess. It was force-pushed over the top of the earlier changes and lost all the earlier reviewed actual production code, and thus all basis for earlier conversation and work put in by real humans.

I'm closing this, as this PR is being used as some step 1 / step 2 / step 3 scratchpad by a slop-bot that is now completely out of sync with its title and PR description. That's completely opaque, and an absolutely ridiculous way to try and work with OSS. Don't force push over a PR and turn it into something completely different.

If you want to create tests alone, open a new PR that does this. Then stack another (draft) PR on top of it if you're trying to make things independently reviewable and note that it's to be reviewed only after the first one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core changes to core tests test cases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: use CPE target_sw to populate ecosystem to reduce FP

4 participants