@@ -3,6 +3,7 @@ package pkg
33import (
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+
292366func getPURL (p ftypes.Package ) string {
293367 if p .Identifier .PURL == nil {
294368 return ""
0 commit comments