github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/db/db.go (about)

     1  package db
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
    10  	"golang.org/x/xerrors"
    11  	"k8s.io/utils/clock"
    12  
    13  	"github.com/aquasecurity/trivy-db/pkg/db"
    14  	"github.com/aquasecurity/trivy-db/pkg/metadata"
    15  	"github.com/devseccon/trivy/pkg/fanal/types"
    16  	"github.com/devseccon/trivy/pkg/log"
    17  	"github.com/devseccon/trivy/pkg/oci"
    18  )
    19  
    20  const (
    21  	dbMediaType         = "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip"
    22  	defaultDBRepository = "ghcr.io/aquasecurity/trivy-db"
    23  )
    24  
    25  // Operation defines the DB operations
    26  type Operation interface {
    27  	NeedsUpdate(cliVersion string, skip bool) (need bool, err error)
    28  	Download(ctx context.Context, dst string, opt types.RegistryOptions) (err error)
    29  }
    30  
    31  type options struct {
    32  	artifact     *oci.Artifact
    33  	clock        clock.Clock
    34  	dbRepository string
    35  }
    36  
    37  // Option is a functional option
    38  type Option func(*options)
    39  
    40  // WithOCIArtifact takes an OCI artifact
    41  func WithOCIArtifact(art *oci.Artifact) Option {
    42  	return func(opts *options) {
    43  		opts.artifact = art
    44  	}
    45  }
    46  
    47  // WithDBRepository takes a dbRepository
    48  func WithDBRepository(dbRepository string) Option {
    49  	return func(opts *options) {
    50  		opts.dbRepository = dbRepository
    51  	}
    52  }
    53  
    54  // WithClock takes a clock
    55  func WithClock(c clock.Clock) Option {
    56  	return func(opts *options) {
    57  		opts.clock = c
    58  	}
    59  }
    60  
    61  // Client implements DB operations
    62  type Client struct {
    63  	*options
    64  
    65  	cacheDir string
    66  	metadata metadata.Client
    67  	quiet    bool
    68  }
    69  
    70  // NewClient is the factory method for DB client
    71  func NewClient(cacheDir string, quiet bool, opts ...Option) *Client {
    72  	o := &options{
    73  		clock:        clock.RealClock{},
    74  		dbRepository: defaultDBRepository,
    75  	}
    76  
    77  	for _, opt := range opts {
    78  		opt(o)
    79  	}
    80  
    81  	return &Client{
    82  		options:  o,
    83  		cacheDir: cacheDir,
    84  		metadata: metadata.NewClient(cacheDir),
    85  		quiet:    quiet,
    86  	}
    87  }
    88  
    89  // NeedsUpdate check is DB needs update
    90  func (c *Client) NeedsUpdate(cliVersion string, skip bool) (bool, error) {
    91  	meta, err := c.metadata.Get()
    92  	if err != nil {
    93  		log.Logger.Debugf("There is no valid metadata file: %s", err)
    94  		if skip {
    95  			log.Logger.Error("The first run cannot skip downloading DB")
    96  			return false, xerrors.New("--skip-update cannot be specified on the first run")
    97  		}
    98  		meta = metadata.Metadata{Version: db.SchemaVersion}
    99  	}
   100  
   101  	if db.SchemaVersion < meta.Version {
   102  		log.Logger.Errorf("Trivy version (%s) is old. Update to the latest version.", cliVersion)
   103  		return false, xerrors.Errorf("the version of DB schema doesn't match. Local DB: %d, Expected: %d",
   104  			meta.Version, db.SchemaVersion)
   105  	}
   106  
   107  	if skip {
   108  		log.Logger.Debug("Skipping DB update...")
   109  		if err = c.validate(meta); err != nil {
   110  			return false, xerrors.Errorf("validate error: %w", err)
   111  		}
   112  		return false, nil
   113  	}
   114  
   115  	if db.SchemaVersion != meta.Version {
   116  		log.Logger.Debugf("The local DB schema version (%d) does not match with supported version schema (%d).", meta.Version, db.SchemaVersion)
   117  		return true, nil
   118  	}
   119  
   120  	return !c.isNewDB(meta), nil
   121  }
   122  
   123  func (c *Client) validate(meta metadata.Metadata) error {
   124  	if db.SchemaVersion != meta.Version {
   125  		log.Logger.Error("The local DB has an old schema version which is not supported by the current version of Trivy CLI. DB needs to be updated.")
   126  		return xerrors.Errorf("--skip-update cannot be specified with the old DB schema. Local DB: %d, Expected: %d",
   127  			meta.Version, db.SchemaVersion)
   128  	}
   129  	return nil
   130  }
   131  
   132  func (c *Client) isNewDB(meta metadata.Metadata) bool {
   133  	if c.clock.Now().Before(meta.NextUpdate) {
   134  		log.Logger.Debug("DB update was skipped because the local DB is the latest")
   135  		return true
   136  	}
   137  
   138  	if c.clock.Now().Before(meta.DownloadedAt.Add(time.Hour)) {
   139  		log.Logger.Debug("DB update was skipped because the local DB was downloaded during the last hour")
   140  		return true
   141  	}
   142  	return false
   143  }
   144  
   145  // Download downloads the DB file
   146  func (c *Client) Download(ctx context.Context, dst string, opt types.RegistryOptions) error {
   147  	// Remove the metadata file under the cache directory before downloading DB
   148  	if err := c.metadata.Delete(); err != nil {
   149  		log.Logger.Debug("no metadata file")
   150  	}
   151  
   152  	art, err := c.initOCIArtifact(opt)
   153  	if err != nil {
   154  		return xerrors.Errorf("OCI artifact error: %w", err)
   155  	}
   156  
   157  	if err = art.Download(ctx, db.Dir(dst), oci.DownloadOption{MediaType: dbMediaType}); err != nil {
   158  		return xerrors.Errorf("database download error: %w", err)
   159  	}
   160  
   161  	if err = c.updateDownloadedAt(dst); err != nil {
   162  		return xerrors.Errorf("failed to update downloaded_at: %w", err)
   163  	}
   164  	return nil
   165  }
   166  
   167  func (c *Client) updateDownloadedAt(dst string) error {
   168  	log.Logger.Debug("Updating database metadata...")
   169  
   170  	// We have to initialize a metadata client here
   171  	// since the destination may be different from the cache directory.
   172  	client := metadata.NewClient(dst)
   173  	meta, err := client.Get()
   174  	if err != nil {
   175  		return xerrors.Errorf("unable to get metadata: %w", err)
   176  	}
   177  
   178  	meta.DownloadedAt = c.clock.Now().UTC()
   179  	if err = client.Update(meta); err != nil {
   180  		return xerrors.Errorf("failed to update metadata: %w", err)
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  func (c *Client) initOCIArtifact(opt types.RegistryOptions) (*oci.Artifact, error) {
   187  	if c.artifact != nil {
   188  		return c.artifact, nil
   189  	}
   190  
   191  	repo := fmt.Sprintf("%s:%d", c.dbRepository, db.SchemaVersion)
   192  	art, err := oci.NewArtifact(repo, c.quiet, opt)
   193  	if err != nil {
   194  		var terr *transport.Error
   195  		if errors.As(err, &terr) {
   196  			for _, diagnostic := range terr.Errors {
   197  				// For better user experience
   198  				if diagnostic.Code == transport.DeniedErrorCode || diagnostic.Code == transport.UnauthorizedErrorCode {
   199  					log.Logger.Warn("See https://aquasecurity.github.io/trivy/latest/docs/references/troubleshooting/#db")
   200  					break
   201  				}
   202  			}
   203  		}
   204  		return nil, xerrors.Errorf("OCI artifact error: %w", err)
   205  	}
   206  	return art, nil
   207  }