k9s/internal/vul/scanner.go

264 lines
6.6 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package vul
import (
"context"
"errors"
"fmt"
"log/slog"
"sync"
"time"
"github.com/anchore/clio"
"github.com/anchore/grype/cmd/grype/cli/options"
"github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/matcher/dotnet"
"github.com/anchore/grype/grype/matcher/golang"
"github.com/anchore/grype/grype/matcher/java"
"github.com/anchore/grype/grype/matcher/javascript"
"github.com/anchore/grype/grype/matcher/python"
"github.com/anchore/grype/grype/matcher/ruby"
"github.com/anchore/grype/grype/matcher/stock"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vex"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/slogs"
)
var ImgScanner *imageScanner
const (
imgChanSize = 3
imgScanTimeout = 2 * time.Second
scanConcurrency = 2
)
type imageScanner struct {
provider vulnerability.Provider
status *vulnerability.ProviderStatus
opts *options.Grype
scans Scans
mx sync.RWMutex
initialized bool
config config.ImageScans
log *slog.Logger
}
// NewImageScanner returns a new instance.
func NewImageScanner(cfg config.ImageScans, l *slog.Logger) *imageScanner {
return &imageScanner{
scans: make(Scans),
config: cfg,
log: l.With(slogs.Subsys, "vul"),
}
}
func (s *imageScanner) ShouldExcludes(ns string, lbls map[string]string) bool {
return s.config.ShouldExclude(ns, lbls)
}
// GetScan fetch scan for a given image. Returns ok=false when not found.
func (s *imageScanner) GetScan(img string) (*Scan, bool) {
s.mx.RLock()
defer s.mx.RUnlock()
scan, ok := s.scans[img]
return scan, ok
}
func (s *imageScanner) setScan(img string, sc *Scan) {
s.mx.Lock()
defer s.mx.Unlock()
s.scans[img] = sc
}
// Init initializes image vulnerability database.
func (s *imageScanner) Init(name, version string) {
defer func(t time.Time) {
slog.Debug("VulDb initialization complete",
slogs.Elapsed, time.Since(t),
)
}(time.Now())
opts := options.DefaultGrype(clio.Identification{Name: name, Version: version})
opts.GenerateMissingCPEs = true
provider, status, err := grype.LoadVulnerabilityDB(
opts.ToClientConfig(),
opts.ToCuratorConfig(),
opts.DB.AutoUpdate,
)
if err != nil {
s.log.Error("VulDb load failed", slogs.Error, err)
return
}
s.mx.Lock()
s.opts, s.provider, s.status = opts, provider, status
s.mx.Unlock()
if e := validateDBLoad(err, status); e != nil {
s.log.Error("VulDb validate failed", slogs.Error, e)
return
}
s.mx.Lock()
s.initialized = true
s.mx.Unlock()
slog.Debug("VulDB initialized")
}
// Stop closes scan database.
func (s *imageScanner) Stop() {
s.mx.RLock()
defer s.mx.RUnlock()
if s.provider != nil {
_ = s.provider.Close()
s.provider = nil
}
}
func (s *imageScanner) Score(ii ...string) string {
var sc scorer
for _, i := range ii {
if scan, ok := s.GetScan(i); ok {
sc = sc.Add(newScorer(scan.Tally))
}
}
return sc.String()
}
func (s *imageScanner) IsInitialized() bool {
s.mx.RLock()
defer s.mx.RUnlock()
return s.initialized
}
func (s *imageScanner) Enqueue(ctx context.Context, images ...string) {
ctx, cancel := context.WithTimeout(ctx, imgScanTimeout)
defer cancel()
for _, img := range images {
if _, ok := s.GetScan(img); ok {
continue
}
go s.scanWorker(ctx, img)
}
}
func (s *imageScanner) scanWorker(ctx context.Context, img string) {
defer s.log.Debug("ScanWorker bailing out!")
s.log.Debug("ScanWorker processing image", slogs.Image, img)
sc := newScan(img)
s.setScan(img, sc)
if err := s.scan(ctx, img, sc); err != nil {
s.log.Warn("Scan failed for image",
slogs.Image, img,
slogs.Error, err,
)
}
}
func (s *imageScanner) scan(_ context.Context, img string, sc *Scan) error {
defer func(t time.Time) {
s.log.Debug("[Vulscan] perf",
slogs.Image, img,
slogs.Elapsed, time.Since(t),
)
}(time.Now())
packages, pkgContext, _, err := pkg.Provide(img, getProviderConfig(s.opts))
if err != nil {
return fmt.Errorf("failed to analyze image packages: %w", err)
}
processor, err := vex.NewProcessor(vex.ProcessorOptions{
Documents: s.opts.VexDocuments,
IgnoreRules: s.opts.Ignore,
})
if err != nil {
return fmt.Errorf("failed to create VEX processor: %w", err)
}
v := grype.VulnerabilityMatcher{
VulnerabilityProvider: s.provider,
IgnoreRules: s.opts.Ignore,
NormalizeByCVE: s.opts.ByCVE,
FailSeverity: s.opts.FailOnSeverity(),
Matchers: getMatchers(s.opts),
VexProcessor: processor,
}
var errs error
mm, _, err := v.FindMatches(packages, pkgContext)
if err != nil {
errs = errors.Join(errs, err)
}
if err := sc.run(mm, s.provider); err != nil {
errs = errors.Join(errs, err)
}
return errs
}
func getProviderConfig(opts *options.Grype) pkg.ProviderConfig {
return pkg.ProviderConfig{
SyftProviderConfig: pkg.SyftProviderConfig{
SBOMOptions: syft.DefaultCreateSBOMConfig(),
RegistryOptions: opts.Registry.ToOptions(),
Exclusions: opts.Exclusions,
Platform: opts.Platform,
Name: opts.Name,
DefaultImagePullSource: opts.DefaultImagePullSource,
},
SynthesisConfig: pkg.SynthesisConfig{
GenerateMissingCPEs: opts.GenerateMissingCPEs,
},
}
}
func getMatchers(opts *options.Grype) []match.Matcher {
return matcher.NewDefaultMatchers(
matcher.Config{
Java: java.MatcherConfig{
ExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(),
UseCPEs: opts.Match.Java.UseCPEs,
},
Ruby: ruby.MatcherConfig(opts.Match.Ruby),
Python: python.MatcherConfig(opts.Match.Python),
Dotnet: dotnet.MatcherConfig(opts.Match.Dotnet),
Javascript: javascript.MatcherConfig(opts.Match.Javascript),
Golang: golang.MatcherConfig{
UseCPEs: opts.Match.Golang.UseCPEs,
AlwaysUseCPEForStdlib: opts.Match.Golang.AlwaysUseCPEForStdlib,
AllowMainModulePseudoVersionComparison: opts.Match.Golang.AllowMainModulePseudoVersionComparison,
},
Stock: stock.MatcherConfig(opts.Match.Stock),
},
)
}
func validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error {
if loadErr != nil {
return fmt.Errorf("failed to load vulnerability db: %w", loadErr)
}
if status == nil {
return fmt.Errorf("unable to determine the status of the vulnerability db")
}
if status.Error != nil {
return fmt.Errorf("db could not be loaded: %w", status.Error)
}
return nil
}