1package db 2 3import ( 4 "compress/gzip" 5 "context" 6 "encoding/json" 7 "io" 8 "os" 9 "path/filepath" 10 11 "github.com/google/wire" 12 "github.com/spf13/afero" 13 "golang.org/x/xerrors" 14 "k8s.io/utils/clock" 15 16 "github.com/aquasecurity/trivy-db/pkg/db" 17 "github.com/aquasecurity/trivy/pkg/github" 18 "github.com/aquasecurity/trivy/pkg/indicator" 19 "github.com/aquasecurity/trivy/pkg/log" 20) 21 22const ( 23 fullDB = "trivy.db.gz" 24 lightDB = "trivy-light.db.gz" 25 26 metadataFile = "metadata.json" 27) 28 29var SuperSet = wire.NewSet( 30 // indicator.ProgressBar 31 indicator.NewProgressBar, 32 33 // clock.Clock 34 wire.Struct(new(clock.RealClock)), 35 wire.Bind(new(clock.Clock), new(clock.RealClock)), 36 37 // db.Config 38 wire.Struct(new(db.Config)), 39 wire.Bind(new(dbOperation), new(db.Config)), 40 41 // github.Client 42 github.NewClient, 43 wire.Bind(new(github.Operation), new(github.Client)), 44 45 // Metadata 46 afero.NewOsFs, 47 NewMetadata, 48 49 // db.Client 50 NewClient, 51 wire.Bind(new(Operation), new(Client)), 52) 53 54type Operation interface { 55 NeedsUpdate(cliVersion string, skip, light bool) (need bool, err error) 56 Download(ctx context.Context, cacheDir string, light bool) (err error) 57 UpdateMetadata(cacheDir string) (err error) 58} 59 60type dbOperation interface { 61 GetMetadata() (metadata db.Metadata, err error) 62 StoreMetadata(metadata db.Metadata, dir string) (err error) 63} 64 65type Client struct { 66 dbc dbOperation 67 githubClient github.Operation 68 pb indicator.ProgressBar 69 clock clock.Clock 70 metadata Metadata 71} 72 73func NewClient(dbc dbOperation, githubClient github.Operation, pb indicator.ProgressBar, clock clock.Clock, metadata Metadata) Client { 74 return Client{ 75 dbc: dbc, 76 githubClient: githubClient, 77 pb: pb, 78 clock: clock, 79 metadata: metadata, 80 } 81} 82 83func (c Client) NeedsUpdate(cliVersion string, light, skip bool) (bool, error) { 84 dbType := db.TypeFull 85 if light { 86 dbType = db.TypeLight 87 } 88 89 metadata, err := c.metadata.Get() 90 if err != nil { 91 log.Logger.Debugf("There is no valid metadata file: %s", err) 92 if skip { 93 log.Logger.Error("The first run cannot skip downloading DB") 94 return false, xerrors.New("--skip-update cannot be specified on the first run") 95 } 96 metadata = db.Metadata{} // suppress a warning 97 } 98 99 if db.SchemaVersion < metadata.Version { 100 log.Logger.Errorf("Trivy version (%s) is old. Update to the latest version.", cliVersion) 101 return false, xerrors.Errorf("the version of DB schema doesn't match. Local DB: %d, Expected: %d", 102 metadata.Version, db.SchemaVersion) 103 } 104 105 if skip { 106 if db.SchemaVersion != metadata.Version { 107 log.Logger.Error("The local DB is old and needs to be updated") 108 return false, xerrors.New("--skip-update cannot be specified with the old DB") 109 } else if metadata.Type != dbType { 110 if dbType == db.TypeFull { 111 log.Logger.Error("The local DB is a lightweight DB. You have to download a full DB") 112 } else { 113 log.Logger.Error("The local DB is a full DB. You have to download a lightweight DB") 114 } 115 return false, xerrors.New("--skip-update cannot be specified with the different schema DB") 116 } 117 return false, nil 118 } 119 120 if db.SchemaVersion == metadata.Version && metadata.Type == dbType && 121 c.clock.Now().Before(metadata.NextUpdate) { 122 log.Logger.Debug("DB update was skipped because DB is the latest") 123 return false, nil 124 } 125 return true, nil 126} 127 128func (c Client) Download(ctx context.Context, cacheDir string, light bool) error { 129 // Remove the metadata file before downloading DB 130 if err := c.metadata.Delete(); err != nil { 131 log.Logger.Debug("no metadata file") 132 } 133 134 dbFile := fullDB 135 if light { 136 dbFile = lightDB 137 } 138 139 rc, size, err := c.githubClient.DownloadDB(ctx, dbFile) 140 if err != nil { 141 return xerrors.Errorf("failed to download vulnerability DB: %w", err) 142 } 143 defer rc.Close() 144 145 bar := c.pb.Start(int64(size)) 146 barReader := bar.NewProxyReader(rc) 147 defer bar.Finish() 148 149 gr, err := gzip.NewReader(barReader) 150 if err != nil { 151 return xerrors.Errorf("invalid gzip file: %w", err) 152 } 153 154 dbPath := db.Path(cacheDir) 155 dbDir := filepath.Dir(dbPath) 156 157 if err = os.MkdirAll(dbDir, 0700); err != nil { 158 return xerrors.Errorf("failed to mkdir: %w", err) 159 } 160 161 file, err := os.Create(dbPath) 162 if err != nil { 163 return xerrors.Errorf("unable to open DB file: %w", err) 164 } 165 defer file.Close() 166 167 if _, err = io.Copy(file, gr); err != nil { 168 return xerrors.Errorf("failed to save DB file: %w", err) 169 } 170 171 return nil 172} 173 174func (c Client) UpdateMetadata(cacheDir string) error { 175 log.Logger.Debug("Updating database metadata...") 176 177 // make sure the DB has been successfully downloaded 178 if err := db.Init(cacheDir); err != nil { 179 return xerrors.Errorf("DB error: %w", err) 180 } 181 defer db.Close() 182 183 metadata, err := c.dbc.GetMetadata() 184 if err != nil { 185 return xerrors.Errorf("unable to get metadata: %w", err) 186 } 187 188 if err = c.dbc.StoreMetadata(metadata, filepath.Join(cacheDir, "db")); err != nil { 189 return xerrors.Errorf("failed to store metadata: %w", err) 190 } 191 192 return nil 193} 194 195type Metadata struct { // TODO: Move all Metadata things to trivy-db repo 196 fs afero.Fs 197 filePath string 198} 199 200func NewMetadata(fs afero.Fs, cacheDir string) Metadata { 201 filePath := MetadataPath(cacheDir) 202 return Metadata{ 203 fs: fs, 204 filePath: filePath, 205 } 206} 207 208func MetadataPath(cacheDir string) string { 209 dbPath := db.Path(cacheDir) 210 dbDir := filepath.Dir(dbPath) 211 return filepath.Join(dbDir, metadataFile) 212} 213 214// DeleteMetadata deletes the file of database metadata 215func (m Metadata) Delete() error { 216 if err := m.fs.Remove(m.filePath); err != nil { 217 return xerrors.Errorf("unable to remove the metadata file: %w", err) 218 } 219 return nil 220} 221 222func (m Metadata) Get() (db.Metadata, error) { 223 f, err := m.fs.Open(m.filePath) 224 if err != nil { 225 return db.Metadata{}, xerrors.Errorf("unable to open a file: %w", err) 226 } 227 defer f.Close() 228 229 var metadata db.Metadata 230 if err = json.NewDecoder(f).Decode(&metadata); err != nil { 231 return db.Metadata{}, xerrors.Errorf("unable to decode metadata: %w", err) 232 } 233 return metadata, nil 234} 235