diff --git a/go.mod b/go.mod index c94d2e4163..adf90ab7fa 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/protobuf v1.28.0 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b k8s.io/utils v0.0.0-20211116205334-6203023598ed ) diff --git a/pkg/commands/app.go b/pkg/commands/app.go index e40df8eba7..306977dbcf 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -17,6 +17,7 @@ import ( "github.com/aquasecurity/trivy/pkg/commands/option" "github.com/aquasecurity/trivy/pkg/commands/plugin" "github.com/aquasecurity/trivy/pkg/commands/server" + "github.com/aquasecurity/trivy/pkg/k8s" "github.com/aquasecurity/trivy/pkg/result" "github.com/aquasecurity/trivy/pkg/types" "github.com/aquasecurity/trivy/pkg/utils" @@ -793,7 +794,7 @@ func NewK8sCommand() *cli.Command { Name: "kubernetes", Aliases: []string{"k8s"}, Usage: "scan kubernetes vulnerabilities and misconfigurations", - Action: artifact.K8sRun, + Action: k8s.Run, Flags: []cli.Flag{ &namespaceFlag, &reportFlag, diff --git a/pkg/commands/artifact/k8s.go b/pkg/commands/artifact/k8s.go deleted file mode 100644 index 04a5495de0..0000000000 --- a/pkg/commands/artifact/k8s.go +++ /dev/null @@ -1,220 +0,0 @@ -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/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(cliCtx *cli.Context) error { - opt, err := InitOption(cliCtx) - if err != nil { - return xerrors.Errorf("option error: %w", err) - } - - ctx, cancel := context.WithTimeout(cliCtx.Context, opt.Timeout) - defer cancel() - - defer func() { - if xerrors.Is(err, context.DeadlineExceeded) { - log.Logger.Warn("Increase --timeout value") - } - }() - - runner, err := NewRunner(opt) - if err != nil { - if errors.Is(err, SkipScan) { - return nil - } - return xerrors.Errorf("init error: %w", err) - } - defer runner.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) - if err != nil { - return xerrors.Errorf("get k8s artifacts error: %w", err) - } - - report, err := k8sRun(ctx, runner, opt, 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(ctx context.Context, runner *Runner, opt Option, artifacts []*artifacts.Artifact) (k8sReport.Report, error) { - opt.SecurityChecks = []string{types.SecurityCheckVulnerability, types.SecurityCheckConfig} - - // progress bar - bar := pb.StartNew(len(artifacts)) - if opt.NoProgress { - bar.SetWriter(io.Discard) - } - defer bar.Finish() - - vulns := make([]k8sReport.Resource, 0) - misconfigs := make([]k8sReport.Resource, 0) - - // disable logs before scanning - err := log.InitLogger(opt.Debug, true) - if err != nil { - return k8sReport.Report{}, xerrors.Errorf("logger error: %w", err) - } - - // 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 artifacts { - bar.Increment() - - // scan images if present - for _, image := range artifact.Images { - opt.Target = image - imageReport, err := runner.ScanImage(ctx, opt) - 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 = runner.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 - configFile, err := createTempFile(artifact) - if err != nil { - return k8sReport.Report{}, xerrors.Errorf("scan error: %w", err) - } - - opt.Target = configFile - configReport, err := runner.ScanFilesystem(ctx, opt) - removeFile(configFile) - 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 = runner.Filter(ctx, opt, configReport) - if err != nil { - return k8sReport.Report{}, xerrors.Errorf("filter error: %w", err) - } - - misconfigs = append(misconfigs, newK8sResource(artifact, configReport, nil)) - } - - // enable logs after scanning - err = log.InitLogger(opt.Debug, opt.Quiet) - if err != nil { - return k8sReport.Report{}, xerrors.Errorf("logger error: %w", err) - } - - return k8sReport.Report{ - SchemaVersion: 0, - Vulnerabilities: vulns, - Misconfigurations: misconfigs, - }, 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) - } -} diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index dbee11ac2f..18c1ca99e4 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -344,7 +344,7 @@ func run(ctx context.Context, opt Option, artifactType ArtifactType) (err error) return xerrors.Errorf("report error: %w", err) } - exit(opt, report.Results.Failed()) + Exit(opt, report.Results.Failed()) return nil } @@ -466,7 +466,7 @@ func scan(ctx context.Context, opt Option, initializeScanner InitializeScanner, return report, nil } -func exit(c Option, failedResults bool) { +func Exit(c Option, failedResults bool) { if c.ExitCode != 0 && failedResults { os.Exit(c.ExitCode) } diff --git a/pkg/k8s/io.go b/pkg/k8s/io.go new file mode 100644 index 0000000000..1b66ae3ffa --- /dev/null +++ b/pkg/k8s/io.go @@ -0,0 +1,40 @@ +package k8s + +import ( + "fmt" + "os" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/log" + + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" +) + +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) + } + }() + + if err := yaml.NewEncoder(file).Encode(artifact.RawResource); err != nil { + removeFile(filename) + return "", xerrors.Errorf("marshaling resource error: %w", err) + } + + return file.Name(), nil +} + +func removeFile(filename string) { + if err := os.Remove(filename); err != nil { + log.Logger.Errorf("failed to remove temp file %s: %s:", filename, err) + } +} diff --git a/pkg/report/k8s/json.go b/pkg/k8s/json.go similarity index 100% rename from pkg/report/k8s/json.go rename to pkg/k8s/json.go diff --git a/pkg/report/k8s/k8s.go b/pkg/k8s/report.go similarity index 72% rename from pkg/report/k8s/k8s.go rename to pkg/k8s/report.go index f25e331564..69a7c25818 100644 --- a/pkg/report/k8s/k8s.go +++ b/pkg/k8s/report.go @@ -1,9 +1,13 @@ package k8s import ( + "fmt" + "golang.org/x/xerrors" + ftypes "github.com/aquasecurity/fanal/types" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" "github.com/aquasecurity/trivy/pkg/report" "github.com/aquasecurity/trivy/pkg/types" @@ -85,8 +89,8 @@ type Writer interface { Write(Report) error } -// Write writes the results in the give format -func Write(report Report, option report.Option, severities []dbTypes.Severity) error { +// write writes the results in the give format +func write(report Report, option report.Option, severities []dbTypes.Severity) error { var writer Writer switch option.Format { case "all": @@ -99,3 +103,30 @@ func Write(report Report, option report.Option, severities []dbTypes.Severity) e return writer.Write(report) } + +func createResource(artifact *artifacts.Artifact, report types.Report, err error) 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 == ftypes.Kubernetes { + result.Target = fmt.Sprintf("%s/%s", artifact.Kind, artifact.Name) + } + results = append(results, result) + } + + r := Resource{ + Namespace: artifact.Namespace, + Kind: artifact.Kind, + Name: artifact.Name, + Results: results, + } + + // if there was any error during the scan + if err != nil { + r.Error = err.Error() + } + + return r +} diff --git a/pkg/k8s/run.go b/pkg/k8s/run.go new file mode 100644 index 0000000000..1e55f482e7 --- /dev/null +++ b/pkg/k8s/run.go @@ -0,0 +1,84 @@ +package k8s + +import ( + "context" + "errors" + + "github.com/urfave/cli/v2" + "golang.org/x/xerrors" + + cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/log" + pkgReport "github.com/aquasecurity/trivy/pkg/report" + + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" + "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" + "github.com/aquasecurity/trivy-kubernetes/pkg/trivyk8s" +) + +// Run runs scan on kubernetes cluster +func Run(cliCtx *cli.Context) error { + opt, err := cmd.InitOption(cliCtx) + if err != nil { + return xerrors.Errorf("option error: %w", err) + } + + ctx, cancel := context.WithTimeout(cliCtx.Context, opt.Timeout) + defer cancel() + + defer func() { + if xerrors.Is(err, context.DeadlineExceeded) { + log.Logger.Warn("Increase --timeout value") + } + }() + + runner, err := cmd.NewRunner(opt) + if err != nil { + if errors.Is(err, cmd.SkipScan) { + return nil + } + return xerrors.Errorf("init error: %w", err) + } + defer runner.Close() + + cluster, err := k8s.GetCluster() + if err != nil { + return xerrors.Errorf("get k8s cluster: %w", err) + } + + // get kubernetes scannable artifacts + artifacts, err := getArtifacts(ctx, cluster, opt.KubernetesOption.Namespace) + if err != nil { + return xerrors.Errorf("get k8s artifacts error: %w", err) + } + + s := &scanner{ + cluster: cluster.GetCurrentContext(), + runner: runner, + opt: opt, + } + + return run(ctx, s, opt, artifacts) +} + +func run(ctx context.Context, s *scanner, opt cmd.Option, artifacts []*artifacts.Artifact) error { + report, err := s.run(ctx, artifacts) + if err != nil { + return xerrors.Errorf("k8s scan error: %w", err) + } + + if err = write(report, pkgReport.Option{ + Format: opt.KubernetesOption.ReportFormat, + Output: opt.Output, + }, opt.Severities); err != nil { + return xerrors.Errorf("unable to write results: %w", err) + } + + cmd.Exit(opt, report.Failed()) + + return nil +} + +func getArtifacts(ctx context.Context, cluster k8s.Cluster, namespace string) ([]*artifacts.Artifact, error) { + return trivyk8s.New(cluster).Namespace(namespace).ListArtifacts(ctx) +} diff --git a/pkg/k8s/scanner.go b/pkg/k8s/scanner.go new file mode 100644 index 0000000000..0cec0ed69c --- /dev/null +++ b/pkg/k8s/scanner.go @@ -0,0 +1,127 @@ +package k8s + +import ( + "context" + "io" + + "github.com/cheggaaa/pb/v3" + "golang.org/x/xerrors" + + cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" +) + +type scanner struct { + cluster string + runner *cmd.Runner + opt cmd.Option +} + +func (s *scanner) run(ctx context.Context, artifacts []*artifacts.Artifact) (Report, error) { + // Todo move to run.go + s.opt.SecurityChecks = []string{types.SecurityCheckVulnerability, types.SecurityCheckConfig} + + // progress bar + bar := pb.StartNew(len(artifacts)) + if s.opt.NoProgress { + bar.SetWriter(io.Discard) + } + defer bar.Finish() + + var vulns, misconfigs []Resource + + // disable logs before scanning + err := log.InitLogger(s.opt.Debug, true) + if err != nil { + return Report{}, xerrors.Errorf("logger error: %w", err) + } + + // 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 artifacts { + bar.Increment() + + resources, err := s.scanVulns(ctx, artifact) + if err != nil { + return Report{}, xerrors.Errorf("scanning vulnerabilities error: %w", err) + } + vulns = append(vulns, resources...) + + resource, err := s.scanMisconfigs(ctx, artifact) + if err != nil { + return Report{}, xerrors.Errorf("scanning misconfigurations error: %w", err) + } + misconfigs = append(misconfigs, resource) + } + + // enable logs after scanning + err = log.InitLogger(s.opt.Debug, s.opt.Quiet) + if err != nil { + return Report{}, xerrors.Errorf("logger error: %w", err) + } + + return Report{ + SchemaVersion: 0, + ClusterName: s.cluster, + Vulnerabilities: vulns, + Misconfigurations: misconfigs, + }, nil +} + +func (s *scanner) scanVulns(ctx context.Context, artifact *artifacts.Artifact) ([]Resource, error) { + resources := make([]Resource, 0, len(artifact.Images)) + + for _, image := range artifact.Images { + + s.opt.Target = image + + imageReport, err := s.runner.ScanImage(ctx, s.opt) + + if err != nil { + log.Logger.Debugf("failed to scan image %s: %s", image, err) + resources = append(resources, createResource(artifact, imageReport, err)) + continue + } + + resource, err := s.filter(ctx, imageReport, artifact) + if err != nil { + return nil, xerrors.Errorf("filter error: %w", err) + } + + resources = append(resources, resource) + } + + return resources, nil +} + +func (s *scanner) scanMisconfigs(ctx context.Context, artifact *artifacts.Artifact) (Resource, error) { + configFile, err := createTempFile(artifact) + if err != nil { + return Resource{}, xerrors.Errorf("scan error: %w", err) + } + + s.opt.Target = configFile + + configReport, err := s.runner.ScanFilesystem(ctx, s.opt) + //remove config file after scanning + removeFile(configFile) + if err != nil { + log.Logger.Debugf("failed to scan config %s/%s: %s", artifact.Kind, artifact.Name, err) + return createResource(artifact, configReport, err), err + } + + return s.filter(ctx, configReport, artifact) +} + +func (s *scanner) filter(ctx context.Context, report types.Report, artifact *artifacts.Artifact) (Resource, error) { + report, err := s.runner.Filter(ctx, s.opt, report) + if err != nil { + return Resource{}, xerrors.Errorf("filter error: %w", err) + } + + return createResource(artifact, report, nil), nil + +} diff --git a/pkg/report/k8s/summary.go b/pkg/k8s/summary.go similarity index 100% rename from pkg/report/k8s/summary.go rename to pkg/k8s/summary.go