Skip to content

grouper.Group misses transitive alias relations when the linking record comes last #3107

Closed

Description

@lenamonj

grouper.Group (internal/grouper/grouper.go) merges two records that share an alias by writing groups[i] = min(groups[i], groups[j]) against the two indices, not against the groups those indices already belong to. When the record that links two existing groups comes after both of them, only the later index is re-pointed and the earlier group's other members stay behind, so records that describe one vulnerability are reported as two groups.

Reproduction against main at f0e28d3, in internal/grouper:

func TestGroupTransitive(t *testing.T) {
	got := grouper.Group([]grouper.IDAliases{
		{ID: "GHSA-aaaa", Aliases: []string{"CVE-1"}},
		{ID: "GHSA-bbbb", Aliases: []string{"CVE-3"}},
		{ID: "GHSA-cccc", Aliases: []string{"CVE-2", "CVE-3"}},
		{ID: "GHSA-dddd", Aliases: []string{"CVE-1", "CVE-2"}},
	})
	if len(got) != 1 {
		t.Fatalf("want 1 group, got %d: %v", len(got), got)
	}
}

Expected: one group holding all four IDs, since aaaa and dddd share CVE-1, cccc and dddd share CVE-2, and bbbb and cccc share CVE-3.

Actual: two groups, [GHSA-aaaa GHSA-cccc GHSA-dddd] and [GHSA-bbbb].

Where it shows: pkg/osvscanner/vulnerability_result.go builds pkg.Groups from this function, so every output that reads Groups reports one vulnerability as two groups whenever the alias sets overlap in a chain rather than pairwise, which is the shape distribution advisories produce once their upstream CVEs are folded into aliases (#2027). It is order dependent: the same four records in a different order group correctly.

Activity

  1. G-Rath commented on Sep 26, 2026

    @G-Rath
    Collaborator

    Do you have a reproduction with real world advisories using the CLI or library?

  2. lenamonj commented on Sep 27, 2026

    @lenamonj
    Author

    Yes. Four Ubuntu Pro livepatch advisories for linux, fetched from api.osv.dev/v1/vulns/ and passed through the same two calls pkg/osvscanner/vulnerability_result.go makes:

    vulns := fetch("LSN-0095-1", "LSN-0096-1", "LSN-0097-1", "LSN-0102-1")
    groups := grouper.Group(grouper.ConvertVulnerabilityToIDAliases(vulns))
    // got:  [LSN-0095-1 LSN-0096-1 LSN-0097-1] [LSN-0102-1]
    // want: one group. LSN-0097-1/LSN-0096-1 share CVE-2023-31248,
    //       LSN-0096-1/LSN-0095-1 share CVE-2023-1380,
    //       LSN-0095-1/LSN-0102-1 share CVE-2023-1872.
  3. G-Rath commented on Sep 27, 2026

    @G-Rath
    Collaborator

    That's not a reproduction though that uses the public API

  4. lenamonj commented on Sep 27, 2026

    @lenamonj
    Author

    Debian linux 5.10.46-4 through the CLI, the v2.6.0 release binary and a build of main at f0e28d3 printing the same lines, OSV data as of 2026-09-27 10:10 UTC:

    $ cat sbom.cdx.json
    {"bomFormat":"CycloneDX","specVersion":"1.5","version":1,"components":[{"type":"library","name":"linux","version":"5.10.46-4","purl":"pkg:deb/debian/[email protected]"}]}
    $ osv-scanner scan source -L sbom.cdx.json --format json > out.json
    $ jq -c '.results[].packages[] | [(.vulnerabilities|length), (.groups|length)]' out.json
    [17348,14451]
    $ jq -c '.results[].packages[].groups[] | select(.aliases | index("CVE-2024-58096")) | [(.ids|length), (.ids|index("DSA-6126-1") != null), (.ids|index("DEBIAN-CVE-2024-58096") != null)]' out.json
    [2388,true,false]
    [40,false,true]
    

    DSA-6126-1 and DEBIAN-CVE-2024-58096 both list CVE-2024-58096 in upstream, which ConvertVulnerabilityToIDAliases folds into aliases, so by the scanner's own rule they share an alias, yet the scan puts that alias in two groups, one holding each record. The same scan with the fix applied gives 14449 groups, and the second query prints [2663,true,true].

  5. self-assigned this
    on Sep 28, 2026
  6. added a commit that references this issue on Oct 12, 2026
    a4d0a04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions