SARIF output emits one result per matched advisory in an alias group, creating duplicate code scanning alerts
- Dominant language
- Go
- Stars
- 11k
- Forks
- 792
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 48
Description
Hey,
We recently added the OSV Scanner GitHub Action to Distr, but it reports a lot duplicates (https://github.com/distr-sh/distr/security/code-scanning).
Happy to draft a pr in order to fix the issue.
## Summary
`--format=sarif` emits one result per matched advisory ID within an alias group instead of one result per group. Each of those results carries the group's `DisplayID` as `ruleId`, the same location, the same message and the same `partialFingerprints`, so a consumer cannot tell them apart. GitHub code scanning opens one alert per result, so a package matched by both a GHSA and a GO entry for the same CVE produces two identical alerts that have to be dismissed individually.
The table output is unaffected, because it prints one row per alias group. That makes the duplication invisible in the workflow log and visible only in the Security tab.
## Version
osv-scanner 2.6.0, run through `ghcr.io/google/osv-scanner-action:v2.6.0`. Also reproducible with the standalone CLI.
## Reproduction
`go.mod`:
```
module example.com/repro
go 1.24
require github.com/docker/docker v28.5.2+incompatible
```
```sh
osv-scanner scan source --format=sarif --output=results.sarif -L go.mod
jq -r '.runs[0].results | group_by(.ruleId)[] | "\(.[0].ruleId) x\(length)"' results.sarif
```
Output, with advisory data as of 2026-09-16:
```
CVE-2026-33997 x2
CVE-2026-34040 x1
CVE-2026-41567 x2
CVE-2026-41568 x2
CVE-2026-42306 x2
```
That is 5 rules and 9 results. `CVE-2026-41567` is matched through two entries that alias each other, `GHSA-x86f-5xw2-fm2r` and `GO-2026-5746`, and is emitted twice. `CVE-2026-34040` is matched through `GO-2026-4887` only and is emitted once. The two results for a doubled rule are identical in `ruleId`, `locations`, `message.text` and `partialFingerprints.primaryLocationLineHash`.
## Root cause
`mapIDsToGroupedSARIFFinding` registers one map key per matched advisory ID, all pointing at the same group object:
https://github.com/google/osv-scanner/blob/v2.6.0/internal/output/result.go#L229-L231
`PrintSARIFReport` then iterates over every key of that map and appends a result per iteration:
https://github.com/google/osv-scanner/blob/v2.6.0/internal/output/sarif.go#L259-L267
`run.AddRule(gv.DisplayID)` deduplicates by rule ID, which is why the rule count stays at one per group while the result count grows:
https://github.com/google/osv-scanner/blob/v2.6.0/internal/output/sarif.go#L301
The CVE never becomes a key itself, since `gi.IDs` holds only the IDs of matched entries, so the number of duplicates equals the number of matched advisories in the group rather than the length of the alias list.
## Expected behaviour
One SARIF result per alias group, per package and source, matching what the table output reports.
## Suggested fix
Skip a group that has already been emitted in that loop, for example with a `seen map[*groupedSARIFFinding]bool`, which preserves the deterministic ordering that the sorted key iteration provides.
## Related
- #2331 added `partialFingerprints` to fix duplicates in GitHub's output. It cannot help here, because these duplicate results are byte-identical, including the fingerprint.
- #3086 covers duplicate packages in the `BuildResults` formatters (table, vertical, Markdown, HTML) caused by ecosystem prefix collisions. SARIF does not use that code path.
Best, Philip
Contributor guide
Research direction
Start with internal/output/result.go at mapIDsToGroupedSARIFFinding and internal/output/sarif.go at PrintSARIFReport and run.AddRule. Reproduce the issue with the provided go.mod and osv-scanner scan command, then verify that SARIF emits one result per alias group while preserving one rule per group and the existing table behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github, go
- Domain
- cli, security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100