mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 23:00:42 -08:00
209 lines
5.7 KiB
Go
209 lines
5.7 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
|
"golang.org/x/xerrors"
|
|
"k8s.io/utils/clock"
|
|
|
|
"github.com/aquasecurity/trivy-db/pkg/db"
|
|
"github.com/aquasecurity/trivy-db/pkg/metadata"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
"github.com/aquasecurity/trivy/pkg/oci"
|
|
)
|
|
|
|
const (
|
|
dbMediaType = "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip"
|
|
defaultDBRepository = "ghcr.io/aquasecurity/trivy-db"
|
|
)
|
|
|
|
// Operation defines the DB operations
|
|
type Operation interface {
|
|
NeedsUpdate(cliVersion string, skip bool) (need bool, err error)
|
|
Download(ctx context.Context, dst string) (err error)
|
|
}
|
|
|
|
type options struct {
|
|
artifact *oci.Artifact
|
|
clock clock.Clock
|
|
dbRepository string
|
|
}
|
|
|
|
// Option is a functional option
|
|
type Option func(*options)
|
|
|
|
// WithOCIArtifact takes an OCI artifact
|
|
func WithOCIArtifact(art *oci.Artifact) Option {
|
|
return func(opts *options) {
|
|
opts.artifact = art
|
|
}
|
|
}
|
|
|
|
// WithDBRepository takes a dbRepository
|
|
func WithDBRepository(dbRepository string) Option {
|
|
return func(opts *options) {
|
|
opts.dbRepository = dbRepository
|
|
}
|
|
}
|
|
|
|
// WithClock takes a clock
|
|
func WithClock(clock clock.Clock) Option {
|
|
return func(opts *options) {
|
|
opts.clock = clock
|
|
}
|
|
}
|
|
|
|
// Client implements DB operations
|
|
type Client struct {
|
|
*options
|
|
|
|
cacheDir string
|
|
metadata metadata.Client
|
|
quiet bool
|
|
insecureSkipTLSVerify bool
|
|
}
|
|
|
|
// NewClient is the factory method for DB client
|
|
func NewClient(cacheDir string, quiet, insecure bool, opts ...Option) *Client {
|
|
o := &options{
|
|
clock: clock.RealClock{},
|
|
dbRepository: defaultDBRepository,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(o)
|
|
}
|
|
|
|
return &Client{
|
|
options: o,
|
|
cacheDir: cacheDir,
|
|
metadata: metadata.NewClient(cacheDir),
|
|
quiet: quiet,
|
|
insecureSkipTLSVerify: insecure, // insecure skip for download DB
|
|
}
|
|
}
|
|
|
|
// NeedsUpdate check is DB needs update
|
|
func (c *Client) NeedsUpdate(cliVersion string, skip bool) (bool, error) {
|
|
meta, err := c.metadata.Get()
|
|
if err != nil {
|
|
log.Logger.Debugf("There is no valid metadata file: %s", err)
|
|
if skip {
|
|
log.Logger.Error("The first run cannot skip downloading DB")
|
|
return false, xerrors.New("--skip-update cannot be specified on the first run")
|
|
}
|
|
meta = metadata.Metadata{Version: db.SchemaVersion}
|
|
}
|
|
|
|
if db.SchemaVersion < meta.Version {
|
|
log.Logger.Errorf("Trivy version (%s) is old. Update to the latest version.", cliVersion)
|
|
return false, xerrors.Errorf("the version of DB schema doesn't match. Local DB: %d, Expected: %d",
|
|
meta.Version, db.SchemaVersion)
|
|
}
|
|
|
|
if skip {
|
|
log.Logger.Debug("Skipping DB update...")
|
|
if err = c.validate(meta); err != nil {
|
|
return false, xerrors.Errorf("validate error: %w", err)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
if db.SchemaVersion != meta.Version {
|
|
log.Logger.Debugf("The local DB schema version (%d) does not match with supported version schema (%d).", meta.Version, db.SchemaVersion)
|
|
return true, nil
|
|
}
|
|
|
|
return !c.isNewDB(meta), nil
|
|
}
|
|
|
|
func (c *Client) validate(meta metadata.Metadata) error {
|
|
if db.SchemaVersion != meta.Version {
|
|
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.")
|
|
return xerrors.Errorf("--skip-update cannot be specified with the old DB schema. Local DB: %d, Expected: %d",
|
|
meta.Version, db.SchemaVersion)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) isNewDB(meta metadata.Metadata) bool {
|
|
if c.clock.Now().Before(meta.NextUpdate) {
|
|
log.Logger.Debug("DB update was skipped because the local DB is the latest")
|
|
return true
|
|
}
|
|
|
|
if c.clock.Now().Before(meta.DownloadedAt.Add(time.Hour)) {
|
|
log.Logger.Debug("DB update was skipped because the local DB was downloaded during the last hour")
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Download downloads the DB file
|
|
func (c *Client) Download(ctx context.Context, dst string) error {
|
|
// Remove the metadata file under the cache directory before downloading DB
|
|
if err := c.metadata.Delete(); err != nil {
|
|
log.Logger.Debug("no metadata file")
|
|
}
|
|
|
|
art, err := c.initOCIArtifact()
|
|
if err != nil {
|
|
return xerrors.Errorf("OCI artifact error: %w", err)
|
|
}
|
|
|
|
if err = art.Download(ctx, db.Dir(dst)); err != nil {
|
|
return xerrors.Errorf("database download error: %w", err)
|
|
}
|
|
|
|
if err = c.updateDownloadedAt(dst); err != nil {
|
|
return xerrors.Errorf("failed to update downloaded_at: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) updateDownloadedAt(dst string) error {
|
|
log.Logger.Debug("Updating database metadata...")
|
|
|
|
// We have to initialize a metadata client here
|
|
// since the destination may be different from the cache directory.
|
|
client := metadata.NewClient(dst)
|
|
meta, err := client.Get()
|
|
if err != nil {
|
|
return xerrors.Errorf("unable to get metadata: %w", err)
|
|
}
|
|
|
|
meta.DownloadedAt = c.clock.Now().UTC()
|
|
if err = client.Update(meta); err != nil {
|
|
return xerrors.Errorf("failed to update metadata: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) initOCIArtifact() (*oci.Artifact, error) {
|
|
if c.artifact != nil {
|
|
return c.artifact, nil
|
|
}
|
|
|
|
repo := fmt.Sprintf("%s:%d", c.dbRepository, db.SchemaVersion)
|
|
art, err := oci.NewArtifact(repo, dbMediaType, c.quiet, c.insecureSkipTLSVerify)
|
|
if err != nil {
|
|
var terr *transport.Error
|
|
if errors.As(err, &terr) {
|
|
for _, diagnostic := range terr.Errors {
|
|
// For better user experience
|
|
if diagnostic.Code == transport.DeniedErrorCode || diagnostic.Code == transport.UnauthorizedErrorCode {
|
|
log.Logger.Warn("See https://aquasecurity.github.io/trivy/latest/docs/references/troubleshooting/#db")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return nil, xerrors.Errorf("OCI artifact error: %w", err)
|
|
}
|
|
return art, nil
|
|
}
|