diff --git a/enricher/epss/epss.go b/enricher/epss/epss.go new file mode 100644 index 000000000..7e4a851e1 --- /dev/null +++ b/enricher/epss/epss.go @@ -0,0 +1,108 @@ +package epss + +import ( + "context" + "encoding/json" + "fmt" + "github.com/google/uuid" + "github.com/quay/claircore" + "github.com/quay/claircore/libvuln/driver" + "github.com/quay/zlog" + "io" + "net/http" + "net/url" + "os" + "path" + "strings" + "time" +) + +var ( + _ driver.Enricher = (*Enricher)(nil) + _ driver.EnrichmentUpdater = (*Enricher)(nil) + + defaultFeed *url.URL +) + +const ( + // DefaultFeeds is the default place to look for EPSS feeds. + // epss_scores-YYYY-MM-DD.csv.gz needs to be specified to get all data + DefaultRootUrl = `https://epss.cyentia.com/` +) + +// Enricher provides EPSS data as enrichments to a VulnerabilityReport. +// +// Configure must be called before any other methods. +type Enricher struct { + driver.NoopUpdater + c *http.Client + feed *url.URL + feedPath string +} + +func (e Enricher) FetchEnrichment(ctx context.Context, fingerprint driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) { + ctx = zlog.ContextWithValues(ctx, "component", "enricher/epss/Enricher/FetchEnrichment") + currentDate := time.Now() + formattedDate := currentDate.Format("2006-01-02") + // Force a new hint, to signal updaters that this is new data. + newUUID := uuid.New() + hint := driver.Fingerprint(newUUID.String()) + zlog.Info(ctx).Str("hint", string(hint)).Msg("starting fetch") + out, err := os.CreateTemp("", "enricher.epss.gz") + if err != nil { + return nil, hint, err + } + var success bool + defer func() { + if !success { + if err := out.Close(); err != nil { + zlog.Warn(ctx).Err(err).Msg("unable to close spool") + } + } + }() + if e.feedPath == "" || !strings.HasSuffix(e.feedPath, ".gz") { + filePath := fmt.Sprintf("epss_scores-%s.csv.gz", formattedDate) + e.feedPath = path.Join(DefaultRootUrl, filePath) + } + // Make an HTTP GET request to download the .gz file + resp, err := http.Get(e.feedPath) + if err != nil { + return nil, "", fmt.Errorf("failed to fetch file from %s: %w", e.feedPath, err) + } + defer resp.Body.Close() + + // Check for a successful response + if resp.StatusCode != http.StatusOK { + return nil, "", fmt.Errorf("failed to fetch file: received status %d", resp.StatusCode) + } + + _, err = io.Copy(out, resp.Body) + if err != nil { + out.Close() + return nil, "", fmt.Errorf("failed to write to temporary file: %w", err) + } + + // Reset file pointer to the beginning + _, err = out.Seek(0, io.SeekStart) + if err != nil { + out.Close() + return nil, "", fmt.Errorf("failed to reset file pointer: %w", err) + } + success = true + return out, hint, nil +} + +func (e Enricher) ParseEnrichment(ctx context.Context, closer io.ReadCloser) ([]driver.EnrichmentRecord, error) { + //TODO implement me + panic("implement me") +} + +func (e Enricher) Name() string { + //TODO implement me + panic("implement me") +} + +func (e Enricher) Enrich(ctx context.Context, getter driver.EnrichmentGetter, report *claircore.VulnerabilityReport) (string, []json.RawMessage, error) { + //TODO implement me + panic("implement me") +}