mirror of https://github.com/artifacthub/hub.git
219 lines
6.4 KiB
Go
219 lines
6.4 KiB
Go
package scanner
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha512"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
|
|
trivyreport "github.com/aquasecurity/trivy/pkg/report"
|
|
"github.com/artifacthub/hub/internal/hub"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
var (
|
|
// ErrImageNotFound indicates that the image provided was not found in the
|
|
// registry.
|
|
ErrImageNotFound = errors.New("image not found")
|
|
|
|
// ErrSchemaV1NotSupported indicates that the image provided is using a v1
|
|
// schema which is not supported.
|
|
ErrSchemaV1NotSupported = errors.New("schema v1 manifest not supported by trivy")
|
|
)
|
|
|
|
// ImageScanner describes the methods an ImageScanner implementation must
|
|
// provide. An image scanner is responsible of scanning a container image for
|
|
// security vulnerabilities.
|
|
type ImageScanner interface {
|
|
// ScanImage scans the provided image for security vulnerabilities,
|
|
// returning a report in json format.
|
|
ScanImage(image string) ([]byte, error)
|
|
}
|
|
|
|
// Scanner is in charge of scanning packages' snapshots for security
|
|
// vulnerabilities. It relies on an image scanner to scan all the containers
|
|
// images listed on the snapshot.
|
|
type Scanner struct {
|
|
is ImageScanner
|
|
ec hub.ErrorsCollector
|
|
}
|
|
|
|
// New creates a new Scanner instance.
|
|
func New(
|
|
ctx context.Context,
|
|
cfg *viper.Viper,
|
|
ec hub.ErrorsCollector,
|
|
opts ...func(s *Scanner),
|
|
) *Scanner {
|
|
if cfg.GetString("scanner.trivyURL") == "" {
|
|
log.Fatal().Msg("trivy url not set")
|
|
}
|
|
s := &Scanner{
|
|
is: &TrivyScanner{
|
|
ctx: ctx,
|
|
cfg: cfg,
|
|
},
|
|
ec: ec,
|
|
}
|
|
for _, o := range opts {
|
|
o(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// WithImageScanner allows providing a specific ImageScanner implementation for
|
|
// a Scanner instance.
|
|
func WithImageScanner(is ImageScanner) func(s *Scanner) {
|
|
return func(s *Scanner) {
|
|
s.is = is
|
|
}
|
|
}
|
|
|
|
// Scan scans the provided package's snapshot for security vulnerabilities
|
|
// returning a report with the results.
|
|
func (s *Scanner) Scan(sn *hub.SnapshotToScan) (*hub.SnapshotSecurityReport, error) {
|
|
s.ec.Init(sn.RepositoryID)
|
|
|
|
report := &hub.SnapshotSecurityReport{
|
|
PackageID: sn.PackageID,
|
|
Version: sn.Version,
|
|
}
|
|
|
|
imagesReports := make(map[string]*trivyreport.Report)
|
|
for _, image := range sn.ContainersImages {
|
|
imageReportJSON, err := s.is.ScanImage(image.Image)
|
|
if err != nil {
|
|
err := fmt.Errorf("error scanning image %s: %w (package %s:%s)", image.Image, err, sn.PackageName, sn.Version)
|
|
s.ec.Append(sn.RepositoryID, err.Error())
|
|
return report, err
|
|
}
|
|
var imageReport *trivyreport.Report
|
|
if err := json.Unmarshal(imageReportJSON, &imageReport); err != nil {
|
|
return report, fmt.Errorf("error unmarshalling image %s report: %w", image.Image, err)
|
|
}
|
|
if imageReport != nil && len(imageReport.Results) > 0 {
|
|
imagesReports[image.Image] = imageReport
|
|
}
|
|
}
|
|
if len(imagesReports) > 0 {
|
|
report.ImagesReports = imagesReports
|
|
report.Summary = generateSummary(imagesReports)
|
|
report.AlertDigest = generateAlertDigest(imagesReports)
|
|
}
|
|
|
|
return report, nil
|
|
}
|
|
|
|
// generateSummary generates a summary of the security report from the images
|
|
// reports.
|
|
func generateSummary(imagesReports map[string]*trivyreport.Report) *hub.SecurityReportSummary {
|
|
summary := &hub.SecurityReportSummary{}
|
|
for _, imageReport := range imagesReports {
|
|
for _, result := range imageReport.Results {
|
|
for _, vulnerability := range result.Vulnerabilities {
|
|
switch vulnerability.Severity {
|
|
case "CRITICAL":
|
|
summary.Critical++
|
|
case "HIGH":
|
|
summary.High++
|
|
case "MEDIUM":
|
|
summary.Medium++
|
|
case "LOW":
|
|
summary.Low++
|
|
case "UNKNOWN":
|
|
summary.Unknown++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return summary
|
|
}
|
|
|
|
// generateAlertDigest generates an alert digest of the security report from
|
|
// the images reports. At the moment the digest is based on the vulnerabilities
|
|
// with a severity of high or critical.
|
|
func generateAlertDigest(imagesReports map[string]*trivyreport.Report) string {
|
|
var vs []string
|
|
for _, imageReport := range imagesReports {
|
|
for _, result := range imageReport.Results {
|
|
for _, v := range result.Vulnerabilities {
|
|
if v.Severity == "HIGH" || v.Severity == "CRITICAL" {
|
|
vs = append(vs, fmt.Sprintf("[%s:%s]", v.Severity, v.VulnerabilityID))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var digest string
|
|
if len(vs) > 0 {
|
|
sort.Strings(vs)
|
|
digest = fmt.Sprintf("%x", sha512.Sum512([]byte(strings.Join(vs, ""))))
|
|
}
|
|
return digest
|
|
}
|
|
|
|
// TrivyScanner is an ImageScanner implementation that uses Trivy to scan
|
|
// containers images for security vulnerabilities.
|
|
type TrivyScanner struct {
|
|
ctx context.Context
|
|
cfg *viper.Viper
|
|
}
|
|
|
|
// ScanImage implements the ImageScanner interface.
|
|
func (s *TrivyScanner) ScanImage(image string) ([]byte, error) {
|
|
// Setup trivy command
|
|
trivyURL := s.cfg.GetString("scanner.trivyURL")
|
|
cmd := exec.CommandContext(s.ctx, "trivy", "--quiet", "client", "--remote", trivyURL, "-f", "json", image) // #nosec
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Env = []string{
|
|
"PATH=" + os.Getenv("PATH"),
|
|
"USER=" + os.Getenv("USER"),
|
|
"HOME=" + os.Getenv("HOME"),
|
|
"TRIVY_CACHE_DIR=" + os.Getenv("TRIVY_CACHE_DIR"),
|
|
"TRIVY_NEW_JSON_SCHEMA=true", // Not needed in Trivy >= 0.20.0
|
|
}
|
|
|
|
// If the registry is the Docker Hub, include credentials to avoid rate
|
|
// limiting issues. Empty registry names will also match this check as the
|
|
// registry name will be set to index.docker.io when parsing the reference.
|
|
ref, err := name.ParseReference(image)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing image %s ref: %w", image, err)
|
|
}
|
|
if strings.HasSuffix(ref.Context().Registry.Name(), "docker.io") {
|
|
cmd.Env = append(cmd.Env,
|
|
"TRIVY_USERNAME="+s.cfg.GetString("creds.dockerUsername"),
|
|
"TRIVY_PASSWORD="+s.cfg.GetString("creds.dockerPassword"),
|
|
)
|
|
}
|
|
|
|
// Run trivy command
|
|
if err := cmd.Run(); err != nil {
|
|
if strings.Contains(stderr.String(), "MANIFEST_UNKNOWN") {
|
|
return nil, ErrImageNotFound
|
|
}
|
|
if strings.Contains(stderr.String(), "UNAUTHORIZED") {
|
|
return nil, ErrImageNotFound
|
|
}
|
|
if strings.Contains(stderr.String(), `unsupported MediaType: "application/vnd.docker.distribution.manifest.v1+prettyjws"`) {
|
|
return nil, ErrSchemaV1NotSupported
|
|
}
|
|
trivyError := stderr.String()
|
|
parts := strings.Split(stderr.String(), "podman/podman.sock: no such file or directory")
|
|
if len(parts) > 1 {
|
|
trivyError = strings.TrimSpace(parts[1])
|
|
}
|
|
return nil, fmt.Errorf("error running trivy on image %s: %s", image, trivyError)
|
|
}
|
|
return stdout.Bytes(), nil
|
|
}
|