Skip to content
Open
56 changes: 42 additions & 14 deletions pkg/detector/ospkg/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/aquasecurity/trivy/pkg/detector/ospkg/minimos"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/oracle"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/photon"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rapidfort"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/redhat"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rocky"
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio"
Expand Down Expand Up @@ -74,12 +75,19 @@ var (
rootio.Provider,
seal.Provider,
}

// labelProviders dynamically generate drivers based on image config labels.
// They are tried before providers and standard OS-specific drivers.
labelProviders = []driver.LabelProvider{
rapidfort.Provider,
}
)

// resolver holds the candidate drivers and providers and resolves one for a scan target.
type resolver struct {
drivers map[ftypes.OSType]driver.Driver
providers []driver.Provider
drivers map[ftypes.OSType]driver.Driver
providers []driver.Provider
labelProviders []driver.LabelProvider
}

// Option configures a Detector. Options are provided for extensibility by users
Expand All @@ -105,14 +113,15 @@ func WithProvider(provider driver.Provider) Option {
// NewDetector creates a new Detector for the given scan target
func NewDetector(target types.ScanTarget, opts ...Option) (*Detector, error) {
r := &resolver{
drivers: maps.Clone(drivers),
providers: slices.Clone(providers),
drivers: maps.Clone(drivers),
providers: slices.Clone(providers),
labelProviders: slices.Clone(labelProviders),
}
for _, opt := range opts {
opt(r)
}

drv, err := r.resolve(target.OS.Family, target.Packages)
drv, err := r.resolve(target.OS.Family, target.Packages, target.ImageLabels)
if err != nil {
return nil, err
}
Expand All @@ -128,8 +137,16 @@ func (d *Detector) Detect(ctx context.Context) ([]types.DetectedVulnerability, b

eosl := !d.driver.IsSupportedVersion(ctx, d.target.OS.Family, d.target.OS.Name)

filteredPkgs := filterPkgs(ctx, d.target.Packages)
vulns, err := d.driver.Detect(ctx, d.target.OS.Name, d.target.Repository, filteredPkgs)
// includeThirdParty reports whether the driver wants third-party packages
// passed through to Detect (e.g. RapidFort curates advisories for them).
// Note: this only affects third-party filtering — gpg-pubkey is still dropped
// unconditionally inside filterPkgs because it has no scannable version.
includeThirdParty := false
if tp, ok := d.driver.(driver.ThirdPartyAware); ok {
includeThirdParty = tp.IncludesThirdParty()
}
pkgs := filterPkgs(ctx, d.target.Packages, includeThirdParty)
vulns, err := d.driver.Detect(ctx, d.target.OS.Name, d.target.Repository, pkgs)
if err != nil {
return nil, false, xerrors.Errorf("failed detection: %w", err)
}
Expand All @@ -138,15 +155,19 @@ func (d *Detector) Detect(ctx context.Context) ([]types.DetectedVulnerability, b
}

// filterPkgs filters out packages that should not be scanned:
// - gpg-pubkey: doesn't use the correct version
// - Third-party packages: not covered by official OS security advisories
func filterPkgs(ctx context.Context, pkgs []ftypes.Package) []ftypes.Package {
// - gpg-pubkey: has no valid version — always dropped regardless of driver.
// - Third-party packages: not covered by official OS security advisories, so
// they are dropped by default. A driver can opt out via ThirdPartyAware
// (e.g. RapidFort curates its own advisories for third-party packages);
// when includeThirdParty is true they are passed through untouched, but
// gpg-pubkey is still stripped above.
func filterPkgs(ctx context.Context, pkgs []ftypes.Package, includeThirdParty bool) []ftypes.Package {
var skipped []string
filtered := lo.Filter(pkgs, func(pkg ftypes.Package, _ int) bool {
if pkg.Name == "gpg-pubkey" {
return false
}
if pkg.Repository.Class == ftypes.RepositoryClassThirdParty {
if !includeThirdParty && pkg.Repository.Class == ftypes.RepositoryClassThirdParty {
skipped = append(skipped, pkg.Name)
return false
}
Expand All @@ -158,15 +179,22 @@ func filterPkgs(ctx context.Context, pkgs []ftypes.Package) []ftypes.Package {
return filtered
}

func (r *resolver) resolve(osFamily ftypes.OSType, pkgs []ftypes.Package) (driver.Driver, error) {
// Try providers first
func (r *resolver) resolve(osFamily ftypes.OSType, pkgs []ftypes.Package, labels map[string]string) (driver.Driver, error) {
// Try label-aware providers first (e.g. RapidFort curated image detection)
for _, provider := range r.labelProviders {
if d := provider(osFamily, pkgs, labels); d != nil {
return d, nil
}
}

// Try package-pattern providers (e.g. rootio, seal)
for _, provider := range r.providers {
if d := provider(osFamily, pkgs); d != nil {
return d, nil
}
}

// Fall back to standard drivers
// Fall back to standard OS-specific drivers
if d, ok := r.drivers[osFamily]; ok {
return d, nil
}
Expand Down
64 changes: 64 additions & 0 deletions pkg/detector/ospkg/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ func (m *mockDriver) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ st
return true
}

// mockThirdPartyDriver implements both Driver and driver.ThirdPartyAware.
// It is used to verify that filterPkgs still drops gpg-pubkey even when the
// driver opts to receive third-party packages.
type mockThirdPartyDriver struct {
mockDriver
}

func (m *mockThirdPartyDriver) IncludesThirdParty() bool {
return true
}

func TestDetector_Detect(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -159,6 +170,59 @@ func TestDetector_Detect(t *testing.T) {
}
}

// TestDetector_Detect_ThirdPartyAware verifies that when the driver opts in via
// ThirdPartyAware, third-party packages reach Detect but gpg-pubkey is still
// filtered out (gpg-pubkey has no scannable version and is dropped for every
// driver regardless of the opt-in).
func TestDetector_Detect_ThirdPartyAware(t *testing.T) {
target := types.ScanTarget{
OS: ftypes.OS{
Family: ftypes.CentOS,
Name: "7",
},
Packages: []ftypes.Package{
{
Name: "vim",
Repository: ftypes.PackageRepository{
Class: ftypes.RepositoryClassOfficial,
},
},
{Name: "gpg-pubkey"},
{
Name: "php",
Repository: ftypes.PackageRepository{
Class: ftypes.RepositoryClassThirdParty,
},
},
},
}

// Expected: gpg-pubkey stripped; the third-party php package passes through
// because the driver opted in.
wantPkgs := []ftypes.Package{
{
Name: "vim",
Repository: ftypes.PackageRepository{
Class: ftypes.RepositoryClassOfficial,
},
},
{
Name: "php",
Repository: ftypes.PackageRepository{
Class: ftypes.RepositoryClassThirdParty,
},
},
}

mockDrv := &mockThirdPartyDriver{}
d, err := ospkg.NewDetector(target, ospkg.WithDriver(target.OS.Family, mockDrv))
require.NoError(t, err)

_, _, err = d.Detect(t.Context())
require.NoError(t, err)
assert.Equal(t, wantPkgs, mockDrv.receivedPkgs)
}

func TestNewDetector(t *testing.T) {
target := types.ScanTarget{
OS: ftypes.OS{
Expand Down
12 changes: 12 additions & 0 deletions pkg/detector/ospkg/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ type Driver interface {

// Provider creates a specialized driver based on the environment
type Provider func(osFamily ftypes.OSType, pkgs []ftypes.Package) Driver

// LabelProvider creates a specialized driver based on the environment and image labels.
// Use this when detection requires access to container image config labels.
type LabelProvider func(osFamily ftypes.OSType, pkgs []ftypes.Package, labels map[string]string) Driver

// ThirdPartyAware is an optional interface a Driver can implement to indicate
// it handles third-party packages itself and should receive them unfiltered.
// Drivers that do NOT implement this interface have third-party packages stripped
// before Detect is called (the default behavior).
type ThirdPartyAware interface {
IncludesThirdParty() bool
}
12 changes: 12 additions & 0 deletions pkg/detector/ospkg/rapidfort/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package rapidfort

import (
"context"

dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
)

// IsVulnerable exports isVulnerable for testing.
func (s *Scanner) IsVulnerable(ctx context.Context, installedVersion string, isRFPackage bool, adv dbTypes.Advisory) bool {
return s.isVulnerable(ctx, installedVersion, isRFPackage, adv)
}
38 changes: 38 additions & 0 deletions pkg/detector/ospkg/rapidfort/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package rapidfort

import (
"strings"

"github.com/aquasecurity/trivy/pkg/detector/ospkg/driver"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
)

const (
// maintainerLabel is the Docker image config label used by RapidFort curated images.
maintainerLabel = "maintainer"

// rapidfortIdentifier is the string that must appear in the maintainer label value.
rapidfortIdentifier = "rapidfort"
)

// Provider creates a RapidFort driver if the image has a RapidFort maintainer label.
func Provider(osFamily ftypes.OSType, _ []ftypes.Package, labels map[string]string) driver.Driver {
if !isRapidFortImage(labels) {
return nil
}
switch osFamily {
case ftypes.Ubuntu, ftypes.Alpine, ftypes.RedHat:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about Debian?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Debian is in progress, and will be out in next release.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you mean your release?
Should we add Debian support for Trivy right away?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The code for integration of Debian in Trivy is already done by us, and once the advisory data is published we'll share that PR with you as well.

Integrating Debian for RapidFort images without proper security advisory data wouldn't be right.

return NewScanner(osFamily)
}
return nil
}

// isRapidFortImage returns true when the image config labels identify this as a
// RapidFort curated image (maintainer label contains "rapidfort", case-insensitive).
func isRapidFortImage(labels map[string]string) bool {
val, ok := labels[maintainerLabel]
if !ok {
return false
}
return strings.Contains(strings.ToLower(val), rapidfortIdentifier)
}
Comment on lines +30 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are there any ways to detect that scanned image is RapidFort image? (e.g. os-release file, etc.)?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No, as of now this is the only reliable way to identify Rapidfort image. Post in future, if there's any other reliable way introduced we will make sure to integrate it here as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, we checked some images (Docker Hub: curl, redis-official, apache-official, elasticsearch-official, cassandra-official, grafana-official, couchdb-official).
For all these images, the maintainer field is either empty or set to the upstream vendor.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the case of Non-Rapidfort Images like the ones you mentioned, Rapidfort Advisory is not used. Our advisory only works in case of Rapidfort Labelled/Maintained images.

Loading