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 }