@@ -12,6 +12,7 @@ import (
1212 trivydbTypes "github.com/aquasecurity/trivy-db/pkg/types"
1313 ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
1414 "github.com/aquasecurity/trivy/pkg/types"
15+ debver "github.com/knqyf263/go-deb-version"
1516
1617 "github.com/future-architect/vuls/models"
1718)
@@ -171,9 +172,21 @@ func Convert(results types.Results, artifactType ftypes.ArtifactType, artifactNa
171172 vulnInfos [vuln .VulnerabilityID ] = vulnInfo
172173 }
173174
174- // --list-all-pkgs flg of trivy will output all installed packages, so collect them.
175175 switch trivyResult .Class {
176176 case types .ClassOSPkg :
177+ // Collect all installed packages (requires --list-all-pkgs flag in Trivy).
178+ //
179+ // On Debian/Ubuntu, Trivy's dpkg analyzer reads both /var/lib/dpkg/status
180+ // and /var/lib/dpkg/status.d/*, producing duplicate entries for the
181+ // same package. The applier's dedup key includes FilePath, so entries
182+ // from different paths survive and appear as duplicates in the result.
183+ // (Other analyzers — RPM, APK — are not affected.)
184+ //
185+ // This is not a complete fix; ideally Trivy itself should deduplicate.
186+ // As a workaround we keep the newer version: for Debian/Ubuntu we
187+ // compare using dpkg version semantics; for other OS types we fall
188+ // back to lexicographic string comparison.
189+
177190 for _ , p := range trivyResult .Packages {
178191 pv := p .Version
179192 if p .Release != "" {
@@ -182,28 +195,34 @@ func Convert(results types.Results, artifactType ftypes.ArtifactType, artifactNa
182195 if p .Epoch > 0 {
183196 pv = fmt .Sprintf ("%d:%s" , p .Epoch , pv )
184197 }
185- pkgs [p .Name ] = models.Package {
186- Name : p .Name ,
187- Version : pv ,
188- Arch : p .Arch ,
189- }
190198
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 )
199+ if existing , ok := pkgs [p .Name ]; ! ok || versionGreaterThan ( trivyResult . Type , pv , existing . Version ) {
200+ pkgs [ p . Name ] = models. Package {
201+ Name : p . Name ,
202+ Version : pv ,
203+ Arch : p .Arch ,
196204 }
197- if p .SrcEpoch > 0 {
198- sv = fmt .Sprintf ("%d:%s" , p .SrcEpoch , sv )
199- }
200- v = models.SrcPackage {
201- Name : p .SrcName ,
202- Version : sv ,
205+ }
206+
207+ sv := p .SrcVersion
208+ if p .SrcRelease != "" {
209+ sv = fmt .Sprintf ("%s-%s" , sv , p .SrcRelease )
210+ }
211+ if p .SrcEpoch > 0 {
212+ sv = fmt .Sprintf ("%d:%s" , p .SrcEpoch , sv )
213+ }
214+
215+ existing := srcPkgs [p .SrcName ]
216+ existing .AddBinaryName (p .Name )
217+ if existing .Name == "" || versionGreaterThan (trivyResult .Type , sv , existing .Version ) {
218+ srcPkgs [p .SrcName ] = models.SrcPackage {
219+ Name : p .SrcName ,
220+ Version : sv ,
221+ BinaryNames : existing .BinaryNames ,
203222 }
223+ } else {
224+ srcPkgs [p .SrcName ] = existing
204225 }
205- v .AddBinaryName (p .Name )
206- srcPkgs [p .SrcName ] = v
207226 }
208227 case types .ClassLangPkg :
209228 for _ , p := range trivyResult .Packages {
@@ -289,6 +308,23 @@ func isTrivySupportedOS(family ftypes.TargetType) bool {
289308 return ok
290309}
291310
311+ // versionGreaterThan reports whether a is strictly greater than b.
312+ // For Debian/Ubuntu, dpkg version semantics are used.
313+ // For other OS types, lexicographic string comparison is used as a fallback.
314+ func versionGreaterThan (osType ftypes.TargetType , a , b string ) bool {
315+ switch osType {
316+ case ftypes .Debian , ftypes .Ubuntu :
317+ va , erra := debver .NewVersion (a )
318+ vb , errb := debver .NewVersion (b )
319+ if erra != nil || errb != nil {
320+ return a > b
321+ }
322+ return va .GreaterThan (vb )
323+ default :
324+ return a > b
325+ }
326+ }
327+
292328func getPURL (p ftypes.Package ) string {
293329 if p .Identifier .PURL == nil {
294330 return ""
0 commit comments