mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
246 lines
7.1 KiB
Go
246 lines
7.1 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"github.com/samber/lo"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/aquasecurity/trivy-db/pkg/db"
|
|
"github.com/aquasecurity/trivy-db/pkg/metadata"
|
|
"github.com/aquasecurity/trivy/pkg/clock"
|
|
"github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
"github.com/aquasecurity/trivy/pkg/oci"
|
|
)
|
|
|
|
const (
|
|
SchemaVersion = db.SchemaVersion
|
|
dbMediaType = "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip"
|
|
)
|
|
|
|
var (
|
|
// GitHub Container Registry
|
|
DefaultGHCRRepository = fmt.Sprintf("%s:%d", "ghcr.io/aquasecurity/trivy-db", db.SchemaVersion)
|
|
defaultGHCRRepository = lo.Must(name.NewTag(DefaultGHCRRepository))
|
|
|
|
// GCR mirror
|
|
DefaultGCRRepository = fmt.Sprintf("%s:%d", "mirror.gcr.io/aquasec/trivy-db", db.SchemaVersion)
|
|
defaultGCRRepository = lo.Must(name.NewTag(DefaultGCRRepository))
|
|
|
|
Init = db.Init
|
|
Close = db.Close
|
|
Path = db.Path
|
|
)
|
|
|
|
type options struct {
|
|
artifact *oci.Artifact
|
|
dbRepositories []name.Reference
|
|
}
|
|
|
|
// 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 []name.Reference) Option {
|
|
return func(opts *options) {
|
|
opts.dbRepositories = dbRepository
|
|
}
|
|
}
|
|
|
|
// Client implements DB operations
|
|
type Client struct {
|
|
*options
|
|
|
|
dbDir string
|
|
metadata metadata.Client
|
|
quiet bool
|
|
}
|
|
|
|
func Dir(cacheDir string) string {
|
|
return filepath.Join(cacheDir, "db")
|
|
}
|
|
|
|
// NewClient is the factory method for DB client
|
|
func NewClient(dbDir string, quiet bool, opts ...Option) *Client {
|
|
o := &options{
|
|
dbRepositories: []name.Reference{
|
|
defaultGCRRepository,
|
|
defaultGHCRRepository,
|
|
},
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(o)
|
|
}
|
|
|
|
return &Client{
|
|
options: o,
|
|
dbDir: dbDir,
|
|
metadata: metadata.NewClient(dbDir),
|
|
quiet: quiet,
|
|
}
|
|
}
|
|
|
|
// NeedsUpdate check is DB needs update
|
|
func (c *Client) NeedsUpdate(ctx context.Context, cliVersion string, skip bool) (bool, error) {
|
|
var noRequiredFiles bool
|
|
if _, err := os.Stat(db.Path(c.dbDir)); errors.Is(err, os.ErrNotExist) {
|
|
log.DebugContext(ctx, "There is no db file")
|
|
noRequiredFiles = true
|
|
}
|
|
meta, err := c.metadata.Get()
|
|
if err != nil {
|
|
log.DebugContext(ctx, "There is no valid metadata file", log.Err(err))
|
|
noRequiredFiles = true
|
|
|
|
meta = metadata.Metadata{Version: db.SchemaVersion}
|
|
}
|
|
|
|
// We can't use the DB if either `trivy.db` or `metadata.json` is missing.
|
|
// In that case, we need to download the DB.
|
|
if noRequiredFiles {
|
|
if skip {
|
|
log.ErrorContext(ctx, "The first run cannot skip downloading DB")
|
|
return false, xerrors.New("--skip-db-update cannot be specified on the first run")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// There are 3 cases when DownloadAt field is zero:
|
|
// - metadata file was not created yet. This is the first run of Trivy.
|
|
// - trivy-db was downloaded with `oras`. In this case user can use `--skip-db-update` (like for air-gapped) or re-download trivy-db.
|
|
// - trivy-db was corrupted while copying from tmp directory to cache directory. We should update this trivy-db.
|
|
// We can't detect these cases, so we will show warning for users who use oras + air-gapped.
|
|
if meta.DownloadedAt.IsZero() && !skip {
|
|
log.WarnContext(ctx, "Trivy DB may be corrupted and will be re-downloaded. If you manually downloaded DB - use the `--skip-db-update` flag to skip updating DB.")
|
|
return true, nil
|
|
}
|
|
|
|
if db.SchemaVersion < meta.Version {
|
|
log.ErrorContext(ctx, "Trivy version is old. Update to the latest version.", log.String("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 {
|
|
if err = c.validate(meta); err != nil {
|
|
return false, xerrors.Errorf("validate error: %w", err)
|
|
}
|
|
|
|
log.DebugContext(ctx, "Skipping DB update...")
|
|
return false, nil
|
|
}
|
|
|
|
if db.SchemaVersion != meta.Version {
|
|
log.DebugContext(ctx, "The local DB schema version does not match with supported version schema.",
|
|
log.Int("local_version", meta.Version), log.Int("supported_version", db.SchemaVersion))
|
|
return true, nil
|
|
}
|
|
|
|
return !c.isNewDB(ctx, meta), nil
|
|
}
|
|
|
|
func (c *Client) validate(meta metadata.Metadata) error {
|
|
if db.SchemaVersion != meta.Version {
|
|
log.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-db-update cannot be specified with the old DB schema. Local DB: %d, Expected: %d",
|
|
meta.Version, db.SchemaVersion)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) isNewDB(ctx context.Context, meta metadata.Metadata) bool {
|
|
now := clock.Now(ctx)
|
|
if now.Before(meta.NextUpdate) {
|
|
log.Debug("DB update was skipped because the local DB is the latest")
|
|
return true
|
|
}
|
|
|
|
if now.Before(meta.DownloadedAt.Add(time.Hour)) {
|
|
log.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, opt types.RegistryOptions) error {
|
|
if err := c.downloadDB(ctx, opt, dst); err != nil {
|
|
return xerrors.Errorf("OCI artifact error: %w", err)
|
|
}
|
|
|
|
if err := c.updateDownloadedAt(ctx, dst); err != nil {
|
|
return xerrors.Errorf("failed to update downloaded_at: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Clear(_ context.Context) error {
|
|
if err := os.RemoveAll(c.dbDir); err != nil {
|
|
return xerrors.Errorf("failed to remove vulnerability database: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) updateDownloadedAt(ctx context.Context, dbDir string) error {
|
|
log.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(dbDir)
|
|
meta, err := client.Get()
|
|
if err != nil {
|
|
return xerrors.Errorf("unable to get metadata: %w", err)
|
|
}
|
|
|
|
meta.DownloadedAt = clock.Now(ctx).UTC()
|
|
if err = client.Update(meta); err != nil {
|
|
return xerrors.Errorf("failed to update metadata: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) initArtifacts(opt types.RegistryOptions) oci.Artifacts {
|
|
if c.artifact != nil {
|
|
return oci.Artifacts{c.artifact}
|
|
}
|
|
return oci.NewArtifacts(c.dbRepositories, opt)
|
|
}
|
|
|
|
func (c *Client) downloadDB(ctx context.Context, opt types.RegistryOptions, dst string) error {
|
|
log.InfoContext(ctx, "Downloading vulnerability DB...")
|
|
downloadOpt := oci.DownloadOption{
|
|
MediaType: dbMediaType,
|
|
Quiet: c.quiet,
|
|
}
|
|
if err := c.initArtifacts(opt).Download(ctx, dst, downloadOpt); err != nil {
|
|
return xerrors.Errorf("failed to download vulnerability DB: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) ShowInfo() error {
|
|
meta, err := c.metadata.Get()
|
|
if err != nil {
|
|
return xerrors.Errorf("something wrong with DB: %w", err)
|
|
}
|
|
log.Debug("DB info", log.Int("schema", meta.Version), log.Time("updated_at", meta.UpdatedAt),
|
|
log.Time("next_update", meta.NextUpdate), log.Time("downloaded_at", meta.DownloadedAt))
|
|
return nil
|
|
}
|