github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/k8s/report/report.go (about)

     1  package report
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"strings"
     7  
     8  	"golang.org/x/exp/maps"
     9  	"golang.org/x/exp/slices"
    10  
    11  	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
    12  	"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
    13  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    14  	"github.com/devseccon/trivy/pkg/log"
    15  	"github.com/devseccon/trivy/pkg/sbom/cyclonedx/core"
    16  	"github.com/devseccon/trivy/pkg/types"
    17  )
    18  
    19  const (
    20  	AllReport     = "all"
    21  	SummaryReport = "summary"
    22  
    23  	workloadComponent = "workload"
    24  	infraComponent    = "infra"
    25  )
    26  
    27  type Option struct {
    28  	Format        types.Format
    29  	Report        string
    30  	Output        io.Writer
    31  	Severities    []dbTypes.Severity
    32  	ColumnHeading []string
    33  	Scanners      types.Scanners
    34  	Components    []string
    35  	APIVersion    string
    36  }
    37  
    38  // Report represents a kubernetes scan report
    39  type Report struct {
    40  	SchemaVersion int `json:",omitempty"`
    41  	ClusterName   string
    42  	Resources     []Resource      `json:",omitempty"`
    43  	RootComponent *core.Component `json:"-"`
    44  	name          string
    45  }
    46  
    47  // ConsolidatedReport represents a kubernetes scan report with consolidated findings
    48  type ConsolidatedReport struct {
    49  	SchemaVersion int `json:",omitempty"`
    50  	ClusterName   string
    51  	Findings      []Resource `json:",omitempty"`
    52  }
    53  
    54  // Resource represents a kubernetes resource report
    55  type Resource struct {
    56  	Namespace string `json:",omitempty"`
    57  	Kind      string
    58  	Name      string
    59  	Metadata  types.Metadata `json:",omitempty"`
    60  	Results   types.Results  `json:",omitempty"`
    61  	Error     string         `json:",omitempty"`
    62  
    63  	// original report
    64  	Report types.Report `json:"-"`
    65  }
    66  
    67  func (r Resource) fullname() string {
    68  	return strings.ToLower(fmt.Sprintf("%s/%s/%s", r.Namespace, r.Kind, r.Name))
    69  }
    70  
    71  // Failed returns whether the k8s report includes any vulnerabilities or misconfigurations
    72  func (r Report) Failed() bool {
    73  	for _, v := range r.Resources {
    74  		if v.Results.Failed() {
    75  			return true
    76  		}
    77  	}
    78  	return false
    79  }
    80  
    81  func (r Report) consolidate() ConsolidatedReport {
    82  	consolidated := ConsolidatedReport{
    83  		SchemaVersion: r.SchemaVersion,
    84  		ClusterName:   r.ClusterName,
    85  	}
    86  
    87  	index := make(map[string]Resource)
    88  	var vulnerabilities []Resource
    89  	for _, m := range r.Resources {
    90  		if vulnerabilitiesOrSecretResource(m) {
    91  			vulnerabilities = append(vulnerabilities, m)
    92  		} else {
    93  			index[m.fullname()] = m
    94  		}
    95  	}
    96  
    97  	for _, v := range vulnerabilities {
    98  		key := v.fullname()
    99  
   100  		if res, ok := index[key]; ok {
   101  			index[key] = Resource{
   102  				Namespace: res.Namespace,
   103  				Kind:      res.Kind,
   104  				Name:      res.Name,
   105  				Metadata:  res.Metadata,
   106  				Results:   append(res.Results, v.Results...),
   107  				Error:     res.Error,
   108  			}
   109  
   110  			continue
   111  		}
   112  
   113  		index[key] = v
   114  	}
   115  
   116  	consolidated.Findings = maps.Values(index)
   117  
   118  	return consolidated
   119  }
   120  
   121  // Writer defines the result write operation
   122  type Writer interface {
   123  	Write(Report) error
   124  }
   125  
   126  type reports struct {
   127  	Report  Report
   128  	Columns []string
   129  }
   130  
   131  // SeparateMisconfigReports returns 3 reports based on scanners and components flags,
   132  // - misconfiguration report
   133  // - rbac report
   134  // - infra checks report
   135  func SeparateMisconfigReports(k8sReport Report, scanners types.Scanners, components []string) []reports {
   136  
   137  	var workloadMisconfig, infraMisconfig, rbacAssessment, workloadVulnerabilities, workloadResource []Resource
   138  	for _, resource := range k8sReport.Resources {
   139  		if vulnerabilitiesOrSecretResource(resource) {
   140  			workloadVulnerabilities = append(workloadVulnerabilities, resource)
   141  			continue
   142  		}
   143  
   144  		switch {
   145  		case scanners.Enabled(types.RBACScanner) && rbacResource(resource):
   146  			rbacAssessment = append(rbacAssessment, resource)
   147  		case infraResource(resource):
   148  			workload, infra := splitInfraAndWorkloadResources(resource)
   149  
   150  			if slices.Contains(components, infraComponent) {
   151  				infraMisconfig = append(infraMisconfig, infra)
   152  			}
   153  
   154  			if slices.Contains(components, workloadComponent) {
   155  				workloadMisconfig = append(workloadMisconfig, workload)
   156  			}
   157  
   158  		case scanners.Enabled(types.MisconfigScanner) && !rbacResource(resource):
   159  			if slices.Contains(components, workloadComponent) {
   160  				workloadMisconfig = append(workloadMisconfig, resource)
   161  			}
   162  		}
   163  	}
   164  
   165  	var r []reports
   166  	workloadResource = append(workloadResource, workloadVulnerabilities...)
   167  	workloadResource = append(workloadResource, workloadMisconfig...)
   168  	if shouldAddWorkloadReport(scanners) {
   169  		workloadReport := Report{
   170  			SchemaVersion: 0,
   171  			ClusterName:   k8sReport.ClusterName,
   172  			Resources:     workloadResource,
   173  			name:          "Workload Assessment",
   174  		}
   175  
   176  		if (slices.Contains(components, workloadComponent) &&
   177  			len(workloadMisconfig) > 0) ||
   178  			len(workloadVulnerabilities) > 0 {
   179  			r = append(r, reports{
   180  				Report:  workloadReport,
   181  				Columns: WorkloadColumns(),
   182  			})
   183  		}
   184  	}
   185  
   186  	if scanners.Enabled(types.RBACScanner) && len(rbacAssessment) > 0 {
   187  		r = append(r, reports{
   188  			Report: Report{
   189  				SchemaVersion: 0,
   190  				ClusterName:   k8sReport.ClusterName,
   191  				Resources:     rbacAssessment,
   192  				name:          "RBAC Assessment",
   193  			},
   194  			Columns: RoleColumns(),
   195  		})
   196  	}
   197  
   198  	if scanners.Enabled(types.MisconfigScanner) &&
   199  		slices.Contains(components, infraComponent) &&
   200  		len(infraMisconfig) > 0 {
   201  
   202  		r = append(r, reports{
   203  			Report: Report{
   204  				SchemaVersion: 0,
   205  				ClusterName:   k8sReport.ClusterName,
   206  				Resources:     infraMisconfig,
   207  				name:          "Infra Assessment",
   208  			},
   209  			Columns: InfraColumns(),
   210  		})
   211  	}
   212  
   213  	return r
   214  }
   215  
   216  func rbacResource(misConfig Resource) bool {
   217  	return misConfig.Kind == "Role" || misConfig.Kind == "RoleBinding" || misConfig.Kind == "ClusterRole" || misConfig.Kind == "ClusterRoleBinding"
   218  }
   219  
   220  func infraResource(misConfig Resource) bool {
   221  	return (misConfig.Kind == "Pod" && misConfig.Namespace == "kube-system") || misConfig.Kind == "NodeInfo"
   222  }
   223  
   224  func CreateResource(artifact *artifacts.Artifact, report types.Report, err error) Resource {
   225  	r := CreateK8sResource(artifact, report.Results)
   226  
   227  	r.Metadata = report.Metadata
   228  	r.Report = report
   229  	// if there was any error during the scan
   230  	if err != nil {
   231  		r.Error = err.Error()
   232  	}
   233  
   234  	return r
   235  }
   236  
   237  func CreateK8sResource(artifact *artifacts.Artifact, scanResults types.Results) Resource {
   238  	results := make([]types.Result, 0, len(scanResults))
   239  	// fix target name
   240  	for _, result := range scanResults {
   241  		// if resource is a kubernetes file fix the target name,
   242  		// to avoid showing the temp file that was removed.
   243  		if result.Type == ftypes.Kubernetes {
   244  			result.Target = fmt.Sprintf("%s/%s", artifact.Kind, artifact.Name)
   245  		}
   246  		results = append(results, result)
   247  	}
   248  
   249  	r := Resource{
   250  		Namespace: artifact.Namespace,
   251  		Kind:      artifact.Kind,
   252  		Name:      artifact.Name,
   253  		Metadata:  types.Metadata{},
   254  		Results:   results,
   255  		Report: types.Report{
   256  			Results:      results,
   257  			ArtifactName: artifact.Name,
   258  		},
   259  	}
   260  
   261  	return r
   262  }
   263  
   264  func (r Report) PrintErrors() {
   265  	for _, resource := range r.Resources {
   266  		if resource.Error != "" {
   267  			log.Logger.Errorf("Error during vulnerabilities or misconfiguration scan: %s", resource.Error)
   268  		}
   269  	}
   270  }
   271  
   272  func splitInfraAndWorkloadResources(misconfig Resource) (Resource, Resource) {
   273  	workload := copyResource(misconfig)
   274  	infra := copyResource(misconfig)
   275  
   276  	workloadResults := make(types.Results, 0)
   277  	infraResults := make(types.Results, 0)
   278  
   279  	for _, result := range misconfig.Results {
   280  		var workloadMisconfigs, infraMisconfigs []types.DetectedMisconfiguration
   281  
   282  		for _, m := range result.Misconfigurations {
   283  			if strings.HasPrefix(m.ID, "KCV") {
   284  				infraMisconfigs = append(infraMisconfigs, m)
   285  				continue
   286  			}
   287  
   288  			workloadMisconfigs = append(workloadMisconfigs, m)
   289  		}
   290  
   291  		if len(workloadMisconfigs) > 0 {
   292  			workloadResults = append(workloadResults, copyResult(result, workloadMisconfigs))
   293  		}
   294  
   295  		if len(infraMisconfigs) > 0 {
   296  			infraResults = append(infraResults, copyResult(result, infraMisconfigs))
   297  		}
   298  	}
   299  
   300  	workload.Results = workloadResults
   301  	workload.Report.Results = workloadResults
   302  
   303  	infra.Results = infraResults
   304  	infra.Report.Results = infraResults
   305  
   306  	return workload, infra
   307  }
   308  
   309  func copyResource(r Resource) Resource {
   310  	return Resource{
   311  		Namespace: r.Namespace,
   312  		Kind:      r.Kind,
   313  		Name:      r.Name,
   314  		Metadata:  r.Metadata,
   315  		Error:     r.Error,
   316  		Report:    r.Report,
   317  	}
   318  }
   319  
   320  func copyResult(r types.Result, misconfigs []types.DetectedMisconfiguration) types.Result {
   321  	return types.Result{
   322  		Target:            r.Target,
   323  		Class:             r.Class,
   324  		Type:              r.Type,
   325  		MisconfSummary:    r.MisconfSummary,
   326  		Misconfigurations: misconfigs,
   327  	}
   328  }
   329  
   330  func shouldAddWorkloadReport(scanners types.Scanners) bool {
   331  	return scanners.AnyEnabled(types.MisconfigScanner, types.VulnerabilityScanner, types.SecretScanner)
   332  }
   333  
   334  func vulnerabilitiesOrSecretResource(resource Resource) bool {
   335  	return len(resource.Results) > 0 && (len(resource.Results[0].Vulnerabilities) > 0 || len(resource.Results[0].Secrets) > 0)
   336  }