Skip to content

fix: match NuGet and crates.io package names case-insensitively - #6112

Open
kanokwut wants to merge 5 commits into
google:masterfrom
kanokwut:match-nuget-crates-names
Open

kanokwut wants to merge 5 commits into
google:masterfrom
kanokwut:match-nuget-crates-names

Conversation

@kanokwut

@kanokwut kanokwut commented Oct 1, 2026 •

Copy link
Copy Markdown

Overview

NuGet package IDs are case-insensitive, and crates.io treats crate names case-insensitively with - and _ as equivalent, but the query API matches these names exactly. Valid spellings therefore miss advisories today. For example, Microsoft.OpenApi.Kiota@1.31.0 does not match CVE-2026-59862 because GHSA-7f3j-j7jj-r3vr spells the package two ways, and lol_html does not match GHSA-c3x7-354f-4p2x, which spells it lol-html.

This makes NuGet and crates.io names match case-insensitively, as agreed in #6039, without changing any published record. npm and Packagist are unchanged.

Fixes #6039

Details

The change only affects matching. Reusing PackageNameNormalizer would also rewrite the published names, since the namenormalize enricher writes them back into the record. For NuGet that would rename 1,828 of 1,910 records and break exact-name consumers such as osv-scalibr's offline matcher, so this adds a separate matching path instead.

  • MatchNameNormalizer and ecosystem.NormalizeMatchName return the name used to index and query a package. NuGet lowercases the ID, and a new cratesIOEcosystem also maps _ to -. Only ASCII letters are folded, so lookalike IDs with characters such as U+0130 or U+212A stay distinct, and invalid UTF-8 is kept as is.
  • computeAffectedVersions also writes a copy of a package's AffectedVersions rows under its match name when that differs. The original rows are unchanged.
  • MatchPackages folds the name when an ecosystem is given and the API runs with OSV_NORMALIZE_MATCH_NAMES=true. Queries without an ecosystem still match the name as written, and they can also hit the copied rows, the same way they already match any ecosystem's package with the same name.
  • npm deliberately does not implement MatchNameNormalizer, and the interface doc explains why. Echo:NuGet and TuxCare are unchanged, since their wrappers do not forward it.

Rollout

The query change is gated behind OSV_NORMALIZE_MATCH_NAMES on the API, which is off by default:

  1. Deploy. The worker starts writing the copied rows. With the variable off, queries are normalized as before, so the only new matches are for spellings that equal the match name, such as newtonsoft.json.
  2. Reprocess NuGet and crates.io records so that each one gets its copied rows.
  3. Set OSV_NORMALIZE_MATCH_NAMES=true on the API.

If the variable is set before the reprocess finishes, ecosystem queries miss records that have no copied rows yet, even with the exact published spelling. That affects about 96% of NuGet records, including MAL ones. The docs change in post-v1-query.md describes the behavior with the variable on.

Other notes:

  • The extra rows add about 13,700 AffectedVersions entities. The largest record grows from 297 to 591 rows, about 200 KB per write.
  • osv-scanner's getNextFixVersion compares names exactly, so for the newly matched spellings it shows the vulnerability without a fix version. That is not a regression, since these queries returned nothing before.
  • Smoke tests after rollout: Microsoft.OpenApi.Kiota@1.31.0 should return GHSA-7f3j-j7jj-r3vr, and Newtonsoft.Json@12.0.1 should still return GHSA-5crp-9r3c-p9vr.

Testing

  • Added tests for NormalizeMatchName, the copied rows in computeAffectedVersions, published names staying unchanged in the namenormalize enricher, and MatchPackages on the Datastore emulator with the variable on and off, covering case variants, - and _, versions, npm, and queries without an ecosystem.
  • go test ./... in go/ passes with the Datastore emulator.
  • go vet and gofmt -s report no issues, and golangci-lint v2.13.0 reports no new issues on the go module.

This change was prepared with AI assistance.

Generated with Claude Code: https://claude.com/claude-code

@michaelkedar michaelkedar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for the delay in reviewing this

Comment thread go/osv/ecosystem/ecosystem.go Outdated
Comment thread go/internal/database/datastore/models.go Outdated
Comment thread go/osv/ecosystem/tuxcare.go Outdated
@kanokwut
kanokwut requested a review from michaelkedar October 20, 2026 05:53

@michaelkedar michaelkedar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, but one more thing

}
if sys, ok := provider.Get(ecosystem); ok {
name = eco.NormalizePackageName(sys, name)
name = eco.NormalizeMatchName(sys, name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just to make rollover easier on our end, can you gate this change behind an environment variable or something so we can enable this API change after we've migrated all the data

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, the API now only uses the match name when OSV_NORMALIZE_MATCH_NAMES is set to true, and it's off by default. Until then, queries are normalized the same way as before. The worker still writes the extra rows, so once NuGet and crates.io have been reprocessed, setting the variable turns it on. I also rebased onto master, since the API setup changed there.

NuGet package IDs are case-insensitive, and crates.io treats crate names
case-insensitively with '-' and '_' as equivalent. Add
PackageNameMatcher and MatchPackageName for names that should match
without being rewritten: NuGet lowercases the ID, and crates.io also
maps '_' to '-'. Only ASCII letters are folded, so lookalike IDs that
the registry keeps distinct stay distinct, and invalid UTF-8 is kept as
is.

computeAffectedVersions now also writes a copy of a package's
AffectedVersions rows under its match name when that differs, marked
match_only. Queries skip match_only rows for now, so this commit does
not change any query results. Published records are unchanged, since
the namenormalize enricher still only applies PackageNameNormalizer.

Once NuGet and crates.io records have been reprocessed, queries can use
the match name.

Refs google#6039
Fold the package name with MatchPackageName when a query gives an
ecosystem, and let such queries use the match_only rows, so that a
NuGet ID matches in any case and a crate name matches in any case with
either '-' or '_'. For example, Microsoft.OpenApi.Kiota@1.31.0
currently misses GHSA-7f3j-j7jj-r3vr, which spells the package two
ways, and lol_html misses GHSA-c3x7-354f-4p2x, which spells it
lol-html.

Queries without an ecosystem still only match the package name. NuGet
and crates.io records need to be reprocessed with the previous commit
before this is deployed, or ecosystem queries miss records that have no
match_only rows yet.

Fixes google#6039
Rename the interface to MatchNameNormalizer, and its method and the
package-level helper to NormalizeMatchName, since they normalize a name
rather than match it. Move unwrap from tuxcare.go to ecosystem.go, now
that the name normalizers use it.

unwrap was the only part of tuxcare.go that dhi.go does not have, so
without it dupl reports the two files as duplicates. Mark both with
nolint:dupl, since each delegates to an ecosystem resolved on demand.
The flag only kept queries without an ecosystem, which do not fold the
name, from matching the rows under the match name. Such queries already
match any ecosystem's package with the same name, so they can match
those rows too, and the AffectedVersions models stay unchanged.

Build the rows under the match name in one place by copying the
package's rows once they are computed.
MatchPackages now only uses NormalizeMatchName when the store is created
with NormalizeMatchNames, which the API and its devserver set from the
OSV_NORMALIZE_MATCH_NAMES environment variable. It is off by default, so
the API keeps normalizing queries as before until NuGet and crates.io
records have been reprocessed.
@kanokwut
kanokwut force-pushed the match-nuget-crates-names branch from 1e6e047 to 8d71063 Compare October 22, 2026 06:11
@kanokwut
kanokwut requested a review from michaelkedar October 22, 2026 06:12

This branch has not been deployed

No deployments
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.

OSV.dev query API returns no advisory for a valid non-canonical package name (false negative across NuGet, crates.io, npm, and Packagist)

2 participants