mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 15:16:33 -08:00
* feat(k8s): Add report flag for summary * chore: add headings to the severity columns * chore: make the default output of k8s summary table Signed-off-by: Owen Rumney <owen.rumney@aquasec.com>
276 lines
7.9 KiB
Go
276 lines
7.9 KiB
Go
package artifact
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/cheggaaa/pb/v3"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/xerrors"
|
|
"gopkg.in/yaml.v2"
|
|
|
|
"github.com/aquasecurity/fanal/analyzer"
|
|
"github.com/aquasecurity/fanal/cache"
|
|
"github.com/aquasecurity/trivy-db/pkg/db"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/log"
|
|
pkgReport "github.com/aquasecurity/trivy/pkg/report"
|
|
k8sReport "github.com/aquasecurity/trivy/pkg/report/k8s"
|
|
"github.com/aquasecurity/trivy/pkg/types"
|
|
|
|
"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
|
|
"github.com/aquasecurity/trivy-kubernetes/pkg/k8s"
|
|
"github.com/aquasecurity/trivy-kubernetes/pkg/trivyk8s"
|
|
)
|
|
|
|
// K8sRun runs scan on kubernetes cluster
|
|
func K8sRun(ctx *cli.Context) error {
|
|
opt, err := initOption(ctx)
|
|
if err != nil {
|
|
return xerrors.Errorf("option error: %w", err)
|
|
}
|
|
|
|
if err = log.InitLogger(opt.Debug, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
cacheClient, err := initCache(opt)
|
|
if err != nil {
|
|
if errors.Is(err, errSkipScan) {
|
|
return nil
|
|
}
|
|
return xerrors.Errorf("cache error: %w", err)
|
|
}
|
|
defer cacheClient.Close()
|
|
|
|
// Disable DB update when using client/server
|
|
if opt.RemoteAddr == "" {
|
|
if err = initDB(opt); err != nil {
|
|
if errors.Is(err, errSkipScan) {
|
|
return nil
|
|
}
|
|
return xerrors.Errorf("DB error: %w", err)
|
|
}
|
|
defer db.Close()
|
|
}
|
|
|
|
cluster, err := k8s.GetCluster()
|
|
if err != nil {
|
|
return xerrors.Errorf("get k8s cluster: %w", err)
|
|
}
|
|
|
|
trivyk8s := trivyk8s.New(cluster).Namespace(opt.KubernetesOption.Namespace)
|
|
|
|
// list all kubernetes scannable artifacts
|
|
k8sArtifacts, err := trivyk8s.ListArtifacts(ctx.Context)
|
|
if err != nil {
|
|
return xerrors.Errorf("get k8s artifacts error: %w", err)
|
|
}
|
|
|
|
report, err := k8sRun(ctx, opt, cacheClient, k8sArtifacts)
|
|
if err != nil {
|
|
return xerrors.Errorf("k8s scan error: %w", err)
|
|
}
|
|
report.ClusterName = cluster.GetCurrentContext()
|
|
|
|
if err = k8sReport.Write(report, pkgReport.Option{
|
|
Format: opt.KubernetesOption.ReportFormat, // for now json is the default
|
|
Output: opt.Output,
|
|
}, opt.Severities); err != nil {
|
|
return xerrors.Errorf("unable to write results: %w", err)
|
|
}
|
|
|
|
exit(opt, report.Failed())
|
|
|
|
return nil
|
|
}
|
|
|
|
func k8sRun(cliContext *cli.Context, opt Option, cacheClient cache.Cache, k8sArtifacts []*artifacts.Artifact) (k8sReport.Report, error) {
|
|
ctx, cancel := context.WithTimeout(cliContext.Context, opt.Timeout)
|
|
defer cancel()
|
|
|
|
// progress bar
|
|
bar := pb.StartNew(len(k8sArtifacts))
|
|
if opt.NoProgress {
|
|
bar.SetWriter(io.Discard)
|
|
}
|
|
defer bar.Finish()
|
|
|
|
// image scanner configurations
|
|
imageScannerConfig, imageScannerOptions, err := initImageScannerConfig(ctx, opt, cacheClient)
|
|
if err != nil {
|
|
return k8sReport.Report{}, xerrors.Errorf("scanner config error: %w", err)
|
|
}
|
|
|
|
// config scanner configurations
|
|
configScannerConfig, configScannerOptions, err := initConfigScannerConfig(ctx, opt, cacheClient)
|
|
if err != nil {
|
|
return k8sReport.Report{}, xerrors.Errorf("scanner config error: %w", err)
|
|
}
|
|
|
|
vulns := make([]k8sReport.Resource, 0)
|
|
misconfigs := make([]k8sReport.Resource, 0)
|
|
|
|
// Loops once over all artifacts, and execute scanners as necessary. Not every artifacts has an image,
|
|
// so image scanner is not always executed.
|
|
for _, artifact := range k8sArtifacts {
|
|
bar.Increment()
|
|
|
|
// scan images if present
|
|
for _, image := range artifact.Images {
|
|
imageReport, err := k8sScan(ctx, image, imageScanner, imageScannerConfig, imageScannerOptions)
|
|
if err != nil {
|
|
// add error to report
|
|
log.Logger.Debugf("failed to scan image %s: %s", image, err)
|
|
vulns = append(vulns, newK8sResource(artifact, imageReport, err))
|
|
continue
|
|
}
|
|
|
|
imageReport, err = filter(ctx, opt, imageReport)
|
|
if err != nil {
|
|
return k8sReport.Report{}, xerrors.Errorf("filter error: %w", err)
|
|
}
|
|
|
|
vulns = append(vulns, newK8sResource(artifact, imageReport, nil))
|
|
}
|
|
|
|
// scan configurations
|
|
configReport, err := k8sScanConfig(ctx, configScannerConfig, configScannerOptions, artifact)
|
|
if err != nil {
|
|
// add error to report
|
|
log.Logger.Debugf("failed to scan config %s/%s: %s", artifact.Kind, artifact.Name, err)
|
|
misconfigs = append(misconfigs, newK8sResource(artifact, configReport, err))
|
|
}
|
|
|
|
configReport, err = filter(ctx, opt, configReport)
|
|
if err != nil {
|
|
return k8sReport.Report{}, xerrors.Errorf("filter error: %w", err)
|
|
}
|
|
|
|
misconfigs = append(misconfigs, newK8sResource(artifact, configReport, nil))
|
|
}
|
|
|
|
return k8sReport.Report{
|
|
SchemaVersion: 0,
|
|
Vulnerabilities: vulns,
|
|
Misconfigurations: misconfigs,
|
|
}, nil
|
|
}
|
|
|
|
func initImageScannerConfig(ctx context.Context, opt Option, cacheClient cache.Cache) (ScannerConfig, types.ScanOptions, error) {
|
|
// Disable the lock file scanning
|
|
opt.DisabledAnalyzers = analyzer.TypeLockfiles
|
|
|
|
return initScannerConfig(ctx, opt, cacheClient)
|
|
}
|
|
|
|
func initConfigScannerConfig(ctx context.Context, opt Option, cacheClient cache.Cache) (ScannerConfig, types.ScanOptions, error) {
|
|
// Disable OS and language analyzers
|
|
opt.DisabledAnalyzers = append(analyzer.TypeOSes, analyzer.TypeLanguages...)
|
|
|
|
// Scan only config files
|
|
opt.VulnType = nil
|
|
opt.SecurityChecks = []string{types.SecurityCheckConfig}
|
|
|
|
// Skip downloading vulnerability DB
|
|
opt.SkipDBUpdate = true
|
|
|
|
return initScannerConfig(ctx, opt, cacheClient)
|
|
}
|
|
|
|
func k8sScanConfig(ctx context.Context, config ScannerConfig, opts types.ScanOptions, a *artifacts.Artifact) (types.Report, error) {
|
|
fileName, err := createTempFile(a)
|
|
if err != nil {
|
|
return types.Report{}, xerrors.Errorf("scan error: %w", err)
|
|
}
|
|
defer removeFile(fileName)
|
|
|
|
report, err := k8sScan(ctx, fileName, filesystemStandaloneScanner, config, opts)
|
|
if err != nil {
|
|
return types.Report{}, xerrors.Errorf("scan error: %w", err)
|
|
}
|
|
|
|
return report, nil
|
|
}
|
|
|
|
func k8sScan(ctx context.Context, target string, initializeScanner InitializeScanner, config ScannerConfig, opts types.ScanOptions) (types.Report, error) {
|
|
config.Target = target
|
|
s, cleanup, err := initializeScanner(ctx, config)
|
|
if err != nil {
|
|
log.Logger.Debugf("unexpected error during scanning %s: %s", config.Target, err)
|
|
return types.Report{}, err
|
|
}
|
|
defer cleanup()
|
|
|
|
report, err := s.ScanArtifact(ctx, opts)
|
|
if err != nil {
|
|
return types.Report{}, xerrors.Errorf("artifact scan failed: %w", err)
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
func createTempFile(artifact *artifacts.Artifact) (string, error) {
|
|
filename := fmt.Sprintf("%s-%s-%s-*.yaml", artifact.Namespace, artifact.Kind, artifact.Name)
|
|
|
|
file, err := os.CreateTemp("", filename)
|
|
if err != nil {
|
|
return "", xerrors.Errorf("creating tmp file error: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := file.Close(); err != nil {
|
|
log.Logger.Errorf("failed to close temp file %s: %s:", file.Name(), err)
|
|
}
|
|
}()
|
|
|
|
// TODO(josedonizetti): marshal and return as byte slice should be on the trivy-kubernetes library?
|
|
data, err := yaml.Marshal(artifact.RawResource)
|
|
if err != nil {
|
|
removeFile(filename)
|
|
return "", xerrors.Errorf("marshaling resource error: %w", err)
|
|
}
|
|
|
|
_, err = file.Write(data)
|
|
if err != nil {
|
|
removeFile(filename)
|
|
return "", xerrors.Errorf("writing tmp file error: %w", err)
|
|
}
|
|
|
|
return file.Name(), nil
|
|
}
|
|
|
|
func newK8sResource(artifact *artifacts.Artifact, report types.Report, err error) k8sReport.Resource {
|
|
results := make([]types.Result, 0, len(report.Results))
|
|
// fix target name
|
|
for _, result := range report.Results {
|
|
// if resource is a kubernetes file fix the target name,
|
|
// to avoid showing the temp file that was removed.
|
|
if result.Type == "kubernetes" {
|
|
result.Target = fmt.Sprintf("%s/%s", artifact.Kind, artifact.Name)
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
|
|
k8sreport := k8sReport.Resource{
|
|
Namespace: artifact.Namespace,
|
|
Kind: artifact.Kind,
|
|
Name: artifact.Name,
|
|
Results: results,
|
|
}
|
|
|
|
// if there was any error during the scan
|
|
if err != nil {
|
|
k8sreport.Error = err.Error()
|
|
}
|
|
|
|
return k8sreport
|
|
}
|
|
|
|
func removeFile(filename string) {
|
|
if err := os.Remove(filename); err != nil {
|
|
log.Logger.Errorf("failed to remove temp file %s: %s:", filename, err)
|
|
}
|
|
}
|