Skip to content

Commit 44e160e

Browse files
shinoCopilot
andauthored
fix(contrib/trivy): dedup package names and add dup warnings (#2491)
* fix(contrib/trivy): dedup package names, almost for dpkg * fix * add warnings * Update contrib/trivy/pkg/converter.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add disclaimer --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 652294a commit 44e160e

File tree

2 files changed

+499
-18
lines changed

2 files changed

+499
-18
lines changed

contrib/trivy/pkg/converter.go

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pkg
33
import (
44
"cmp"
55
"fmt"
6+
"maps"
67
"os"
78
"path/filepath"
89
"slices"
@@ -12,6 +13,7 @@ import (
1213
trivydbTypes "github.com/aquasecurity/trivy-db/pkg/types"
1314
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
1415
"github.com/aquasecurity/trivy/pkg/types"
16+
debver "github.com/knqyf263/go-deb-version"
1517

1618
"github.com/future-architect/vuls/models"
1719
)
@@ -40,6 +42,7 @@ func Convert(results types.Results, artifactType ftypes.ArtifactType, artifactNa
4042
pkgs := models.Packages{}
4143
srcPkgs := models.SrcPackages{}
4244
vulnInfos := models.VulnInfos{}
45+
dupPkgs := map[string][]string{} // name -> list of versions seen (for duplicate detection)
4346
libraryScannerPaths := map[string]models.LibraryScanner{}
4447
for _, trivyResult := range results {
4548
for _, vuln := range trivyResult.Vulnerabilities {
@@ -171,9 +174,21 @@ func Convert(results types.Results, artifactType ftypes.ArtifactType, artifactNa
171174
vulnInfos[vuln.VulnerabilityID] = vulnInfo
172175
}
173176

174-
// --list-all-pkgs flg of trivy will output all installed packages, so collect them.
175177
switch trivyResult.Class {
176178
case types.ClassOSPkg:
179+
// Collect all installed packages (requires --list-all-pkgs flag in Trivy).
180+
//
181+
// On Debian/Ubuntu, Trivy's dpkg analyzer reads both /var/lib/dpkg/status
182+
// and /var/lib/dpkg/status.d/*, producing duplicate entries for the
183+
// same package. The applier's dedup key includes FilePath, so entries
184+
// from different paths survive and appear as duplicates in the result.
185+
// (Other analyzers — RPM, APK — are not affected.)
186+
//
187+
// This is not a complete fix; ideally Trivy itself should deduplicate.
188+
// As a workaround we keep the newer version: for Debian/Ubuntu we
189+
// compare using dpkg version semantics; for other OS types we fall
190+
// back to lexicographic string comparison.
191+
177192
for _, p := range trivyResult.Packages {
178193
pv := p.Version
179194
if p.Release != "" {
@@ -182,28 +197,60 @@ func Convert(results types.Results, artifactType ftypes.ArtifactType, artifactNa
182197
if p.Epoch > 0 {
183198
pv = fmt.Sprintf("%d:%s", p.Epoch, pv)
184199
}
185-
pkgs[p.Name] = models.Package{
186-
Name: p.Name,
187-
Version: pv,
188-
Arch: p.Arch,
189-
}
190200

191-
v, ok := srcPkgs[p.SrcName]
192-
if !ok {
193-
sv := p.SrcVersion
194-
if p.SrcRelease != "" {
195-
sv = fmt.Sprintf("%s-%s", sv, p.SrcRelease)
201+
if existing, ok := pkgs[p.Name]; ok {
202+
if existing.Version != pv {
203+
if versions, seen := dupPkgs[p.Name]; !seen {
204+
dupPkgs[p.Name] = []string{existing.Version, pv}
205+
} else if !slices.Contains(versions, pv) {
206+
dupPkgs[p.Name] = append(versions, pv)
207+
}
208+
}
209+
// >= (not >) so that the Arch-bearing entry from Packages
210+
// overwrites the Arch-less one written by the Vulnerabilities
211+
// loop above, even when the version is identical.
212+
if compareVersions(trivyResult.Type, pv, existing.Version) >= 0 {
213+
pkgs[p.Name] = models.Package{
214+
Name: p.Name,
215+
Version: pv,
216+
Arch: p.Arch,
217+
}
196218
}
197-
if p.SrcEpoch > 0 {
198-
sv = fmt.Sprintf("%d:%s", p.SrcEpoch, sv)
219+
} else {
220+
pkgs[p.Name] = models.Package{
221+
Name: p.Name,
222+
Version: pv,
223+
Arch: p.Arch,
224+
}
225+
}
226+
227+
sv := p.SrcVersion
228+
if p.SrcRelease != "" {
229+
sv = fmt.Sprintf("%s-%s", sv, p.SrcRelease)
230+
}
231+
if p.SrcEpoch > 0 {
232+
sv = fmt.Sprintf("%d:%s", p.SrcEpoch, sv)
233+
}
234+
235+
if existing, ok := srcPkgs[p.SrcName]; ok {
236+
existing.AddBinaryName(p.Name)
237+
// >= for consistency with pkgs above.
238+
if compareVersions(trivyResult.Type, sv, existing.Version) >= 0 {
239+
srcPkgs[p.SrcName] = models.SrcPackage{
240+
Name: p.SrcName,
241+
Version: sv,
242+
BinaryNames: existing.BinaryNames,
243+
}
244+
} else {
245+
srcPkgs[p.SrcName] = existing
199246
}
200-
v = models.SrcPackage{
201-
Name: p.SrcName,
202-
Version: sv,
247+
} else {
248+
srcPkgs[p.SrcName] = models.SrcPackage{
249+
Name: p.SrcName,
250+
Version: sv,
251+
BinaryNames: []string{p.Name},
203252
}
204253
}
205-
v.AddBinaryName(p.Name)
206-
srcPkgs[p.SrcName] = v
207254
}
208255
case types.ClassLangPkg:
209256
for _, p := range trivyResult.Packages {
@@ -255,6 +302,15 @@ func Convert(results types.Results, artifactType ftypes.ArtifactType, artifactNa
255302
scanResult.Packages = pkgs
256303
scanResult.SrcPackages = srcPkgs
257304
scanResult.LibraryScanners = libraryScanners
305+
306+
for _, name := range slices.Sorted(maps.Keys(dupPkgs)) {
307+
slices.Sort(dupPkgs[name])
308+
scanResult.Warnings = append(scanResult.Warnings, fmt.Sprintf(
309+
"Duplicate OS package detected: %s (%s). The newest version is kept, but false-positive CVEs may remain.",
310+
name, strings.Join(dupPkgs[name], ", "),
311+
))
312+
}
313+
258314
return scanResult, nil
259315
}
260316

@@ -289,6 +345,24 @@ func isTrivySupportedOS(family ftypes.TargetType) bool {
289345
return ok
290346
}
291347

348+
// compareVersions returns a positive value if a > b, zero if a == b,
349+
// and a negative value if a < b.
350+
// For Debian/Ubuntu, dpkg version semantics are used.
351+
// For other OS types, lexicographic string comparison is used as a fallback.
352+
func compareVersions(osType ftypes.TargetType, a, b string) int {
353+
switch osType {
354+
case ftypes.Debian, ftypes.Ubuntu:
355+
va, erra := debver.NewVersion(a)
356+
vb, errb := debver.NewVersion(b)
357+
if erra != nil || errb != nil {
358+
return cmp.Compare(a, b)
359+
}
360+
return va.Compare(vb)
361+
default:
362+
return cmp.Compare(a, b)
363+
}
364+
}
365+
292366
func getPURL(p ftypes.Package) string {
293367
if p.Identifier.PURL == nil {
294368
return ""

0 commit comments

Comments
 (0)