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