Skip to content

Add Root IO vulnerability qualifier support#3137

Open
chait-slim wants to merge 8 commits intoanchore:mainfrom
chait-slim:feat/rootio-support
Open

Add Root IO vulnerability qualifier support#3137
chait-slim wants to merge 8 commits intoanchore:mainfrom
chait-slim:feat/rootio-support

Conversation

@chait-slim
Copy link
Copy Markdown

Adds support for Root IO package qualification to enable the NAK
(Negative Acknowledgment) pattern, where vulnerabilities with Root IO
qualifier only match Root IO patched packages.

Implementation:

  • Add RootIO field to PackageQualifiers in grype/db/v6/blobs.go
  • Increment DB schema Addition to 6.1.4 with changelog
  • Add rootio qualifier implementation in grype/pkg/qualifier/rootio/
  • Wire up qualifier in vulnerability.go toPackageQualifiers()

The Root IO qualifier ensures that Root IO vulnerabilities (from
api.root.io/external/osv) only match packages with rootio- prefix,
preventing false matches against standard upstream packages.

  Adds support for Root IO package qualification to enable the NAK
  (Negative Acknowledgment) pattern, where vulnerabilities with Root IO
  qualifier only match Root IO patched packages.

  Implementation:
  - Add RootIO field to PackageQualifiers in grype/db/v6/blobs.go
  - Increment DB schema Addition to 6.1.4 with changelog
  - Add rootio qualifier implementation in grype/pkg/qualifier/rootio/
  - Wire up qualifier in vulnerability.go toPackageQualifiers()

  The Root IO qualifier ensures that Root IO vulnerabilities (from
  api.root.io/external/osv) only match packages with rootio- prefix,
  preventing false matches against standard upstream packages.

Signed-off-by: Chai Tadmor <chai.tadmor@root.io>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
    Bumps MatchesSchemaVersion to 1.1.4 to reflect the addition of the
    RootIO field in PackageQualifiers (an ADDITION — backward-compatible
    optional field).

Signed-off-by: Chai Tadmor <chai.tadmor@root.io>
@chait-slim chait-slim force-pushed the feat/rootio-support branch from 441f80e to 2b96fcb Compare April 6, 2026 15:33
for _, rel := range relatedEntries.Related {
aph, ok := rel.(db.AffectedPackageHandle)
require.True(t, ok, "related entry should be AffectedPackageHandle")
require.NotNil(t, aph.Package, "Package should not be nil")
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.

Can you make these assertions specific? "not empty" type tests are easy to pass with an incorrect value.

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.

Also, I expected that rootio data would make an unaffected package handle, not an affected package handle.

@willmurphyscode
Copy link
Copy Markdown
Contributor

Hi @chait-slim I spent some time testing this out this week. I think it's missing changes to actual matching behavior. In other words, this change puts new rootio records in the database, but nothing queries for them.

I think probably what we want to:

  1. At the top of https://github.com/anchore/grype/blob/main/grype/matcher/dpkg/matcher.go#L39 , ask, "are we scanning a rootio package" and if so jump into a new matcher that
  2. Trims the rootio-prefix from the package name and searches the appropriate ubuntu vulns
  3. Searches rootio for appropriate unaffected packages, and subtracts fixes
  4. Trims the rootio-prefix from the upstream package's name and search the appropriate ubuntu vulns
  5. Search rootio for appropriate unaffected packages on the upstream, and subtracts fixes
  6. Returns the matching results of these.

We will probably need to do something similar in the APK matcher and other supported rootio ecosystems.

… model

  Rootio OSV records were stored in the DB as AffectedPackageHandle with a
  rootio qualifier, but nothing in the matching pipeline queried them — making
  the integration a no-op at scan time. Two additional issues: the data model
  was semantically wrong (rootio records are NAKs, not disclosures), and the
  ecosystem prefix conventions were incorrect.

  - Emit UnaffectedPackageHandle for rootio records in the transformer;
    rootio data represents backported fixes, not new vulnerability disclosures
  - Add two-pass distro matching for rootio OS packages (dpkg, apk): strip
    the rootio- prefix to search upstream Debian/Alpine/Ubuntu vulns, then
    query rootio unaffected records by the original prefixed name and subtract
    any fixes — applied to both direct and upstream packages
  - Add bare-name search for rootio language packages (NPM, PyPI, Java) in
    the language matcher so upstream CVE records stored without the rootio
    prefix are also found
  - Fix PyPI prefix: rootio_ (underscore) per PEP 503, not rootio- (hyphen)
  - Add Java/Maven detection: io.root. groupId prefix
  - Fix NPM StripPrefix to reconstruct scoped packages correctly
    (@rootio/babel__core → @babel/core)
  - Replace weak NotEmpty assertions with specific per-fixture checks:
    package name, CVE alias, fix version constraint, RootIO qualifier, >= range

Signed-off-by: Chai Tadmor <chai.tadmor@root.io>
@chait-slim chait-slim force-pushed the feat/rootio-support branch from 51a2baf to 72d9a4e Compare April 9, 2026 17:51
Copy link
Copy Markdown
Contributor

@willmurphyscode willmurphyscode left a comment

Choose a reason for hiding this comment

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

Thanks for putting some more work into this! It's much closer. I have quite a few inline comments, but we're moving in the right direction.

I do think that anchore/vulnerability-match-labels#167 could use some more images and a lot more labels. I left instructions at https://github.com/anchore/vunnel/pull/963/changes#r3065955122 about why some matches aren't showing up in the diff to be labeled.

@@ -34,34 +35,59 @@ func (m *Matcher) Type() match.MatcherType {
}

func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {
if rootio.IsRootIOPackage(p) {
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.

We don't have adequate test coverage for this change.

  1. Can we get an Alpine-based test image and labels added to Add Root IO provider labels for ubuntu test image vulnerability-match-labels#167 ?
  2. This needs a to be well unit tested.

@@ -37,6 +38,10 @@ func (m *Matcher) Type() match.MatcherType {
}

func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {
if rootio.IsRootIOPackage(p) {
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.

Changes to matchers need good unit tests.

return nil, nil, fmt.Errorf("matcher failed to fetch rootio naks distro=%q pkg=%q: %w", p.Distro, p.Name, err)
}

remaining := disclosures.Remove(naks)
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.

This depends on aliases, but in my testing the aliases seem to be missing from the rootio unaffected package handles, so nothing gets removed. For example, in the ubuntu 22 test images from the vuln match labels PR has rootio-libgcrypt20 at 1.9.4-3ubuntu3.root.io.2 in it, which is listed as fixed at https://osv.dev/vulnerability/ROOT-OS-UBUNTU-2204-CVE-2024-2236, but grype built from this branch finds CVE-2024-2236 on that package anyway.

return ""
}

ecosystemLower := strings.ToLower(ecosystem)
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.

I think I'd prefer to use a narrower change here. There are a lot of changes in this file that affect all OSV providers, but are implemented for RootIO. I think it makes sense to instead try to have something like:

if isRootIO {
    return handleRootIO(...)
}

pretty early in the OSV transformers. This helps everyone out: other OSV providers' bugs can't break RootIO (and vice versa) and reviewers don't have to spend much focus on whether changes to shared parts of the transformer will break other providers.

// Check if this is an advisory record
if isAdvisory {
// For advisory records, emit unaffected packages
if isAdvisory || isRootIO {
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.

https://github.com/anchore/vunnel/pull/963/changes#diff-e3183aa5564d1be5757075e9114b9ca21572612f5cb34afdb6a08f0091dae127R132 should be marking this as advisory type records. I think maybe your test data in this repo is missing that normalization.

aliases := vulnerability.Aliases

if isAdvisory {
aliases = append(aliases, vulnerability.Related...)
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.

I think the origin of the bug causing FPs (see https://github.com/anchore/grype/pull/3137/changes#r3065870827 ) is that the Upstream and Related vulnerabilities on your OSV data aren't being added to the aliases here, so the resultSet.Remove call is skipping them (Because ROOT-OS-UBUNTU-2204-CVE-2024-2236 is not the same as CVE-2024-2236 and there's no entry added to the alias table).

The fix for this should probably be basically to around line 36 say, "this is a rootio advisory and we are going to go transform it in handleRootIO(...)". I think that will make this change safer and easier to reason about.

@@ -17,7 +18,20 @@ func MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcher
var matches []match.Match
var ignored []match.IgnoreFilter

for _, name := range store.PackageSearchNames(p) {
searchNames := store.PackageSearchNames(p)
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.

Changes to matchers need good unit tests.

@@ -0,0 +1,52 @@
{
"schema_version": "1.6.1",
"id": "ROOT-APP-NPM-CVE-2022-25883",
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.

It would be wonderful to get npm and pypi test cases added to anchore/vulnerability-match-labels#167 and we need npm and pypi records at least handled by unit tests in this repo.

…cher unit tests

  The core false-positive bug was a two-part alias gap:

  1. transform.go: `Related` CVE IDs were only appended to the
     VulnerabilityHandle aliases for advisory records, not RootIO records.
     Changed `if isAdvisory` → `if isAdvisory || isRootIO` so upstream
     CVE IDs (e.g. CVE-2024-2236) are included in aliases for rootio
     vulnerability handles.

  2. getUnaffectedPackages was passing vuln.Aliases (the original field)
     to getUnaffectedBlob, not the augmented alias list that includes
     Related IDs. NAK PackageBlob.CVEs were therefore missing the upstream
     CVE, causing disclosures.Remove(naks) to find no identity overlap and
     skip suppression. Fixed by threading the augmented aliases through
     getUnaffectedPackages/getUnaffectedBlob.

  Also update test fixtures to include the vunnel advisory-type
  normalization (`database_specific.anchore.record_type: advisory`) that
  real pipeline data carries, so both isAdvisory and isRootIO are true in
  production conditions.

  Unit tests added:
  - TestRootIORelatedAliases: asserts Related CVE IDs appear in both
    VulnerabilityHandle.BlobValue.Aliases and
    UnaffectedPackageHandle.BlobValue.CVEs
  - TestMatcherApk_RootIOPackage: below-fix, at-fix, above-fix cases
  - TestMatcherDpkg_RootIOPackage: below-fix and at-fix cases
  - TestMatchPackageByLanguage_RootIONPM: stripped-name search finds
    upstream CVEs; NAK produces ignore filters covering the upstream CVE
  - TestMatchPackageByLanguage_RootIOPyPI: stripped-name search via
    rootio_requests → requests; PEP 440 local version >= upstream fix

Signed-off-by: Chai Tadmor <chai.tadmor@root.io>
  Python packages use MatchPackageByEcosystemAndCPEs (not MatchPackageByLanguage),
  which lacked the rootio prefix-stripping logic. Without it, grype searched for
  "rootio-jinja2" instead of "Jinja2", finding no CVE records and thus never
  triggering the NAK suppression.

  Also accept "rootio-" (hyphen) as a Python package prefix in qualifier.go, since
  PEP 426 normalizes "rootio_jinja2" to "rootio-jinja2" on install.

Signed-off-by: Chai Tadmor <chai.tadmor@root.io>
@chait-slim
Copy link
Copy Markdown
Author

@willmurphyscode

  1. Fixed all comments
  2. Added a bunch of images and Root FPs as labels for different ecosystems (npm,alpine,python)

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

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

3 participants