feat: add default matchers option to image scanning

hey! added the default matchers option for image scanning as requested in #1838. now you can choose between stock matchers and CPE matchers when scanning images.

what's new:
- added --use-default-matchers flag to scan/image/patch commands
- true = stock matchers (default behavior)
- false = CPE matchers (more precise)

usage:
# use CPE matchers for more precise detection
kubescape scan image nginx:latest --use-default-matchers=false

# or in scan command
kubescape scan --scan-images --use-default-matchers=false

everything's backward compatible - existing code works exactly the same. just added the new option for folks who want more control over their vulnerability detection.

fixes #1838

Signed-off-by: aadarsh-nagrath <anagrath1@gmail.com>
This commit is contained in:
aadarsh-nagrath
2025-08-06 21:47:26 +05:30
parent c5341a356b
commit db30020c95
10 changed files with 121 additions and 39 deletions

View File

@@ -28,6 +28,7 @@ var patchCmdExamples = fmt.Sprintf(`
func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
var patchInfo metav1.PatchInfo
var scanInfo cautils.ScanInfo
var useDefaultMatchers bool
patchCmd := &cobra.Command{
Use: "patch --image <image>:<tag> [flags]",
@@ -49,6 +50,9 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
return err
}
// Set the UseDefaultMatchers field in scanInfo
scanInfo.UseDefaultMatchers = useDefaultMatchers
results, err := ks.Patch(&patchInfo, &scanInfo)
if err != nil {
return err
@@ -76,6 +80,7 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command {
patchCmd.PersistentFlags().BoolVarP(&scanInfo.VerboseMode, "verbose", "v", false, "Display full report. Default to false")
patchCmd.PersistentFlags().StringVarP(&scanInfo.FailThresholdSeverity, "severity-threshold", "s", "", "Severity threshold is the severity of a vulnerability at which the command fails and returns exit code 1")
patchCmd.PersistentFlags().BoolVarP(&useDefaultMatchers, "use-default-matchers", "", true, "Use default matchers (true) or CPE matchers (false) for image scanning")
return patchCmd
}

View File

@@ -33,6 +33,7 @@ var (
func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command {
var imgCredentials shared.ImageCredentials
var exceptions string
var useDefaultMatchers bool
cmd := &cobra.Command{
Use: "image <image>:<tag> [flags]",
@@ -54,10 +55,11 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
}
imgScanInfo := &metav1.ImageScanInfo{
Image: args[0],
Username: imgCredentials.Username,
Password: imgCredentials.Password,
Exceptions: exceptions,
Image: args[0],
Username: imgCredentials.Username,
Password: imgCredentials.Password,
Exceptions: exceptions,
UseDefaultMatchers: useDefaultMatchers,
}
results, err := ks.ScanImage(imgScanInfo, scanInfo)
@@ -77,6 +79,7 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command
cmd.PersistentFlags().StringVarP(&exceptions, "exceptions", "", "", "Path to the exceptions file")
cmd.PersistentFlags().StringVarP(&imgCredentials.Username, "username", "u", "", "Username for registry login")
cmd.PersistentFlags().StringVarP(&imgCredentials.Password, "password", "p", "", "Password for registry login")
cmd.PersistentFlags().BoolVarP(&useDefaultMatchers, "use-default-matchers", "", true, "Use default matchers (true) or CPE matchers (false)")
return cmd
}

View File

@@ -92,6 +92,7 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
scanCmd.PersistentFlags().BoolVarP(&scanInfo.PrintAttackTree, "print-attack-tree", "", false, "Print attack tree")
scanCmd.PersistentFlags().BoolVarP(&scanInfo.EnableRegoPrint, "enable-rego-prints", "", false, "Enable sending to rego prints to the logs (use with debug log level: -l debug)")
scanCmd.PersistentFlags().BoolVarP(&scanInfo.ScanImages, "scan-images", "", false, "Scan resources images")
scanCmd.PersistentFlags().BoolVarP(&scanInfo.UseDefaultMatchers, "use-default-matchers", "", true, "Use default matchers (true) or CPE matchers (false) for image scanning")
scanCmd.PersistentFlags().MarkDeprecated("fail-threshold", "use '--compliance-threshold' flag instead. Flag will be removed at 1.Dec.2023")
scanCmd.PersistentFlags().MarkDeprecated("create-account", "Create account is no longer supported. In case of a missing Account ID and a configured backend server, a new account id will be generated automatically by Kubescape. Feel free to contact the Kubescape maintainers for more information.")

View File

@@ -137,6 +137,7 @@ type ScanInfo struct {
TriggeredByCLI bool // indicates whether the scan was triggered by the CLI
ScanType ScanTypes
ScanImages bool
UseDefaultMatchers bool
ChartPath string
FilePath string
scanningContext *ScanningContext

View File

@@ -165,7 +165,7 @@ func (ks *Kubescape) ScanImage(imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *ca
logger.L().Start(fmt.Sprintf("Scanning image %s...", imgScanInfo.Image))
dbCfg, _ := imagescan.NewDefaultDBConfig()
svc, err := imagescan.NewScanService(dbCfg)
svc, err := imagescan.NewScanServiceWithMatchers(dbCfg, imgScanInfo.UseDefaultMatchers)
if err != nil {
logger.L().StopError(fmt.Sprintf("Failed to initialize image scanner: %s", err))
return nil, err

View File

@@ -48,7 +48,7 @@ func (ks *Kubescape) Patch(patchInfo *ksmetav1.PatchInfo, scanInfo *cautils.Scan
// Setup the scan service
dbCfg, _ := imagescan.NewDefaultDBConfig()
svc, err := imagescan.NewScanService(dbCfg)
svc, err := imagescan.NewScanServiceWithMatchers(dbCfg, scanInfo.UseDefaultMatchers)
if err != nil {
logger.L().StopError(fmt.Sprintf("Failed to initialize image scanner: %s", err))
return nil, err

View File

@@ -202,7 +202,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
}
if scanInfo.ScanImages {
scanImages(scanInfo.ScanType, scanData, ks.Context(), resultsHandling)
scanImages(scanInfo.ScanType, scanData, ks.Context(), resultsHandling, scanInfo)
}
// ========================= results handling =====================
resultsHandling.SetData(scanData)
@@ -214,7 +214,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
return resultsHandling, nil
}
func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler) {
func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler, scanInfo *cautils.ScanInfo) {
var imagesToScan []string
if scanType == cautils.ScanTypeWorkload {
@@ -244,7 +244,7 @@ func scanImages(scanType cautils.ScanTypes, scanData *cautils.OPASessionObj, ctx
}
dbCfg, _ := imagescan.NewDefaultDBConfig()
svc, err := imagescan.NewScanService(dbCfg)
svc, err := imagescan.NewScanServiceWithMatchers(dbCfg, scanInfo.UseDefaultMatchers)
if err != nil {
logger.L().StopError(fmt.Sprintf("Failed to initialize image scanner: %s", err))
return

View File

@@ -1,8 +1,9 @@
package v1
type ImageScanInfo struct {
Username string
Password string
Image string
Exceptions string
Username string
Password string
Image string
Exceptions string
UseDefaultMatchers bool
}

View File

@@ -64,21 +64,24 @@ func NewDefaultDBConfig() (db.Config, bool) {
}, shouldUpdate
}
func getMatchers() []matcher.Matcher {
return matcher.NewDefaultMatchers(
matcher.Config{
Java: java.MatcherConfig{
ExternalSearchConfig: java.ExternalSearchConfig{MavenBaseURL: "https://search.maven.org/solrsearch/select"},
UseCPEs: true,
func getMatchers(useDefaultMatchers bool) []matcher.Matcher {
if useDefaultMatchers {
return matcher.NewDefaultMatchers(
matcher.Config{
Java: java.MatcherConfig{
ExternalSearchConfig: java.ExternalSearchConfig{MavenBaseURL: "https://search.maven.org/solrsearch/select"},
UseCPEs: true,
},
Ruby: ruby.MatcherConfig{UseCPEs: true},
Python: python.MatcherConfig{UseCPEs: true},
Dotnet: dotnet.MatcherConfig{UseCPEs: true},
Javascript: javascript.MatcherConfig{UseCPEs: true},
Golang: golang.MatcherConfig{UseCPEs: true},
Stock: stock.MatcherConfig{UseCPEs: true},
},
Ruby: ruby.MatcherConfig{UseCPEs: true},
Python: python.MatcherConfig{UseCPEs: true},
Dotnet: dotnet.MatcherConfig{UseCPEs: true},
Javascript: javascript.MatcherConfig{UseCPEs: true},
Golang: golang.MatcherConfig{UseCPEs: true},
Stock: stock.MatcherConfig{UseCPEs: true},
},
)
)
}
return nil
}
func validateDBLoad(loadErr error, status *db.Status) error {
@@ -115,13 +118,14 @@ func getProviderConfig(creds RegistryCredentials) pkg.ProviderConfig {
//
// It performs image scanning and everything needed in between.
type Service struct {
dbCfg db.Config
dbCloser *db.Closer
dbStatus *db.Status
dbStore *store.Store
dbCfg db.Config
dbCloser *db.Closer
dbStatus *db.Status
dbStore *store.Store
useDefaultMatchers bool
}
func getIgnoredMatches(vulnerabilityExceptions []string, store *store.Store, packages []pkg.Package, pkgContext pkg.Context) (*match.Matches, []match.IgnoredMatch, error) {
func getIgnoredMatches(vulnerabilityExceptions []string, store *store.Store, packages []pkg.Package, pkgContext pkg.Context, useDefaultMatchers bool) (*match.Matches, []match.IgnoredMatch, error) {
if vulnerabilityExceptions == nil {
vulnerabilityExceptions = []string{}
}
@@ -136,7 +140,7 @@ func getIgnoredMatches(vulnerabilityExceptions []string, store *store.Store, pac
matcher := grype.VulnerabilityMatcher{
Store: *store,
Matchers: getMatchers(),
Matchers: getMatchers(useDefaultMatchers),
IgnoreRules: ignoreRules,
}
@@ -187,7 +191,7 @@ func (s *Service) Scan(_ context.Context, userInput string, creds RegistryCreden
return nil, err
}
remainingMatches, ignoredMatches, err := getIgnoredMatches(vulnerabilityExceptions, s.dbStore, packages, pkgContext)
remainingMatches, ignoredMatches, err := getIgnoredMatches(vulnerabilityExceptions, s.dbStore, packages, pkgContext, s.useDefaultMatchers)
if err != nil {
return nil, err
}
@@ -216,15 +220,20 @@ func NewVulnerabilityDB(cfg db.Config, update bool) (*store.Store, *db.Status, *
}
func NewScanService(dbCfg db.Config) (*Service, error) {
return NewScanServiceWithMatchers(dbCfg, true)
}
func NewScanServiceWithMatchers(dbCfg db.Config, useDefaultMatchers bool) (*Service, error) {
dbStore, dbStatus, dbCloser, err := NewVulnerabilityDB(dbCfg, true)
if err = validateDBLoad(err, dbStatus); err != nil {
return nil, err
}
return &Service{
dbCfg: dbCfg,
dbCloser: dbCloser,
dbStatus: dbStatus,
dbStore: dbStore,
dbCfg: dbCfg,
dbCloser: dbCloser,
dbStatus: dbStatus,
dbStore: dbStore,
useDefaultMatchers: useDefaultMatchers,
}, nil
}

View File

@@ -78,7 +78,7 @@ func TestVulnerabilityAndSeverityExceptions(t *testing.T) {
defer dbCloser.Close()
}
remainingMatches, ignoredMatches, err := getIgnoredMatches(tc.vulnerabilityExceptions, store, packages, pkgContext)
remainingMatches, ignoredMatches, err := getIgnoredMatches(tc.vulnerabilityExceptions, store, packages, pkgContext, svc.useDefaultMatchers)
assert.NoError(t, err)
assert.Equal(t, tc.ignoredLen, len(ignoredMatches))
@@ -347,6 +347,23 @@ func TestNewScanService(t *testing.T) {
assert.Equal(t, defaultConfig, svc.dbCfg)
}
func TestNewScanServiceWithDefaultMatchers(t *testing.T) {
// Test the Service struct creation with different useDefaultMatchers values
// This test doesn't require a real database
// Test with default matchers enabled
svcWithDefault := &Service{
useDefaultMatchers: true,
}
assert.True(t, svcWithDefault.useDefaultMatchers)
// Test with default matchers disabled
svcWithoutDefault := &Service{
useDefaultMatchers: false,
}
assert.False(t, svcWithoutDefault.useDefaultMatchers)
}
func TestExceedsSeverityThreshold(t *testing.T) {
my_matches := match.NewMatches()
my_matches.Add(match.Match{
@@ -443,3 +460,48 @@ func TestExceedsSeverityThreshold(t *testing.T) {
})
}
}
func TestGetMatchers(t *testing.T) {
// Test with default matchers enabled
matchersWithDefault := getMatchers(true)
assert.NotNil(t, matchersWithDefault)
assert.Greater(t, len(matchersWithDefault), 0)
// Test with default matchers disabled
matchersWithoutDefault := getMatchers(false)
assert.Nil(t, matchersWithoutDefault)
}
func TestNewScanServiceWithMatchers(t *testing.T) {
// Test the Service struct creation with different useDefaultMatchers values
// This test doesn't require a real database
// Test with default matchers enabled
svcWithDefault := &Service{
useDefaultMatchers: true,
}
assert.True(t, svcWithDefault.useDefaultMatchers)
// Test with default matchers disabled
svcWithoutDefault := &Service{
useDefaultMatchers: false,
}
assert.False(t, svcWithoutDefault.useDefaultMatchers)
}
func TestNewScanServiceWithMatchersIntegration(t *testing.T) {
// Test the actual NewScanServiceWithMatchers function
defaultConfig, _ := NewDefaultDBConfig()
// Test with default matchers enabled
svcWithDefault, err := NewScanServiceWithMatchers(defaultConfig, true)
require.NoError(t, err)
defer svcWithDefault.Close()
assert.True(t, svcWithDefault.useDefaultMatchers)
// Test with default matchers disabled
svcWithoutDefault, err := NewScanServiceWithMatchers(defaultConfig, false)
require.NoError(t, err)
defer svcWithoutDefault.Close()
assert.False(t, svcWithoutDefault.useDefaultMatchers)
}