mirror of
https://github.com/kubescape/kubescape.git
synced 2026-04-15 06:58:11 +00:00
Merge branch 'feat-image-scan-svc' of github.com:kubescape/kubescape into feat-image-scan-svc
This commit is contained in:
+92
-65
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/anchore/grype/grype"
|
||||
"github.com/anchore/grype/grype/db"
|
||||
"github.com/anchore/grype/grype/grypeerr"
|
||||
@@ -19,16 +21,34 @@ import (
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/store"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultGrypeListingURL = "https://toolbox-data.anchore.io/grype/databases/listing.json"
|
||||
defaultDBDirName = "grypedb"
|
||||
)
|
||||
|
||||
type RegistryCredentials struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (c RegistryCredentials) IsEmpty() bool {
|
||||
return c.Username == "" || c.Password == ""
|
||||
}
|
||||
|
||||
// ExceedsSeverityThreshold returns true if vulnerabilities in the scan results exceed the severity threshold, false otherwise.
|
||||
//
|
||||
// Values equal to the threshold are considered failing, too.
|
||||
func ExceedsSeverityThreshold(scanResults *models.PresenterConfig, severity vulnerability.Severity) bool {
|
||||
return grype.HasSeverityAtOrAbove(scanResults.MetadataProvider, severity, scanResults.Matches)
|
||||
}
|
||||
|
||||
func NewDefaultDBConfig() (db.Config, bool) {
|
||||
dir := "~/.test-deleteme/"
|
||||
dir := filepath.Join(xdg.CacheHome, defaultDBDirName)
|
||||
url := defaultGrypeListingURL
|
||||
shouldUpdate := true
|
||||
|
||||
@@ -38,66 +58,6 @@ func NewDefaultDBConfig() (db.Config, bool) {
|
||||
}, shouldUpdate
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
dbCfg db.Config
|
||||
}
|
||||
|
||||
func (s *Service) Scan(ctx context.Context, userInput string) (*models.PresenterConfig, error) {
|
||||
var err error
|
||||
|
||||
errs := make(chan error)
|
||||
|
||||
store, status, dbCloser, err := grype.LoadVulnerabilityDB(s.dbCfg, true)
|
||||
if err = validateDBLoad(err, status); err != nil {
|
||||
errs <- err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packages, pkgContext, sbom, err := pkg.Provide(userInput, getProviderConfig())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dbCloser != nil {
|
||||
defer dbCloser.Close()
|
||||
}
|
||||
|
||||
// applyDistroHint(packages, &pkgContext, appConfig)
|
||||
|
||||
vulnMatcher := grype.VulnerabilityMatcher{
|
||||
Store: *store,
|
||||
Matchers: getMatchers(),
|
||||
}
|
||||
|
||||
remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(packages, pkgContext)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pb := models.PresenterConfig{
|
||||
Matches: *remainingMatches,
|
||||
IgnoredMatches: ignoredMatches,
|
||||
Packages: packages,
|
||||
Context: pkgContext,
|
||||
MetadataProvider: store,
|
||||
SBOM: sbom,
|
||||
AppConfig: nil,
|
||||
DBStatus: status,
|
||||
}
|
||||
return &pb, nil
|
||||
}
|
||||
|
||||
func NewVulnerabilityDB(cfg db.Config, update bool) (*store.Store, *db.Status, *db.Closer, error) {
|
||||
return grype.LoadVulnerabilityDB(cfg, update)
|
||||
}
|
||||
|
||||
func NewScanService(dbCfg db.Config) Service {
|
||||
return Service{dbCfg: dbCfg}
|
||||
}
|
||||
|
||||
func getMatchers() []matcher.Matcher {
|
||||
return matcher.NewDefaultMatchers(
|
||||
matcher.Config{
|
||||
@@ -128,13 +88,13 @@ func validateDBLoad(loadErr error, status *db.Status) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getProviderConfig() pkg.ProviderConfig {
|
||||
func getProviderConfig(creds RegistryCredentials) pkg.ProviderConfig {
|
||||
syftCreds := []image.RegistryCredentials{{Username: creds.Username, Password: creds.Password}}
|
||||
regOpts := &image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: true,
|
||||
// InsecureUseHTTP: true,
|
||||
Credentials: syftCreds,
|
||||
}
|
||||
catOpts := cataloger.DefaultConfig()
|
||||
return pkg.ProviderConfig{
|
||||
pc := pkg.ProviderConfig{
|
||||
SyftProviderConfig: pkg.SyftProviderConfig{
|
||||
RegistryOptions: regOpts,
|
||||
CatalogingOptions: catOpts,
|
||||
@@ -146,4 +106,71 @@ func getProviderConfig() pkg.ProviderConfig {
|
||||
GenerateMissingCPEs: true,
|
||||
},
|
||||
}
|
||||
return pc
|
||||
}
|
||||
|
||||
// Service is a facade for image scanning functionality.
|
||||
//
|
||||
// It performs image scanning and everything needed in between.
|
||||
type Service struct {
|
||||
dbCfg db.Config
|
||||
}
|
||||
|
||||
func (s *Service) Scan(ctx context.Context, userInput string, creds RegistryCredentials) (*models.PresenterConfig, error) {
|
||||
var err error
|
||||
|
||||
store, status, dbCloser, err := grype.LoadVulnerabilityDB(s.dbCfg, true)
|
||||
if err = validateDBLoad(err, status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packages, pkgContext, sbom, err := pkg.Provide(userInput, getProviderConfig(creds))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dbCloser != nil {
|
||||
defer dbCloser.Close()
|
||||
}
|
||||
|
||||
// applyDistroHint(packages, &pkgContext, appConfig)
|
||||
|
||||
matcher := grype.VulnerabilityMatcher{
|
||||
Store: *store,
|
||||
Matchers: getMatchers(),
|
||||
}
|
||||
|
||||
remainingMatches, ignoredMatches, err := matcher.FindMatches(packages, pkgContext)
|
||||
if err != nil {
|
||||
if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pb := models.PresenterConfig{
|
||||
Matches: *remainingMatches,
|
||||
IgnoredMatches: ignoredMatches,
|
||||
Packages: packages,
|
||||
Context: pkgContext,
|
||||
MetadataProvider: store,
|
||||
SBOM: sbom,
|
||||
AppConfig: nil,
|
||||
DBStatus: status,
|
||||
}
|
||||
return &pb, nil
|
||||
}
|
||||
|
||||
func NewVulnerabilityDB(cfg db.Config, update bool) (*store.Store, *db.Status, *db.Closer, error) {
|
||||
return grype.LoadVulnerabilityDB(cfg, update)
|
||||
}
|
||||
|
||||
func NewScanService(dbCfg db.Config) Service {
|
||||
return Service{dbCfg: dbCfg}
|
||||
}
|
||||
|
||||
// ParseSeverity returns a Grype severity given a severity string
|
||||
//
|
||||
// Used as a thin wrapper for ease of access from one image scan package
|
||||
func ParseSeverity(severity string) vulnerability.Severity {
|
||||
return vulnerability.ParseSeverity(severity)
|
||||
}
|
||||
|
||||
+182
-24
@@ -2,26 +2,19 @@ package imagescan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/grype/grype"
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
"github.com/anchore/grype/grype/db"
|
||||
grypedb "github.com/anchore/grype/grype/db/v5"
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewVulnerabilityDBMatchesGrype(t *testing.T) {
|
||||
dbCfg, shouldUpdate := NewDefaultDBConfig()
|
||||
|
||||
wantStore, wantStatus, wantCloser, wantErr := grype.LoadVulnerabilityDB(dbCfg, shouldUpdate)
|
||||
gotStore, gotStatus, gotCloser, gotErr := NewVulnerabilityDB(dbCfg, shouldUpdate)
|
||||
|
||||
assert.Equal(t, wantStore, gotStore)
|
||||
assert.Equal(t, wantStatus, gotStatus)
|
||||
assert.Equal(t, wantCloser, gotCloser)
|
||||
assert.Equal(t, wantErr, gotErr)
|
||||
}
|
||||
|
||||
func TestNewScanService(t *testing.T) {
|
||||
dbCfg, _ := NewDefaultDBConfig()
|
||||
|
||||
@@ -30,34 +23,199 @@ func TestNewScanService(t *testing.T) {
|
||||
assert.IsType(t, Service{}, svc)
|
||||
}
|
||||
|
||||
func TestRegistryCredentials(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Valid credentials should not be empty",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Empty username should be considered empty credentials",
|
||||
username: "",
|
||||
password: "pass",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Empty password should be considered empty credentials",
|
||||
username: "user",
|
||||
password: "",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Empty user and password should be considered empty credentials",
|
||||
username: "",
|
||||
password: "",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
creds := RegistryCredentials{Username: tc.username, Password: tc.password}
|
||||
|
||||
got := creds.IsEmpty()
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScan(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
image string
|
||||
creds RegistryCredentials
|
||||
}{
|
||||
{
|
||||
name: "performs as scan",
|
||||
name: "Valid image name produces a non-nil scan result",
|
||||
image: "nginx",
|
||||
},
|
||||
{
|
||||
name: "Scanning a valid image with provided credentials should produce a non-nil scan result",
|
||||
image: "nginx",
|
||||
creds: RegistryCredentials{
|
||||
Username: "test",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dbCfg, _ := NewDefaultDBConfig()
|
||||
|
||||
svc := NewScanService(dbCfg)
|
||||
creds := RegistryCredentials{}
|
||||
|
||||
scanResults, err := svc.Scan(ctx, tc.image)
|
||||
scanResults, err := svc.Scan(ctx, tc.image, creds)
|
||||
|
||||
presenterConfig, _ := presenter.ValidatedConfig("json", "", false)
|
||||
pres := presenter.GetPresenter(presenterConfig, *scanResults)
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, &models.PresenterConfig{}, scanResults)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
pres.Present(&out)
|
||||
// fakeMetaProvider is a test double that fakes an actual MetadataProvider
|
||||
type fakeMetaProvider struct {
|
||||
vulnerabilities map[string]map[string][]grypedb.Vulnerability
|
||||
metadata map[string]map[string]*grypedb.VulnerabilityMetadata
|
||||
}
|
||||
|
||||
t.Logf("Res: %v, err: %v", out.String(), err)
|
||||
assert.NotNil(t, scanResults)
|
||||
func newFakeMetaProvider() *fakeMetaProvider {
|
||||
d := fakeMetaProvider{
|
||||
vulnerabilities: make(map[string]map[string][]grypedb.Vulnerability),
|
||||
metadata: make(map[string]map[string]*grypedb.VulnerabilityMetadata),
|
||||
}
|
||||
d.fillWithData()
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *fakeMetaProvider) GetAllVulnerabilityMetadata() (*[]grypedb.VulnerabilityMetadata, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *fakeMetaProvider) GetVulnerabilityMatchExclusion(id string) ([]grypedb.VulnerabilityMatchExclusion, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *fakeMetaProvider) GetVulnerabilityMetadata(id, namespace string) (*grypedb.VulnerabilityMetadata, error) {
|
||||
return d.metadata[id][namespace], nil
|
||||
}
|
||||
|
||||
func (d *fakeMetaProvider) fillWithData() {
|
||||
d.metadata["CVE-2014-fake-1"] = map[string]*grypedb.VulnerabilityMetadata{
|
||||
"debian:distro:debian:8": {
|
||||
ID: "CVE-2014-fake-1",
|
||||
Namespace: "debian:distro:debian:8",
|
||||
Severity: "medium",
|
||||
},
|
||||
}
|
||||
|
||||
d.vulnerabilities["debian:distro:debian:8"] = map[string][]grypedb.Vulnerability{
|
||||
"neutron": {
|
||||
{
|
||||
PackageName: "neutron",
|
||||
Namespace: "debian:distro:debian:8",
|
||||
VersionConstraint: "< 2014.1.3-6",
|
||||
ID: "CVE-2014-fake-1",
|
||||
VersionFormat: "deb",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestExceedsSeverityThreshold(t *testing.T) {
|
||||
thePkg := pkg.Package{
|
||||
ID: pkg.ID(uuid.NewString()),
|
||||
Name: "the-package",
|
||||
Version: "v0.1",
|
||||
Type: syftPkg.RpmPkg,
|
||||
}
|
||||
|
||||
matches := match.NewMatches()
|
||||
matches.Add(match.Match{
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-2014-fake-1",
|
||||
Namespace: "debian:distro:debian:8",
|
||||
},
|
||||
Package: thePkg,
|
||||
Details: match.Details{
|
||||
{
|
||||
Type: match.ExactDirectMatch,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
failOnSeverity string
|
||||
matches match.Matches
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
name: "No severity set should pass",
|
||||
failOnSeverity: "",
|
||||
matches: matches,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "Fail severity higher than vulnerability should not fail",
|
||||
failOnSeverity: "high",
|
||||
matches: matches,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "Fail severity equal to vulnerability should fail",
|
||||
failOnSeverity: "medium",
|
||||
matches: matches,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "Fail severity below found vuln should fail",
|
||||
failOnSeverity: "low",
|
||||
matches: matches,
|
||||
expectedResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
metadataProvider := db.NewVulnerabilityMetadataProvider(newFakeMetaProvider())
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
scanResults := &models.PresenterConfig{
|
||||
Matches: tc.matches,
|
||||
MetadataProvider: metadataProvider,
|
||||
}
|
||||
inputSeverity := vulnerability.ParseSeverity(tc.failOnSeverity)
|
||||
ours := ExceedsSeverityThreshold(scanResults, inputSeverity)
|
||||
|
||||
assert.Equal(t, tc.expectedResult, ours)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user