Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 92 additions & 18 deletions contrib/trivy/pkg/converter.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package pkg

Check failure on line 1 in contrib/trivy/pkg/converter.go

View workflow job for this annotation

GitHub Actions / Build

should have a package comment https://revive.run/r#package-comments

import (
"cmp"
"fmt"
"maps"
"os"
"path/filepath"
"slices"
Expand All @@ -12,6 +13,7 @@
trivydbTypes "github.com/aquasecurity/trivy-db/pkg/types"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/types"
debver "github.com/knqyf263/go-deb-version"

"github.com/future-architect/vuls/models"
)
Expand Down Expand Up @@ -40,6 +42,7 @@
pkgs := models.Packages{}
srcPkgs := models.SrcPackages{}
vulnInfos := models.VulnInfos{}
dupPkgs := map[string][]string{} // name -> list of versions seen (for duplicate detection)
libraryScannerPaths := map[string]models.LibraryScanner{}
for _, trivyResult := range results {
for _, vuln := range trivyResult.Vulnerabilities {
Expand Down Expand Up @@ -171,9 +174,21 @@
vulnInfos[vuln.VulnerabilityID] = vulnInfo
}

// --list-all-pkgs flg of trivy will output all installed packages, so collect them.
switch trivyResult.Class {
case types.ClassOSPkg:
// Collect all installed packages (requires --list-all-pkgs flag in Trivy).
//
// On Debian/Ubuntu, Trivy's dpkg analyzer reads both /var/lib/dpkg/status
// and /var/lib/dpkg/status.d/*, producing duplicate entries for the
// same package. The applier's dedup key includes FilePath, so entries
// from different paths survive and appear as duplicates in the result.
// (Other analyzers — RPM, APK — are not affected.)
//
// This is not a complete fix; ideally Trivy itself should deduplicate.
// As a workaround we keep the newer version: for Debian/Ubuntu we
// compare using dpkg version semantics; for other OS types we fall
// back to lexicographic string comparison.

for _, p := range trivyResult.Packages {
pv := p.Version
if p.Release != "" {
Expand All @@ -182,28 +197,60 @@
if p.Epoch > 0 {
pv = fmt.Sprintf("%d:%s", p.Epoch, pv)
}
pkgs[p.Name] = models.Package{
Name: p.Name,
Version: pv,
Arch: p.Arch,
}

v, ok := srcPkgs[p.SrcName]
if !ok {
sv := p.SrcVersion
if p.SrcRelease != "" {
sv = fmt.Sprintf("%s-%s", sv, p.SrcRelease)
if existing, ok := pkgs[p.Name]; ok {
if existing.Version != pv {
if versions, seen := dupPkgs[p.Name]; !seen {
dupPkgs[p.Name] = []string{existing.Version, pv}
} else if !slices.Contains(versions, pv) {
dupPkgs[p.Name] = append(versions, pv)
}
}
// >= (not >) so that the Arch-bearing entry from Packages
// overwrites the Arch-less one written by the Vulnerabilities
// loop above, even when the version is identical.
if compareVersions(trivyResult.Type, pv, existing.Version) >= 0 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: compareVersions(...) >= 0 means the map entry is overwritten even when the two versions are equal. Changing the condition to > 0 would skip the redundant reassignment and make the intent clearer — we only want to replace when the new version is strictly newer.

Suggested change
if compareVersions(trivyResult.Type, pv, existing.Version) >= 0 {
if compareVersions(trivyResult.Type, pv, existing.Version) > 0 {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky part (maybe I should have written comment). 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make sense? d3e5edb

pkgs[p.Name] = models.Package{
Name: p.Name,
Version: pv,
Arch: p.Arch,
}
}
if p.SrcEpoch > 0 {
sv = fmt.Sprintf("%d:%s", p.SrcEpoch, sv)
} else {
pkgs[p.Name] = models.Package{
Name: p.Name,
Version: pv,
Arch: p.Arch,
}
}

sv := p.SrcVersion
if p.SrcRelease != "" {
sv = fmt.Sprintf("%s-%s", sv, p.SrcRelease)
}
if p.SrcEpoch > 0 {
sv = fmt.Sprintf("%d:%s", p.SrcEpoch, sv)
}

if existing, ok := srcPkgs[p.SrcName]; ok {
existing.AddBinaryName(p.Name)
// >= for consistency with pkgs above.
if compareVersions(trivyResult.Type, sv, existing.Version) >= 0 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: compareVersions(...) >= 0 means the map entry is overwritten even when the two versions are equal. Changing the condition to > 0 would skip the redundant reassignment and make the intent clearer — we only want to replace when the new version is strictly newer.

Suggested change
if compareVersions(trivyResult.Type, sv, existing.Version) >= 0 {
if compareVersions(trivyResult.Type, sv, existing.Version) > 0 {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, = does not needed here.
I added it to make bin/src collection seems as same as the possible.
But it can be removed. It may be a matter of taste? Which do you prefer?

srcPkgs[p.SrcName] = models.SrcPackage{
Name: p.SrcName,
Version: sv,
BinaryNames: existing.BinaryNames,
}
} else {
srcPkgs[p.SrcName] = existing
}
v = models.SrcPackage{
Name: p.SrcName,
Version: sv,
} else {
srcPkgs[p.SrcName] = models.SrcPackage{
Name: p.SrcName,
Version: sv,
BinaryNames: []string{p.Name},
}
}
v.AddBinaryName(p.Name)
srcPkgs[p.SrcName] = v
}
case types.ClassLangPkg:
for _, p := range trivyResult.Packages {
Expand Down Expand Up @@ -255,6 +302,15 @@
scanResult.Packages = pkgs
scanResult.SrcPackages = srcPkgs
scanResult.LibraryScanners = libraryScanners

for _, name := range slices.Sorted(maps.Keys(dupPkgs)) {
slices.Sort(dupPkgs[name])
scanResult.Warnings = append(scanResult.Warnings, fmt.Sprintf(
"Duplicate OS package detected: %s (%s). The newest version is kept, but false-positive CVEs may remain.",
name, strings.Join(dupPkgs[name], ", "),
))
}

return scanResult, nil
}

Expand Down Expand Up @@ -289,6 +345,24 @@
return ok
}

// compareVersions returns a positive value if a > b, zero if a == b,
// and a negative value if a < b.
// For Debian/Ubuntu, dpkg version semantics are used.
// For other OS types, lexicographic string comparison is used as a fallback.
func compareVersions(osType ftypes.TargetType, a, b string) int {
switch osType {
case ftypes.Debian, ftypes.Ubuntu:
va, erra := debver.NewVersion(a)
vb, errb := debver.NewVersion(b)
if erra != nil || errb != nil {
return cmp.Compare(a, b)
}
return va.Compare(vb)
default:
return cmp.Compare(a, b)
}
}

func getPURL(p ftypes.Package) string {
if p.Identifier.PURL == nil {
return ""
Expand Down
Loading
Loading