mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat: add k8s components (#2589)
Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
@@ -231,3 +231,49 @@ $ trivy k8s --format json -o results.json cluster
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
## Infra checks
|
||||
|
||||
Trivy by default scans kubernetes infra components (apiserver, controller-manager, scheduler and etcd)
|
||||
if they exist under the `kube-system` namespace. For example, if you run a full cluster scan, or scan all
|
||||
components under `kube-system` with commands:
|
||||
|
||||
```
|
||||
$ trivy k8s cluster --report summary # full cluster scan
|
||||
$ trivy k8s all -n kube-system --report summary # scan all componetns under kube-system
|
||||
```
|
||||
|
||||
A table will be printed about misconfigurations found on kubernetes core components:
|
||||
|
||||
```
|
||||
Summary Report for minikube
|
||||
┌─────────────┬──────────────────────────────────────┬─────────────────────────────┐
|
||||
│ Namespace │ Resource │ Kubernetes Infra Assessment │
|
||||
│ │ ├────┬────┬────┬─────┬────────┤
|
||||
│ │ │ C │ H │ M │ L │ U │
|
||||
├─────────────┼──────────────────────────────────────┼────┼────┼────┼─────┼────────┤
|
||||
│ kube-system │ Pod/kube-apiserver-minikube │ │ │ 1 │ 10 │ │
|
||||
│ kube-system │ Pod/kube-controller-manager-minikube │ │ │ │ 3 │ │
|
||||
│ kube-system │ Pod/kube-scheduler-minikube │ │ │ │ 1 │ │
|
||||
└─────────────┴──────────────────────────────────────┴────┴────┴────┴─────┴────────┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN
|
||||
```
|
||||
|
||||
The infra checks are based on CIS Benchmarks recommendations for kubernetes.
|
||||
|
||||
|
||||
If you want filter only for the infra checks, you can use the flag `--components` along with the `--security-checks=config`
|
||||
|
||||
```
|
||||
$ trivy k8s cluster --report summary --components=infra --security-checks=config # scan only infra
|
||||
```
|
||||
|
||||
Or, to filter for all other checks besides the infra checks, you can:
|
||||
|
||||
```
|
||||
$ trivy k8s cluster --report summary --components=workload --security-checks=config # scan all components besides infra
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -741,7 +741,8 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
|
||||
"%s,%s,%s,%s",
|
||||
types.SecurityCheckVulnerability,
|
||||
types.SecurityCheckConfig,
|
||||
types.SecurityCheckSecret, types.SecurityCheckRbac,
|
||||
types.SecurityCheckSecret,
|
||||
types.SecurityCheckRbac,
|
||||
)
|
||||
scanFlags.SecurityChecks = &securityChecks
|
||||
|
||||
@@ -760,7 +761,7 @@ func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "kubernetes [flags] { cluster | all | specific resources like kubectl. eg: pods, pod/NAME }",
|
||||
Aliases: []string{"k8s"},
|
||||
Short: "scan kubernetes cluster",
|
||||
Short: "[EXPERIMENTAL] Scan kubernetes cluster",
|
||||
Example: ` # cluster scanning
|
||||
$ trivy k8s --report summary cluster
|
||||
|
||||
|
||||
@@ -20,18 +20,26 @@ var (
|
||||
Value: "",
|
||||
Usage: "specify the kubeconfig file path to use",
|
||||
}
|
||||
ComponentsFlag = Flag{
|
||||
Name: "components",
|
||||
ConfigName: "kubernetes.components",
|
||||
Value: []string{"workload", "infra"},
|
||||
Usage: "specify which components to scan",
|
||||
}
|
||||
)
|
||||
|
||||
type K8sFlagGroup struct {
|
||||
ClusterContext *Flag
|
||||
Namespace *Flag
|
||||
KubeConfig *Flag
|
||||
Components *Flag
|
||||
}
|
||||
|
||||
type K8sOptions struct {
|
||||
ClusterContext string
|
||||
Namespace string
|
||||
KubeConfig string
|
||||
Components []string
|
||||
}
|
||||
|
||||
func NewK8sFlagGroup() *K8sFlagGroup {
|
||||
@@ -39,6 +47,7 @@ func NewK8sFlagGroup() *K8sFlagGroup {
|
||||
ClusterContext: &ClusterContextFlag,
|
||||
Namespace: &K8sNamespaceFlag,
|
||||
KubeConfig: &KubeConfigFlag,
|
||||
Components: &ComponentsFlag,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +56,7 @@ func (f *K8sFlagGroup) Name() string {
|
||||
}
|
||||
|
||||
func (f *K8sFlagGroup) Flags() []*Flag {
|
||||
return []*Flag{f.ClusterContext, f.Namespace, f.KubeConfig}
|
||||
return []*Flag{f.ClusterContext, f.Namespace, f.KubeConfig, f.Components}
|
||||
}
|
||||
|
||||
func (f *K8sFlagGroup) ToOptions() K8sOptions {
|
||||
@@ -55,5 +64,6 @@ func (f *K8sFlagGroup) ToOptions() K8sOptions {
|
||||
ClusterContext: getString(f.ClusterContext),
|
||||
Namespace: getString(f.Namespace),
|
||||
KubeConfig: getString(f.KubeConfig),
|
||||
Components: getStringSlice(f.Components),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@ func clusterRun(ctx context.Context, opts flag.Options, cluster k8s.Cluster) err
|
||||
return xerrors.Errorf("get k8s artifacts error: %w", err)
|
||||
}
|
||||
|
||||
return run(ctx, opts, cluster.GetCurrentContext(), artifacts, true)
|
||||
runner := newRunner(opts, cluster.GetCurrentContext())
|
||||
return runner.run(ctx, artifacts)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ func namespaceRun(ctx context.Context, opts flag.Options, cluster k8s.Cluster) e
|
||||
return xerrors.Errorf("get k8s artifacts error: %w", err)
|
||||
}
|
||||
|
||||
return run(ctx, opts, cluster.GetCurrentContext(), artifacts, true)
|
||||
runner := newRunner(opts, cluster.GetCurrentContext())
|
||||
return runner.run(ctx, artifacts)
|
||||
}
|
||||
|
||||
func getNamespace(opts flag.Options, currentNamespace string) string {
|
||||
|
||||
@@ -22,6 +22,7 @@ func resourceRun(ctx context.Context, args []string, opts flag.Options, cluster
|
||||
}
|
||||
|
||||
trivyk8s := trivyk8s.New(cluster, log.Logger).Namespace(getNamespace(opts, cluster.GetCurrentNamespace()))
|
||||
runner := newRunner(opts, cluster.GetCurrentContext())
|
||||
|
||||
if len(name) == 0 { // pods or configmaps etc
|
||||
if err = validateReportArguments(opts); err != nil {
|
||||
@@ -33,7 +34,7 @@ func resourceRun(ctx context.Context, args []string, opts flag.Options, cluster
|
||||
return err
|
||||
}
|
||||
|
||||
return run(ctx, opts, cluster.GetCurrentContext(), targets, false)
|
||||
return runner.run(ctx, targets)
|
||||
}
|
||||
|
||||
// pod/NAME or pod NAME etc
|
||||
@@ -42,7 +43,7 @@ func resourceRun(ctx context.Context, args []string, opts flag.Options, cluster
|
||||
return err
|
||||
}
|
||||
|
||||
return run(ctx, opts, cluster.GetCurrentContext(), []*artifacts.Artifact{artifact}, false)
|
||||
return runner.run(ctx, []*artifacts.Artifact{artifact})
|
||||
}
|
||||
|
||||
func extractKindAndName(args []string) (string, string, error) {
|
||||
|
||||
@@ -44,8 +44,17 @@ func Run(ctx context.Context, args []string, opts flag.Options) error {
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, opts flag.Options, cluster string, artifacts []*artifacts.Artifact, showEmpty bool) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, opts.Timeout)
|
||||
type runner struct {
|
||||
flagOpts flag.Options
|
||||
cluster string
|
||||
}
|
||||
|
||||
func newRunner(flagOpts flag.Options, cluster string) *runner {
|
||||
return &runner{flagOpts, cluster}
|
||||
}
|
||||
|
||||
func (r *runner) run(ctx context.Context, artifacts []*artifacts.Artifact) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, r.flagOpts.Timeout)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
@@ -55,7 +64,7 @@ func run(ctx context.Context, opts flag.Options, cluster string, artifacts []*ar
|
||||
}
|
||||
}()
|
||||
|
||||
runner, err := cmd.NewRunner(ctx, opts)
|
||||
runner, err := cmd.NewRunner(ctx, r.flagOpts)
|
||||
if err != nil {
|
||||
if errors.Is(err, cmd.SkipScan) {
|
||||
return nil
|
||||
@@ -68,23 +77,25 @@ func run(ctx context.Context, opts flag.Options, cluster string, artifacts []*ar
|
||||
}
|
||||
}()
|
||||
|
||||
s := scanner.NewScanner(cluster, runner, opts)
|
||||
s := scanner.NewScanner(r.cluster, runner, r.flagOpts)
|
||||
|
||||
r, err := s.Scan(ctx, artifacts)
|
||||
rpt, err := s.Scan(ctx, artifacts)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("k8s scan error: %w", err)
|
||||
}
|
||||
|
||||
if err := report.Write(r, report.Option{
|
||||
Format: opts.Format,
|
||||
Report: opts.ReportFormat,
|
||||
Output: opts.Output,
|
||||
Severities: opts.Severities,
|
||||
}, opts.ScanOptions.SecurityChecks, showEmpty); err != nil {
|
||||
if err := report.Write(rpt, report.Option{
|
||||
Format: r.flagOpts.Format,
|
||||
Report: r.flagOpts.ReportFormat,
|
||||
Output: r.flagOpts.Output,
|
||||
Severities: r.flagOpts.Severities,
|
||||
Components: r.flagOpts.Components,
|
||||
SecurityChecks: r.flagOpts.ScanOptions.SecurityChecks,
|
||||
}); err != nil {
|
||||
return xerrors.Errorf("unable to write results: %w", err)
|
||||
}
|
||||
|
||||
cmd.Exit(opts, r.Failed())
|
||||
cmd.Exit(r.flagOpts, rpt.Failed())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,16 +5,15 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/report/table"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
@@ -24,14 +23,19 @@ const (
|
||||
|
||||
tableFormat = "table"
|
||||
jsonFormat = "json"
|
||||
|
||||
workloadComponent = "workload"
|
||||
infraComponent = "infra"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Format string
|
||||
Report string
|
||||
Output io.Writer
|
||||
Severities []dbTypes.Severity
|
||||
ColumnHeading []string
|
||||
Format string
|
||||
Report string
|
||||
Output io.Writer
|
||||
Severities []dbTypes.Severity
|
||||
ColumnHeading []string
|
||||
SecurityChecks []string
|
||||
Components []string
|
||||
}
|
||||
|
||||
// Report represents a kubernetes scan report
|
||||
@@ -40,6 +44,7 @@ type Report struct {
|
||||
ClusterName string
|
||||
Vulnerabilities []Resource `json:",omitempty"`
|
||||
Misconfigurations []Resource `json:",omitempty"`
|
||||
name string
|
||||
}
|
||||
|
||||
// ConsolidatedReport represents a kubernetes scan report with consolidated findings
|
||||
@@ -69,14 +74,14 @@ func (r Resource) fullname() string {
|
||||
|
||||
// Failed returns whether the k8s report includes any vulnerabilities or misconfigurations
|
||||
func (r Report) Failed() bool {
|
||||
for _, r := range r.Vulnerabilities {
|
||||
if r.Results.Failed() {
|
||||
for _, v := range r.Vulnerabilities {
|
||||
if v.Results.Failed() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range r.Misconfigurations {
|
||||
if r.Results.Failed() {
|
||||
for _, m := range r.Misconfigurations {
|
||||
if m.Results.Failed() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -84,10 +89,6 @@ func (r Report) Failed() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r Report) empty() bool {
|
||||
return len(r.Misconfigurations) == 0 && len(r.Vulnerabilities) == 0
|
||||
}
|
||||
|
||||
func (r Report) consolidate() ConsolidatedReport {
|
||||
consolidated := ConsolidatedReport{
|
||||
SchemaVersion: r.SchemaVersion,
|
||||
@@ -103,13 +104,13 @@ func (r Report) consolidate() ConsolidatedReport {
|
||||
for _, v := range r.Vulnerabilities {
|
||||
key := v.fullname()
|
||||
|
||||
if r, ok := index[key]; ok {
|
||||
if res, ok := index[key]; ok {
|
||||
index[key] = Resource{
|
||||
Namespace: r.Namespace,
|
||||
Kind: r.Kind,
|
||||
Name: r.Name,
|
||||
Results: append(r.Results, v.Results...),
|
||||
Error: r.Error,
|
||||
Namespace: res.Namespace,
|
||||
Kind: res.Kind,
|
||||
Name: res.Name,
|
||||
Results: append(res.Results, v.Results...),
|
||||
Error: res.Error,
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -129,7 +130,7 @@ type Writer interface {
|
||||
}
|
||||
|
||||
// Write writes the results in the give format
|
||||
func Write(report Report, option Option, securityChecks []string, showEmpty bool) error {
|
||||
func Write(report Report, option Option) error {
|
||||
report.printErrors()
|
||||
|
||||
switch option.Format {
|
||||
@@ -137,29 +138,24 @@ func Write(report Report, option Option, securityChecks []string, showEmpty bool
|
||||
jwriter := JSONWriter{Output: option.Output, Report: option.Report}
|
||||
return jwriter.Write(report)
|
||||
case tableFormat:
|
||||
workloadReport, rbacReport := separateMisConfigRoleAssessment(report, securityChecks)
|
||||
separatedReports := separateMisconfigReports(report, option.SecurityChecks, option.Components)
|
||||
|
||||
if !workloadReport.empty() || showEmpty {
|
||||
WorkloadWriter := &TableWriter{
|
||||
Output: option.Output,
|
||||
Report: option.Report,
|
||||
Severities: option.Severities,
|
||||
ColumnHeading: ColumnHeading(securityChecks, WorkloadColumns()),
|
||||
}
|
||||
err := WorkloadWriter.Write(workloadReport)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if option.Report == summaryReport {
|
||||
target := fmt.Sprintf("Summary Report for %s", report.ClusterName)
|
||||
table.RenderTarget(option.Output, target, table.IsOutputToTerminal(option.Output))
|
||||
}
|
||||
|
||||
if !rbacReport.empty() || showEmpty {
|
||||
rbacWriter := &TableWriter{
|
||||
for _, r := range separatedReports {
|
||||
writer := &TableWriter{
|
||||
Output: option.Output,
|
||||
Report: option.Report,
|
||||
Severities: option.Severities,
|
||||
ColumnHeading: ColumnHeading(securityChecks, RoleColumns()),
|
||||
ColumnHeading: ColumnHeading(option.SecurityChecks, option.Components, r.columns),
|
||||
}
|
||||
|
||||
if err := writer.Write(r.report); err != nil {
|
||||
return err
|
||||
}
|
||||
return rbacWriter.Write(rbacReport)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -168,34 +164,99 @@ func Write(report Report, option Option, securityChecks []string, showEmpty bool
|
||||
}
|
||||
}
|
||||
|
||||
func separateMisConfigRoleAssessment(k8sReport Report, securityChecks []string) (Report, Report) {
|
||||
type reports struct {
|
||||
report Report
|
||||
columns []string
|
||||
}
|
||||
|
||||
// separateMisconfigReports returns 3 reports based on securityChecks and components flags,
|
||||
// - misconfiguration report
|
||||
// - rbac report
|
||||
// - infra checks report
|
||||
func separateMisconfigReports(k8sReport Report, securityChecks, components []string) []reports {
|
||||
|
||||
workloadMisconfig := make([]Resource, 0)
|
||||
infraMisconfig := make([]Resource, 0)
|
||||
rbacAssessment := make([]Resource, 0)
|
||||
|
||||
for _, misConfig := range k8sReport.Misconfigurations {
|
||||
if slices.Contains(securityChecks, types.SecurityCheckRbac) && rbacResource(misConfig) {
|
||||
switch {
|
||||
case slices.Contains(securityChecks, types.SecurityCheckRbac) && rbacResource(misConfig):
|
||||
rbacAssessment = append(rbacAssessment, misConfig)
|
||||
} else {
|
||||
if slices.Contains(securityChecks, types.SecurityCheckConfig) && !rbacResource(misConfig) {
|
||||
case infraResource(misConfig):
|
||||
workload, infra := splitInfraAndWorkloadResources(misConfig)
|
||||
|
||||
if slices.Contains(components, infraComponent) {
|
||||
infraMisconfig = append(infraMisconfig, infra)
|
||||
}
|
||||
|
||||
if slices.Contains(components, workloadComponent) {
|
||||
workloadMisconfig = append(workloadMisconfig, workload)
|
||||
}
|
||||
|
||||
case slices.Contains(securityChecks, types.SecurityCheckConfig) && !rbacResource(misConfig):
|
||||
if slices.Contains(components, workloadComponent) {
|
||||
workloadMisconfig = append(workloadMisconfig, misConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Report{
|
||||
|
||||
r := make([]reports, 0)
|
||||
|
||||
if shouldAddWorkloadReport(securityChecks) {
|
||||
workloadReport := Report{
|
||||
SchemaVersion: 0,
|
||||
ClusterName: k8sReport.ClusterName,
|
||||
Vulnerabilities: k8sReport.Vulnerabilities,
|
||||
Misconfigurations: workloadMisconfig,
|
||||
}, Report{
|
||||
SchemaVersion: 0,
|
||||
ClusterName: k8sReport.ClusterName,
|
||||
Misconfigurations: rbacAssessment,
|
||||
Vulnerabilities: k8sReport.Vulnerabilities,
|
||||
name: "Workload Assessment",
|
||||
}
|
||||
|
||||
if (slices.Contains(components, workloadComponent) &&
|
||||
len(workloadMisconfig) > 0) ||
|
||||
len(k8sReport.Vulnerabilities) > 0 {
|
||||
r = append(r, reports{report: workloadReport, columns: WorkloadColumns()})
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(securityChecks, types.SecurityCheckRbac) && len(rbacAssessment) > 0 {
|
||||
r = append(r, reports{
|
||||
report: Report{
|
||||
SchemaVersion: 0,
|
||||
ClusterName: k8sReport.ClusterName,
|
||||
Misconfigurations: rbacAssessment,
|
||||
name: "RBAC Assessment",
|
||||
},
|
||||
columns: RoleColumns(),
|
||||
})
|
||||
}
|
||||
|
||||
if slices.Contains(securityChecks, types.SecurityCheckConfig) &&
|
||||
slices.Contains(components, infraComponent) &&
|
||||
len(infraMisconfig) > 0 {
|
||||
|
||||
r = append(r, reports{
|
||||
report: Report{
|
||||
SchemaVersion: 0,
|
||||
ClusterName: k8sReport.ClusterName,
|
||||
Misconfigurations: infraMisconfig,
|
||||
name: "Infra Assessment",
|
||||
},
|
||||
columns: InfraColumns(),
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func rbacResource(misConfig Resource) bool {
|
||||
return misConfig.Kind == "Role" || misConfig.Kind == "RoleBinding" || misConfig.Kind == "ClusterRole" || misConfig.Kind == "ClusterRoleBinding"
|
||||
}
|
||||
|
||||
func infraResource(misConfig Resource) bool {
|
||||
return misConfig.Kind == "Pod" && misConfig.Namespace == "kube-system"
|
||||
}
|
||||
|
||||
func CreateResource(artifact *artifacts.Artifact, report types.Report, err error) Resource {
|
||||
results := make([]types.Result, 0, len(report.Results))
|
||||
// fix target name
|
||||
@@ -237,3 +298,67 @@ func (r Report) printErrors() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func splitInfraAndWorkloadResources(misconfig Resource) (Resource, Resource) {
|
||||
workload := copyResource(misconfig)
|
||||
infra := copyResource(misconfig)
|
||||
|
||||
workloadResults := make(types.Results, 0)
|
||||
infraResults := make(types.Results, 0)
|
||||
|
||||
for _, result := range misconfig.Results {
|
||||
workloadMisconfigs := make([]types.DetectedMisconfiguration, 0)
|
||||
infraMisconfigs := make([]types.DetectedMisconfiguration, 0)
|
||||
|
||||
for _, m := range result.Misconfigurations {
|
||||
if strings.HasPrefix(m.ID, "KCV") {
|
||||
infraMisconfigs = append(infraMisconfigs, m)
|
||||
continue
|
||||
}
|
||||
|
||||
workloadMisconfigs = append(workloadMisconfigs, m)
|
||||
}
|
||||
|
||||
if len(workloadMisconfigs) > 0 {
|
||||
workloadResults = append(workloadResults, copyResult(result, workloadMisconfigs))
|
||||
}
|
||||
|
||||
if len(infraMisconfigs) > 0 {
|
||||
infraResults = append(infraResults, copyResult(result, infraMisconfigs))
|
||||
}
|
||||
}
|
||||
|
||||
workload.Results = workloadResults
|
||||
workload.Report.Results = workloadResults
|
||||
|
||||
infra.Results = infraResults
|
||||
infra.Report.Results = infraResults
|
||||
|
||||
return workload, infra
|
||||
}
|
||||
|
||||
func copyResource(r Resource) Resource {
|
||||
return Resource{
|
||||
Namespace: r.Namespace,
|
||||
Kind: r.Kind,
|
||||
Name: r.Name,
|
||||
Error: r.Error,
|
||||
Report: r.Report,
|
||||
}
|
||||
}
|
||||
|
||||
func copyResult(r types.Result, misconfigs []types.DetectedMisconfiguration) types.Result {
|
||||
return types.Result{
|
||||
Target: r.Target,
|
||||
Class: r.Class,
|
||||
Type: r.Type,
|
||||
MisconfSummary: r.MisconfSummary,
|
||||
Misconfigurations: misconfigs,
|
||||
}
|
||||
}
|
||||
|
||||
func shouldAddWorkloadReport(securityChecks []string) bool {
|
||||
return slices.Contains(securityChecks, types.SecurityCheckConfig) ||
|
||||
slices.Contains(securityChecks, types.SecurityCheckVulnerability) ||
|
||||
slices.Contains(securityChecks, types.SecurityCheckSecret)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
@@ -15,7 +19,15 @@ var (
|
||||
Kind: "Deploy",
|
||||
Name: "orion",
|
||||
Results: types.Results{
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "ID100", Status: types.StatusFailure}}},
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{
|
||||
{ID: "ID100", Status: types.StatusFailure, Severity: "LOW"},
|
||||
{ID: "ID101", Status: types.StatusFailure, Severity: "MEDIUM"},
|
||||
{ID: "ID102", Status: types.StatusFailure, Severity: "HIGH"},
|
||||
{ID: "ID103", Status: types.StatusFailure, Severity: "CRITICAL"},
|
||||
{ID: "ID104", Status: types.StatusFailure, Severity: "UNKNOWN"},
|
||||
{ID: "ID105", Status: types.StatusFailure, Severity: "LOW"},
|
||||
{ID: "ID106", Status: types.StatusFailure, Severity: "HIGH"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,7 +36,15 @@ var (
|
||||
Kind: "Deploy",
|
||||
Name: "orion",
|
||||
Results: types.Results{
|
||||
{Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-2020-8888"}}},
|
||||
{Vulnerabilities: []types.DetectedVulnerability{
|
||||
{VulnerabilityID: "CVE-2022-1111", Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}},
|
||||
{VulnerabilityID: "CVE-2022-2222", Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}},
|
||||
{VulnerabilityID: "CVE-2022-3333", Vulnerability: dbTypes.Vulnerability{Severity: "HIGH"}},
|
||||
{VulnerabilityID: "CVE-2022-4444", Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}},
|
||||
{VulnerabilityID: "CVE-2022-5555", Vulnerability: dbTypes.Vulnerability{Severity: "UNKNOWN"}},
|
||||
{VulnerabilityID: "CVE-2022-6666", Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}},
|
||||
{VulnerabilityID: "CVE-2022-7777", Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -33,8 +53,24 @@ var (
|
||||
Kind: "Deploy",
|
||||
Name: "orion",
|
||||
Results: types.Results{
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "ID100", Status: types.StatusFailure}}},
|
||||
{Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-2020-8888"}}},
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{
|
||||
{ID: "ID100", Status: types.StatusFailure, Severity: "LOW"},
|
||||
{ID: "ID101", Status: types.StatusFailure, Severity: "MEDIUM"},
|
||||
{ID: "ID102", Status: types.StatusFailure, Severity: "HIGH"},
|
||||
{ID: "ID103", Status: types.StatusFailure, Severity: "CRITICAL"},
|
||||
{ID: "ID104", Status: types.StatusFailure, Severity: "UNKNOWN"},
|
||||
{ID: "ID105", Status: types.StatusFailure, Severity: "LOW"},
|
||||
{ID: "ID106", Status: types.StatusFailure, Severity: "HIGH"},
|
||||
}},
|
||||
{Vulnerabilities: []types.DetectedVulnerability{
|
||||
{VulnerabilityID: "CVE-2022-1111", Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}},
|
||||
{VulnerabilityID: "CVE-2022-2222", Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}},
|
||||
{VulnerabilityID: "CVE-2022-3333", Vulnerability: dbTypes.Vulnerability{Severity: "HIGH"}},
|
||||
{VulnerabilityID: "CVE-2022-4444", Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}},
|
||||
{VulnerabilityID: "CVE-2022-5555", Vulnerability: dbTypes.Vulnerability{Severity: "UNKNOWN"}},
|
||||
{VulnerabilityID: "CVE-2022-6666", Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}},
|
||||
{VulnerabilityID: "CVE-2022-7777", Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,6 +91,45 @@ var (
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "ID100"}}},
|
||||
},
|
||||
}
|
||||
|
||||
roleWithMisconfig = Resource{
|
||||
Namespace: "default",
|
||||
Kind: "Role",
|
||||
Name: "system::leader-locking-kube-controller-manager",
|
||||
Results: types.Results{
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{
|
||||
{ID: "ID100", Status: types.StatusFailure, Severity: "MEDIUM"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
deployLuaWithSecrets = Resource{
|
||||
Namespace: "default",
|
||||
Kind: "Deploy",
|
||||
Name: "lua",
|
||||
Results: types.Results{
|
||||
{Secrets: []ftypes.SecretFinding{
|
||||
{RuleID: "secret1", Severity: "CRITICAL"},
|
||||
{RuleID: "secret2", Severity: "MEDIUM"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
apiseverPodWithMisconfigAndInfra = Resource{
|
||||
Namespace: "kube-system",
|
||||
Kind: "Pod",
|
||||
Name: "kube-apiserver",
|
||||
Results: types.Results{
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{
|
||||
{ID: "KSV-ID100", Status: types.StatusFailure, Severity: "LOW"},
|
||||
{ID: "KSV-ID101", Status: types.StatusFailure, Severity: "MEDIUM"},
|
||||
{ID: "KSV-ID102", Status: types.StatusFailure, Severity: "HIGH"},
|
||||
|
||||
{ID: "KCV-ID100", Status: types.StatusFailure, Severity: "LOW"},
|
||||
{ID: "KCV-ID101", Status: types.StatusFailure, Severity: "MEDIUM"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestReport_consolidate(t *testing.T) {
|
||||
@@ -209,48 +284,298 @@ func Test_rbacResource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_separateMisConfigRoleAssessment(t *testing.T) {
|
||||
func Test_separateMisconfigReports(t *testing.T) {
|
||||
k8sReport := Report{Misconfigurations: []Resource{
|
||||
{Kind: "Role"},
|
||||
{Kind: "Deployment"},
|
||||
{Kind: "StatefulSet"},
|
||||
{Kind: "Pod", Namespace: "kube-system", Results: []types.Result{
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "KCV-0001"}}},
|
||||
{Misconfigurations: []types.DetectedMisconfiguration{{ID: "KSV-0001"}}},
|
||||
}},
|
||||
}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
k8sReport Report
|
||||
opts flag.ScanOptions
|
||||
wantRbacReport Report
|
||||
wantMisConfigReport Report
|
||||
name string
|
||||
k8sReport Report
|
||||
securityChecks []string
|
||||
components []string
|
||||
expectedReports []Report
|
||||
}{
|
||||
{
|
||||
name: "Role and Deployment Reports",
|
||||
k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}},
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{"config", "rbac"}},
|
||||
wantRbacReport: Report{Misconfigurations: []Resource{{Kind: "Role"}}},
|
||||
wantMisConfigReport: Report{Misconfigurations: []Resource{{Kind: "Deployment"}}},
|
||||
name: "Config, Rbac, and Infra Reports",
|
||||
k8sReport: k8sReport,
|
||||
securityChecks: []string{types.SecurityCheckConfig, types.SecurityCheckRbac},
|
||||
components: []string{workloadComponent, infraComponent},
|
||||
expectedReports: []Report{ // the order matter for the test
|
||||
{Misconfigurations: []Resource{{Kind: "Deployment"}, {Kind: "StatefulSet"}, {Kind: "Pod"}}},
|
||||
{Misconfigurations: []Resource{{Kind: "Role"}}},
|
||||
{Misconfigurations: []Resource{{Kind: "Pod"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Role Report Only",
|
||||
k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}},
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{"rbac"}},
|
||||
wantRbacReport: Report{Misconfigurations: []Resource{{Kind: "Role"}}},
|
||||
wantMisConfigReport: Report{Misconfigurations: []Resource{}},
|
||||
name: "Config and Infra for the same resource",
|
||||
k8sReport: k8sReport,
|
||||
securityChecks: []string{types.SecurityCheckConfig},
|
||||
components: []string{workloadComponent, infraComponent},
|
||||
expectedReports: []Report{ // the order matter for the test
|
||||
{Misconfigurations: []Resource{{Kind: "Deployment"}, {Kind: "StatefulSet"}, {Kind: "Pod"}}},
|
||||
{Misconfigurations: []Resource{{Kind: "Pod"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Deployment Report Only",
|
||||
k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}},
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{"config"}},
|
||||
wantRbacReport: Report{Misconfigurations: []Resource{}},
|
||||
wantMisConfigReport: Report{Misconfigurations: []Resource{{Kind: "Deployment"}}},
|
||||
name: "Role Report Only",
|
||||
k8sReport: k8sReport,
|
||||
securityChecks: []string{types.SecurityCheckRbac},
|
||||
expectedReports: []Report{
|
||||
{Misconfigurations: []Resource{{Kind: "Role"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No Deployment & No Role Reports",
|
||||
k8sReport: Report{Misconfigurations: []Resource{{Kind: "Role"}, {Kind: "Deployment"}}},
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{"vuln"}},
|
||||
wantRbacReport: Report{Misconfigurations: []Resource{}},
|
||||
wantMisConfigReport: Report{Misconfigurations: []Resource{}},
|
||||
name: "Config Report Only",
|
||||
k8sReport: k8sReport,
|
||||
securityChecks: []string{types.SecurityCheckConfig},
|
||||
components: []string{workloadComponent},
|
||||
expectedReports: []Report{
|
||||
{Misconfigurations: []Resource{{Kind: "Deployment"}, {Kind: "StatefulSet"}, {Kind: "Pod"}}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Infra Report Only",
|
||||
k8sReport: k8sReport,
|
||||
securityChecks: []string{types.SecurityCheckConfig},
|
||||
components: []string{infraComponent},
|
||||
expectedReports: []Report{
|
||||
{Misconfigurations: []Resource{{Kind: "Pod"}}},
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: add vuln only
|
||||
// TODO: add secret only
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
misConfig, rbac := separateMisConfigRoleAssessment(tt.k8sReport, tt.opts.SecurityChecks)
|
||||
assert.Equal(t, len(tt.wantMisConfigReport.Misconfigurations), len(misConfig.Misconfigurations))
|
||||
assert.Equal(t, len(tt.wantRbacReport.Misconfigurations), len(rbac.Misconfigurations))
|
||||
reports := separateMisconfigReports(tt.k8sReport, tt.securityChecks, tt.components)
|
||||
assert.Equal(t, len(tt.expectedReports), len(reports))
|
||||
|
||||
for i := range reports {
|
||||
assert.Equal(t, len(tt.expectedReports[i].Misconfigurations), len(reports[i].report.Misconfigurations))
|
||||
for j, m := range tt.expectedReports[i].Misconfigurations {
|
||||
assert.Equal(t, m.Kind, reports[i].report.Misconfigurations[j].Kind)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportWrite_Summary(t *testing.T) {
|
||||
allSeverities := []dbTypes.Severity{
|
||||
dbTypes.SeverityUnknown,
|
||||
dbTypes.SeverityLow,
|
||||
dbTypes.SeverityMedium,
|
||||
dbTypes.SeverityHigh,
|
||||
dbTypes.SeverityCritical,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
report Report
|
||||
opt Option
|
||||
securityChecks []string
|
||||
components []string
|
||||
severities []dbTypes.Severity
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "Only config, all serverities",
|
||||
report: Report{
|
||||
ClusterName: "test",
|
||||
Misconfigurations: []Resource{deployOrionWithMisconfigs},
|
||||
},
|
||||
securityChecks: []string{types.SecurityCheckConfig},
|
||||
components: []string{workloadComponent},
|
||||
severities: allSeverities,
|
||||
expectedOutput: `Summary Report for test
|
||||
=======================
|
||||
|
||||
Workload Assessment
|
||||
┌───────────┬──────────────┬───────────────────┐
|
||||
│ Namespace │ Resource │ Misconfigurations │
|
||||
│ │ ├───┬───┬───┬───┬───┤
|
||||
│ │ │ C │ H │ M │ L │ U │
|
||||
├───────────┼──────────────┼───┼───┼───┼───┼───┤
|
||||
│ default │ Deploy/orion │ 1 │ 2 │ 1 │ 2 │ 1 │
|
||||
└───────────┴──────────────┴───┴───┴───┴───┴───┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`,
|
||||
},
|
||||
{
|
||||
name: "Only vuln, all serverities",
|
||||
report: Report{
|
||||
ClusterName: "test",
|
||||
Vulnerabilities: []Resource{deployOrionWithVulns},
|
||||
},
|
||||
securityChecks: []string{types.SecurityCheckVulnerability},
|
||||
severities: allSeverities,
|
||||
expectedOutput: `Summary Report for test
|
||||
=======================
|
||||
|
||||
Workload Assessment
|
||||
┌───────────┬──────────────┬───────────────────┐
|
||||
│ Namespace │ Resource │ Vulnerabilities │
|
||||
│ │ ├───┬───┬───┬───┬───┤
|
||||
│ │ │ C │ H │ M │ L │ U │
|
||||
├───────────┼──────────────┼───┼───┼───┼───┼───┤
|
||||
│ default │ Deploy/orion │ 2 │ 1 │ 2 │ 1 │ 1 │
|
||||
└───────────┴──────────────┴───┴───┴───┴───┴───┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`,
|
||||
},
|
||||
{
|
||||
name: "Only rbac, all serverities",
|
||||
report: Report{
|
||||
ClusterName: "test",
|
||||
Misconfigurations: []Resource{roleWithMisconfig},
|
||||
},
|
||||
securityChecks: []string{types.SecurityCheckRbac},
|
||||
severities: allSeverities,
|
||||
expectedOutput: `Summary Report for test
|
||||
=======================
|
||||
|
||||
RBAC Assessment
|
||||
┌───────────┬─────────────────────────────────────────────────────┬───────────────────┐
|
||||
│ Namespace │ Resource │ RBAC Assessment │
|
||||
│ │ ├───┬───┬───┬───┬───┤
|
||||
│ │ │ C │ H │ M │ L │ U │
|
||||
├───────────┼─────────────────────────────────────────────────────┼───┼───┼───┼───┼───┤
|
||||
│ default │ Role/system::leader-locking-kube-controller-manager │ │ │ 1 │ │ │
|
||||
└───────────┴─────────────────────────────────────────────────────┴───┴───┴───┴───┴───┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`,
|
||||
},
|
||||
{
|
||||
name: "Only secret, all serverities",
|
||||
report: Report{
|
||||
ClusterName: "test",
|
||||
Vulnerabilities: []Resource{deployLuaWithSecrets},
|
||||
},
|
||||
securityChecks: []string{types.SecurityCheckSecret},
|
||||
severities: allSeverities,
|
||||
expectedOutput: `Summary Report for test
|
||||
=======================
|
||||
|
||||
Workload Assessment
|
||||
┌───────────┬────────────┬───────────────────┐
|
||||
│ Namespace │ Resource │ Secrets │
|
||||
│ │ ├───┬───┬───┬───┬───┤
|
||||
│ │ │ C │ H │ M │ L │ U │
|
||||
├───────────┼────────────┼───┼───┼───┼───┼───┤
|
||||
│ default │ Deploy/lua │ 1 │ │ 1 │ │ │
|
||||
└───────────┴────────────┴───┴───┴───┴───┴───┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`,
|
||||
},
|
||||
{
|
||||
name: "apiserver, only infra and serverities",
|
||||
report: Report{
|
||||
ClusterName: "test",
|
||||
Misconfigurations: []Resource{apiseverPodWithMisconfigAndInfra},
|
||||
},
|
||||
securityChecks: []string{types.SecurityCheckConfig},
|
||||
components: []string{infraComponent},
|
||||
severities: allSeverities,
|
||||
expectedOutput: `Summary Report for test
|
||||
=======================
|
||||
|
||||
Infra Assessment
|
||||
┌─────────────┬────────────────────┬─────────────────────────────┐
|
||||
│ Namespace │ Resource │ Kubernetes Infra Assessment │
|
||||
│ │ ├─────┬─────┬─────┬─────┬─────┤
|
||||
│ │ │ C │ H │ M │ L │ U │
|
||||
├─────────────┼────────────────────┼─────┼─────┼─────┼─────┼─────┤
|
||||
│ kube-system │ Pod/kube-apiserver │ │ │ 1 │ 1 │ │
|
||||
└─────────────┴────────────────────┴─────┴─────┴─────┴─────┴─────┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`,
|
||||
},
|
||||
{
|
||||
name: "apiserver, vuln,config,secret and serverities",
|
||||
report: Report{
|
||||
ClusterName: "test",
|
||||
Misconfigurations: []Resource{apiseverPodWithMisconfigAndInfra},
|
||||
},
|
||||
securityChecks: []string{types.SecurityCheckVulnerability, types.SecurityCheckConfig, types.SecurityCheckSecret},
|
||||
components: []string{workloadComponent},
|
||||
severities: allSeverities,
|
||||
expectedOutput: `Summary Report for test
|
||||
=======================
|
||||
|
||||
Workload Assessment
|
||||
┌─────────────┬────────────────────┬───────────────────┬───────────────────┬───────────────────┐
|
||||
│ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ Secrets │
|
||||
│ │ ├───┬───┬───┬───┬───┼───┬───┬───┬───┬───┼───┬───┬───┬───┬───┤
|
||||
│ │ │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │
|
||||
├─────────────┼────────────────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
||||
│ kube-system │ Pod/kube-apiserver │ │ │ │ │ │ │ 1 │ 1 │ 1 │ │ │ │ │ │ │
|
||||
└─────────────┴────────────────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`,
|
||||
},
|
||||
{
|
||||
name: "apiserver, all security-checks and serverities",
|
||||
report: Report{
|
||||
ClusterName: "test",
|
||||
Misconfigurations: []Resource{apiseverPodWithMisconfigAndInfra},
|
||||
},
|
||||
securityChecks: []string{types.SecurityCheckConfig, types.SecurityCheckVulnerability,
|
||||
types.SecurityCheckRbac, types.SecurityCheckSecret},
|
||||
components: []string{workloadComponent, infraComponent},
|
||||
severities: allSeverities,
|
||||
expectedOutput: `Summary Report for test
|
||||
=======================
|
||||
|
||||
Workload Assessment
|
||||
┌─────────────┬────────────────────┬───────────────────┬───────────────────┬───────────────────┐
|
||||
│ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ Secrets │
|
||||
│ │ ├───┬───┬───┬───┬───┼───┬───┬───┬───┬───┼───┬───┬───┬───┬───┤
|
||||
│ │ │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │
|
||||
├─────────────┼────────────────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
|
||||
│ kube-system │ Pod/kube-apiserver │ │ │ │ │ │ │ 1 │ 1 │ 1 │ │ │ │ │ │ │
|
||||
└─────────────┴────────────────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN
|
||||
|
||||
|
||||
Infra Assessment
|
||||
┌─────────────┬────────────────────┬─────────────────────────────┐
|
||||
│ Namespace │ Resource │ Kubernetes Infra Assessment │
|
||||
│ │ ├─────┬─────┬─────┬─────┬─────┤
|
||||
│ │ │ C │ H │ M │ L │ U │
|
||||
├─────────────┼────────────────────┼─────┼─────┼─────┼─────┼─────┤
|
||||
│ kube-system │ Pod/kube-apiserver │ │ │ 1 │ 1 │ │
|
||||
└─────────────┴────────────────────┴─────┴─────┴─────┴─────┴─────┘
|
||||
Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
output := bytes.Buffer{}
|
||||
|
||||
opt := Option{
|
||||
Format: "table",
|
||||
Report: "summary",
|
||||
Output: &output,
|
||||
SecurityChecks: tc.securityChecks,
|
||||
Severities: tc.severities,
|
||||
Components: tc.components,
|
||||
}
|
||||
|
||||
Write(tc.report, opt)
|
||||
|
||||
assert.Equal(t, tc.expectedOutput, stripAnsi(output.String()), tc.name)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
|
||||
|
||||
var ansiRegexp = regexp.MustCompile(ansi)
|
||||
|
||||
func stripAnsi(str string) string {
|
||||
return strings.TrimSpace(ansiRegexp.ReplaceAllString(str, ""))
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func NewSummaryWriter(output io.Writer, requiredSevs []dbTypes.Severity, columnH
|
||||
}
|
||||
}
|
||||
|
||||
func ColumnHeading(securityChecks []string, availableColumns []string) []string {
|
||||
func ColumnHeading(securityChecks, components, availableColumns []string) []string {
|
||||
columns := []string{NamespaceColumn, ResourceColumn}
|
||||
securityOptions := make(map[string]interface{}, 0)
|
||||
//maintain column order (vuln,config,secret)
|
||||
@@ -44,7 +44,12 @@ func ColumnHeading(securityChecks []string, availableColumns []string) []string
|
||||
case types.SecurityCheckVulnerability:
|
||||
securityOptions[VulnerabilitiesColumn] = nil
|
||||
case types.SecurityCheckConfig:
|
||||
securityOptions[MisconfigurationsColumn] = nil
|
||||
if slices.Contains(components, workloadComponent) {
|
||||
securityOptions[MisconfigurationsColumn] = nil
|
||||
}
|
||||
if slices.Contains(components, infraComponent) {
|
||||
securityOptions[InfraAssessmentColumn] = nil
|
||||
}
|
||||
case types.SecurityCheckSecret:
|
||||
securityOptions[SecretsColumn] = nil
|
||||
case types.SecurityCheckRbac:
|
||||
@@ -71,8 +76,8 @@ func (s SummaryWriter) Write(report Report) error {
|
||||
return xerrors.Errorf("failed to write summary report: %w", err)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(s.Output, "Summary Report for %s\n", consolidated.ClusterName); err != nil {
|
||||
return xerrors.Errorf("failed to write summary report: %w", err)
|
||||
if _, err := fmt.Fprintln(s.Output, report.name); err != nil {
|
||||
return xerrors.Errorf("failed to write summary report title: %w", err)
|
||||
}
|
||||
|
||||
t := table.New(s.Output)
|
||||
@@ -94,12 +99,17 @@ func (s SummaryWriter) Write(report Report) error {
|
||||
if slices.Contains(s.ColumnsHeading, VulnerabilitiesColumn) {
|
||||
rowParts = append(rowParts, s.generateSummary(vCount)...)
|
||||
}
|
||||
if slices.Contains(s.ColumnsHeading, MisconfigurationsColumn) || slices.Contains(s.ColumnsHeading, RbacAssessmentColumn) {
|
||||
|
||||
if slices.Contains(s.ColumnsHeading, MisconfigurationsColumn) ||
|
||||
slices.Contains(s.ColumnsHeading, RbacAssessmentColumn) ||
|
||||
slices.Contains(s.ColumnsHeading, InfraAssessmentColumn) {
|
||||
rowParts = append(rowParts, s.generateSummary(mCount)...)
|
||||
}
|
||||
|
||||
if slices.Contains(s.ColumnsHeading, SecretsColumn) {
|
||||
rowParts = append(rowParts, s.generateSummary(sCount)...)
|
||||
}
|
||||
|
||||
t.AddRow(rowParts...)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,46 +6,63 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
func TestReport_ColumnHeading(t *testing.T) {
|
||||
allSecurityChecks := []string{
|
||||
types.SecurityCheckVulnerability,
|
||||
types.SecurityCheckConfig,
|
||||
types.SecurityCheckSecret,
|
||||
types.SecurityCheckRbac,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts flag.ScanOptions
|
||||
securityChecks []string
|
||||
components []string
|
||||
availableColumns []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "all workload columns",
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckVulnerability,
|
||||
types.SecurityCheckConfig, types.SecurityCheckSecret, types.SecurityCheckRbac}},
|
||||
name: "filter workload columns",
|
||||
securityChecks: allSecurityChecks,
|
||||
availableColumns: WorkloadColumns(),
|
||||
components: []string{workloadComponent, infraComponent},
|
||||
want: []string{NamespaceColumn, ResourceColumn, VulnerabilitiesColumn, MisconfigurationsColumn, SecretsColumn},
|
||||
},
|
||||
{
|
||||
name: "all rbac columns",
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckVulnerability,
|
||||
types.SecurityCheckConfig, types.SecurityCheckSecret, types.SecurityCheckRbac}},
|
||||
name: "filter rbac columns",
|
||||
securityChecks: allSecurityChecks,
|
||||
components: []string{},
|
||||
availableColumns: RoleColumns(),
|
||||
want: []string{NamespaceColumn, ResourceColumn, RbacAssessmentColumn},
|
||||
},
|
||||
{
|
||||
name: "filter infra columns",
|
||||
securityChecks: allSecurityChecks,
|
||||
components: []string{workloadComponent, infraComponent},
|
||||
availableColumns: InfraColumns(),
|
||||
want: []string{NamespaceColumn, ResourceColumn, InfraAssessmentColumn},
|
||||
},
|
||||
{
|
||||
name: "config column only",
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckConfig}},
|
||||
securityChecks: []string{types.SecurityCheckConfig},
|
||||
components: []string{workloadComponent, infraComponent},
|
||||
availableColumns: WorkloadColumns(),
|
||||
want: []string{NamespaceColumn, ResourceColumn, MisconfigurationsColumn},
|
||||
},
|
||||
{
|
||||
name: "secret column only",
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckSecret}},
|
||||
securityChecks: []string{types.SecurityCheckSecret},
|
||||
components: []string{},
|
||||
availableColumns: WorkloadColumns(),
|
||||
want: []string{NamespaceColumn, ResourceColumn, SecretsColumn},
|
||||
},
|
||||
{
|
||||
name: "vuln column only",
|
||||
opts: flag.ScanOptions{SecurityChecks: []string{types.SecurityCheckVulnerability}},
|
||||
securityChecks: []string{types.SecurityCheckVulnerability},
|
||||
components: []string{},
|
||||
availableColumns: WorkloadColumns(),
|
||||
want: []string{NamespaceColumn, ResourceColumn, VulnerabilitiesColumn},
|
||||
},
|
||||
@@ -53,7 +70,7 @@ func TestReport_ColumnHeading(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
column := ColumnHeading(tt.opts.SecurityChecks, tt.availableColumns)
|
||||
column := ColumnHeading(tt.securityChecks, tt.components, tt.availableColumns)
|
||||
if !assert.Equal(t, column, tt.want) {
|
||||
t.Error(fmt.Errorf("TestReport_ColumnHeading want %v got %v", tt.want, column))
|
||||
}
|
||||
|
||||
@@ -24,15 +24,21 @@ const (
|
||||
MisconfigurationsColumn = "Misconfigurations"
|
||||
SecretsColumn = "Secrets"
|
||||
RbacAssessmentColumn = "RBAC Assessment"
|
||||
InfraAssessmentColumn = "Kubernetes Infra Assessment"
|
||||
)
|
||||
|
||||
func WorkloadColumns() []string {
|
||||
return []string{VulnerabilitiesColumn, MisconfigurationsColumn, SecretsColumn}
|
||||
}
|
||||
|
||||
func RoleColumns() []string {
|
||||
return []string{RbacAssessmentColumn}
|
||||
}
|
||||
|
||||
func InfraColumns() []string {
|
||||
return []string{InfraAssessmentColumn}
|
||||
}
|
||||
|
||||
func (tw TableWriter) Write(report Report) error {
|
||||
switch tw.Report {
|
||||
case allReport:
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/aquasecurity/trivy/pkg/flag"
|
||||
"github.com/aquasecurity/trivy/pkg/k8s/report"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/scanner/local"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
@@ -58,7 +59,7 @@ func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (re
|
||||
for _, artifact := range artifacts {
|
||||
bar.Increment()
|
||||
|
||||
if slices.Contains(s.opts.SecurityChecks, types.SecurityCheckVulnerability) {
|
||||
if shouldScanVulnsOrSecrets(s.opts.SecurityChecks) {
|
||||
resources, err := s.scanVulns(ctx, artifact)
|
||||
if err != nil {
|
||||
return report.Report{}, xerrors.Errorf("scanning vulnerabilities error: %w", err)
|
||||
@@ -66,7 +67,7 @@ func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (re
|
||||
vulns = append(vulns, resources...)
|
||||
}
|
||||
|
||||
if s.shouldScanMisconfig(s.opts.SecurityChecks) {
|
||||
if local.ShouldScanMisconfigOrRbac(s.opts.SecurityChecks) {
|
||||
resource, err := s.scanMisconfigs(ctx, artifact)
|
||||
if err != nil {
|
||||
return report.Report{}, xerrors.Errorf("scanning misconfigurations error: %w", err)
|
||||
@@ -83,10 +84,6 @@ func (s *Scanner) Scan(ctx context.Context, artifacts []*artifacts.Artifact) (re
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) shouldScanMisconfig(securityChecks []string) bool {
|
||||
return slices.Contains(securityChecks, types.SecurityCheckConfig) || slices.Contains(securityChecks, types.SecurityCheckRbac)
|
||||
}
|
||||
|
||||
func (s *Scanner) scanVulns(ctx context.Context, artifact *artifacts.Artifact) ([]report.Resource, error) {
|
||||
resources := make([]report.Resource, 0, len(artifact.Images))
|
||||
|
||||
@@ -139,3 +136,8 @@ func (s *Scanner) filter(ctx context.Context, r types.Report, artifact *artifact
|
||||
|
||||
return report.CreateResource(artifact, r, nil), nil
|
||||
}
|
||||
|
||||
func shouldScanVulnsOrSecrets(securityChecks []string) bool {
|
||||
return slices.Contains(securityChecks, types.SecurityCheckVulnerability) ||
|
||||
slices.Contains(securityChecks, types.SecurityCheckSecret)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (r pkgLicenseRenderer) Render() string {
|
||||
total, summaries := summarize(r.severities, r.countSeverities())
|
||||
|
||||
target := r.result.Target + " (license)"
|
||||
renderTarget(r.w, target, r.isTerminal)
|
||||
RenderTarget(r.w, target, r.isTerminal)
|
||||
r.printf("Total: %d (%s)\n\n", total, strings.Join(summaries, ", "))
|
||||
|
||||
r.tableWriter.Render()
|
||||
@@ -116,7 +116,7 @@ func (r fileLicenseRenderer) Render() string {
|
||||
total, summaries := summarize(r.severities, r.countSeverities())
|
||||
|
||||
target := r.result.Target + " (license)"
|
||||
renderTarget(r.w, target, r.isTerminal)
|
||||
RenderTarget(r.w, target, r.isTerminal)
|
||||
r.printf("Total: %d (%s)\n\n", total, strings.Join(summaries, ", "))
|
||||
|
||||
r.tableWriter.Render()
|
||||
|
||||
@@ -53,7 +53,7 @@ func NewMisconfigRenderer(result types.Result, severities []dbTypes.Severity, tr
|
||||
|
||||
func (r *misconfigRenderer) Render() string {
|
||||
target := fmt.Sprintf("%s (%s)", r.result.Target, r.result.Type)
|
||||
renderTarget(r.w, target, r.ansi)
|
||||
RenderTarget(r.w, target, r.ansi)
|
||||
|
||||
total, summaries := summarize(r.severities, r.countSeverities())
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func NewSecretRenderer(target string, secrets []types.SecretFinding, ansi bool,
|
||||
|
||||
func (r *secretRenderer) Render() string {
|
||||
target := r.target + " (secrets)"
|
||||
renderTarget(r.w, target, r.ansi)
|
||||
RenderTarget(r.w, target, r.ansi)
|
||||
|
||||
severityCount := r.countSeverities()
|
||||
total, summaries := summarize(r.severities, severityCount)
|
||||
|
||||
@@ -92,14 +92,7 @@ func (tw Writer) write(result types.Result) {
|
||||
}
|
||||
|
||||
func (tw Writer) isOutputToTerminal() bool {
|
||||
if tw.Output != os.Stdout {
|
||||
return false
|
||||
}
|
||||
o, err := os.Stdout.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice
|
||||
return IsOutputToTerminal(tw.Output)
|
||||
}
|
||||
|
||||
func newTableWriter(output io.Writer, isTerminal bool) *table.Table {
|
||||
@@ -136,7 +129,18 @@ func summarize(specifiedSeverities []dbTypes.Severity, severityCount map[string]
|
||||
return total, summaries
|
||||
}
|
||||
|
||||
func renderTarget(w io.Writer, target string, isTerminal bool) {
|
||||
func IsOutputToTerminal(output io.Writer) bool {
|
||||
if output != os.Stdout {
|
||||
return false
|
||||
}
|
||||
o, err := os.Stdout.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice
|
||||
}
|
||||
|
||||
func RenderTarget(w io.Writer, target string, isTerminal bool) {
|
||||
if isTerminal {
|
||||
// nolint
|
||||
_ = tml.Fprintf(w, "\n<underline><bold>%s</bold></underline>\n\n", target)
|
||||
|
||||
@@ -56,7 +56,7 @@ func (r vulnerabilityRenderer) Render() string {
|
||||
if r.result.Class == types.ClassLangPkg {
|
||||
target += fmt.Sprintf(" (%s)", r.result.Type)
|
||||
}
|
||||
renderTarget(r.w, target, r.isTerminal)
|
||||
RenderTarget(r.w, target, r.isTerminal)
|
||||
r.printf("Total: %d (%s)\n\n", total, strings.Join(summaries, ", "))
|
||||
|
||||
r.tableWriter.Render()
|
||||
|
||||
@@ -133,7 +133,7 @@ func (s Scanner) Scan(ctx context.Context, target, artifactKey string, blobKeys
|
||||
}
|
||||
|
||||
// Scan IaC config files
|
||||
if shouldScanMisconfig(options.SecurityChecks) {
|
||||
if ShouldScanMisconfigOrRbac(options.SecurityChecks) {
|
||||
configResults := s.misconfsToResults(artifactDetail.Misconfigurations)
|
||||
results = append(results, configResults...)
|
||||
}
|
||||
@@ -535,6 +535,7 @@ func mergePkgs(pkgs, pkgsFromCommands []ftypes.Package) []ftypes.Package {
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func shouldScanMisconfig(securityChecks []string) bool {
|
||||
return slices.Contains(securityChecks, types.SecurityCheckConfig) || slices.Contains(securityChecks, types.SecurityCheckRbac)
|
||||
func ShouldScanMisconfigOrRbac(securityChecks []string) bool {
|
||||
return slices.Contains(securityChecks, types.SecurityCheckConfig) ||
|
||||
slices.Contains(securityChecks, types.SecurityCheckRbac)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const (
|
||||
var (
|
||||
VulnTypes = []string{VulnTypeOS, VulnTypeLibrary}
|
||||
SecurityChecks = []string{
|
||||
SecurityCheckVulnerability, SecurityCheckConfig,
|
||||
SecurityCheckRbac, SecurityCheckSecret, SecurityCheckLicense,
|
||||
SecurityCheckVulnerability, SecurityCheckConfig, SecurityCheckRbac,
|
||||
SecurityCheckSecret, SecurityCheckLicense,
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user