Merge branch 'feat-image-scan-svc' of github.com:kubescape/kubescape into feat-image-scan-svc

This commit is contained in:
Daniel Grunberger
2023-07-25 17:00:12 +03:00
38 changed files with 42084 additions and 875 deletions
+92 -65
View File
@@ -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
View File
@@ -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)
})
}
}