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

     1  package scanner
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  
    10  	cdx "github.com/CycloneDX/cyclonedx-go"
    11  	ms "github.com/mitchellh/mapstructure"
    12  	"github.com/package-url/packageurl-go"
    13  	"github.com/samber/lo"
    14  	"golang.org/x/xerrors"
    15  
    16  	"github.com/aquasecurity/go-version/pkg/version"
    17  	"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
    18  	"github.com/aquasecurity/trivy-kubernetes/pkg/bom"
    19  	cmd "github.com/devseccon/trivy/pkg/commands/artifact"
    20  	"github.com/devseccon/trivy/pkg/digest"
    21  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    22  	"github.com/devseccon/trivy/pkg/flag"
    23  	"github.com/devseccon/trivy/pkg/k8s"
    24  	"github.com/devseccon/trivy/pkg/k8s/report"
    25  	"github.com/devseccon/trivy/pkg/log"
    26  	"github.com/devseccon/trivy/pkg/parallel"
    27  	"github.com/devseccon/trivy/pkg/purl"
    28  	cyc "github.com/devseccon/trivy/pkg/sbom/cyclonedx"
    29  	"github.com/devseccon/trivy/pkg/sbom/cyclonedx/core"
    30  	"github.com/devseccon/trivy/pkg/scanner/local"
    31  	"github.com/devseccon/trivy/pkg/types"
    32  )
    33  
    34  const (
    35  	k8sCoreComponentNamespace = core.Namespace + "resource:"
    36  	k8sComponentType          = "Type"
    37  	k8sComponentName          = "Name"
    38  	k8sComponentNode          = "node"
    39  )
    40  
    41  type Scanner struct {
    42  	cluster string
    43  	runner  cmd.Runner
    44  	opts    flag.Options
    45  }
    46  
    47  func NewScanner(cluster string, runner cmd.Runner, opts flag.Options) *Scanner {
    48  	return &Scanner{
    49  		cluster,
    50  		runner,
    51  		opts,
    52  	}
    53  }
    54  
    55  func (s *Scanner) Scan(ctx context.Context, artifactsData []*artifacts.Artifact) (report.Report, error) {
    56  	// disable logs before scanning
    57  	err := log.InitLogger(s.opts.Debug, true)
    58  	if err != nil {
    59  		return report.Report{}, xerrors.Errorf("logger error: %w", err)
    60  	}
    61  
    62  	// enable log, this is done in a defer function,
    63  	// to enable logs even when the function returns earlier
    64  	// due to an error
    65  	defer func() {
    66  		err = log.InitLogger(s.opts.Debug, false)
    67  		if err != nil {
    68  			// we use log.Fatal here because the error was to enable the logger
    69  			log.Fatal(xerrors.Errorf("can't enable logger error: %w", err))
    70  		}
    71  	}()
    72  
    73  	if s.opts.Format == types.FormatCycloneDX {
    74  		rootComponent, err := clusterInfoToReportResources(artifactsData)
    75  		if err != nil {
    76  			return report.Report{}, err
    77  		}
    78  		return report.Report{
    79  			SchemaVersion: 0,
    80  			RootComponent: rootComponent,
    81  		}, nil
    82  	}
    83  	var resourceArtifacts []*artifacts.Artifact
    84  	var k8sCoreArtifacts []*artifacts.Artifact
    85  	for _, artifact := range artifactsData {
    86  		if strings.HasSuffix(artifact.Kind, "Components") || strings.HasSuffix(artifact.Kind, "Cluster") {
    87  			k8sCoreArtifacts = append(k8sCoreArtifacts, artifact)
    88  			continue
    89  		}
    90  		resourceArtifacts = append(resourceArtifacts, artifact)
    91  	}
    92  
    93  	var resources []report.Resource
    94  
    95  	type scanResult struct {
    96  		vulns     []report.Resource
    97  		misconfig report.Resource
    98  	}
    99  
   100  	onItem := func(ctx context.Context, artifact *artifacts.Artifact) (scanResult, error) {
   101  		scanResults := scanResult{}
   102  		if s.opts.Scanners.AnyEnabled(types.VulnerabilityScanner, types.SecretScanner) {
   103  			opts := s.opts
   104  			opts.Credentials = make([]ftypes.Credential, len(s.opts.Credentials))
   105  			copy(opts.Credentials, s.opts.Credentials)
   106  			// add image private registry credential auto detected from workload imagePullsecret / serviceAccount
   107  			if len(artifact.Credentials) > 0 {
   108  				for _, cred := range artifact.Credentials {
   109  					opts.RegistryOptions.Credentials = append(opts.RegistryOptions.Credentials,
   110  						ftypes.Credential{
   111  							Username: cred.Username,
   112  							Password: cred.Password,
   113  						},
   114  					)
   115  				}
   116  			}
   117  			vulns, err := s.scanVulns(ctx, artifact, opts)
   118  			if err != nil {
   119  				return scanResult{}, xerrors.Errorf("scanning vulnerabilities error: %w", err)
   120  			}
   121  			scanResults.vulns = vulns
   122  		}
   123  		if local.ShouldScanMisconfigOrRbac(s.opts.Scanners) {
   124  			misconfig, err := s.scanMisconfigs(ctx, artifact)
   125  			if err != nil {
   126  				return scanResult{}, xerrors.Errorf("scanning misconfigurations error: %w", err)
   127  			}
   128  			scanResults.misconfig = misconfig
   129  		}
   130  		return scanResults, nil
   131  	}
   132  
   133  	onResult := func(result scanResult) error {
   134  		resources = append(resources, result.vulns...)
   135  		// don't add empty misconfig results to resources slice to avoid an empty resource
   136  		if result.misconfig.Results != nil {
   137  			resources = append(resources, result.misconfig)
   138  		}
   139  		return nil
   140  	}
   141  
   142  	p := parallel.NewPipeline(s.opts.Parallel, !s.opts.Quiet, resourceArtifacts, onItem, onResult)
   143  	err = p.Do(ctx)
   144  	if err != nil {
   145  		return report.Report{}, err
   146  	}
   147  	if s.opts.Scanners.AnyEnabled(types.VulnerabilityScanner) {
   148  		k8sResource, err := s.scanK8sVulns(ctx, k8sCoreArtifacts)
   149  		if err != nil {
   150  			return report.Report{}, err
   151  		}
   152  		resources = append(resources, k8sResource...)
   153  	}
   154  	return report.Report{
   155  		SchemaVersion: 0,
   156  		ClusterName:   s.cluster,
   157  		Resources:     resources,
   158  	}, nil
   159  
   160  }
   161  
   162  func (s *Scanner) scanVulns(ctx context.Context, artifact *artifacts.Artifact, opts flag.Options) ([]report.Resource, error) {
   163  	resources := make([]report.Resource, 0, len(artifact.Images))
   164  
   165  	for _, image := range artifact.Images {
   166  
   167  		opts.Target = image
   168  
   169  		imageReport, err := s.runner.ScanImage(ctx, opts)
   170  
   171  		if err != nil {
   172  			log.Logger.Warnf("failed to scan image %s: %s", image, err)
   173  			resources = append(resources, report.CreateResource(artifact, imageReport, err))
   174  			continue
   175  		}
   176  
   177  		resource, err := s.filter(ctx, imageReport, artifact)
   178  		if err != nil {
   179  			return nil, xerrors.Errorf("filter error: %w", err)
   180  		}
   181  
   182  		resources = append(resources, resource)
   183  	}
   184  
   185  	return resources, nil
   186  }
   187  
   188  func (s *Scanner) scanMisconfigs(ctx context.Context, artifact *artifacts.Artifact) (report.Resource, error) {
   189  	configFile, err := createTempFile(artifact)
   190  	if err != nil {
   191  		return report.Resource{}, xerrors.Errorf("scan error: %w", err)
   192  	}
   193  
   194  	s.opts.Target = configFile
   195  
   196  	configReport, err := s.runner.ScanFilesystem(ctx, s.opts)
   197  	// remove config file after scanning
   198  	removeFile(configFile)
   199  	if err != nil {
   200  		log.Logger.Debugf("failed to scan config %s/%s: %s", artifact.Kind, artifact.Name, err)
   201  		return report.CreateResource(artifact, configReport, err), err
   202  	}
   203  
   204  	return s.filter(ctx, configReport, artifact)
   205  }
   206  func (s *Scanner) filter(ctx context.Context, r types.Report, artifact *artifacts.Artifact) (report.Resource, error) {
   207  	var err error
   208  	r, err = s.runner.Filter(ctx, s.opts, r)
   209  	if err != nil {
   210  		return report.Resource{}, xerrors.Errorf("filter error: %w", err)
   211  	}
   212  	return report.CreateResource(artifact, r, nil), nil
   213  }
   214  
   215  const (
   216  	golang                 = "golang"
   217  	oci                    = "oci"
   218  	kubelet                = "k8s.io/kubelet"
   219  	controlPlaneComponents = "ControlPlaneComponents"
   220  	clusterInfo            = "Cluster"
   221  	nodeComponents         = "NodeComponents"
   222  	nodeCoreComponents     = "node-core-components"
   223  )
   224  
   225  func (s *Scanner) scanK8sVulns(ctx context.Context, artifactsData []*artifacts.Artifact) ([]report.Resource, error) {
   226  	var resources []report.Resource
   227  	var nodeName string
   228  	if nodeName = findNodeName(artifactsData); nodeName == "" {
   229  		return resources, nil
   230  	}
   231  
   232  	k8sScanner := k8s.NewKubenetesScanner()
   233  	scanOptions := types.ScanOptions{
   234  		Scanners: s.opts.Scanners,
   235  		VulnType: s.opts.VulnType,
   236  	}
   237  	for _, artifact := range artifactsData {
   238  		switch artifact.Kind {
   239  		case controlPlaneComponents:
   240  			var comp bom.Component
   241  			err := ms.Decode(artifact.RawResource, &comp)
   242  			if err != nil {
   243  				return nil, err
   244  			}
   245  
   246  			lang := k8sNamespace(comp.Version, nodeName)
   247  			results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{
   248  				Applications: []ftypes.Application{
   249  					{
   250  						Type:     ftypes.LangType(lang),
   251  						FilePath: artifact.Name,
   252  						Libraries: []ftypes.Package{
   253  							{
   254  								Name:    comp.Name,
   255  								Version: comp.Version,
   256  							},
   257  						},
   258  					},
   259  				},
   260  			}, scanOptions)
   261  			if err != nil {
   262  				return nil, err
   263  			}
   264  			if results != nil {
   265  				resources = append(resources, report.CreateK8sResource(artifact, results))
   266  			}
   267  		case nodeComponents:
   268  			var nf bom.NodeInfo
   269  			err := ms.Decode(artifact.RawResource, &nf)
   270  			if err != nil {
   271  				return nil, err
   272  			}
   273  			kubeletVersion := sanitizedVersion(nf.KubeletVersion)
   274  			lang := k8sNamespace(kubeletVersion, nodeName)
   275  			runtimeName, runtimeVersion := runtimeNameVersion(nf.ContainerRuntimeVersion)
   276  			results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{
   277  				Applications: []ftypes.Application{
   278  					{
   279  						Type:     ftypes.LangType(lang),
   280  						FilePath: artifact.Name,
   281  						Libraries: []ftypes.Package{
   282  							{
   283  								Name:    kubelet,
   284  								Version: kubeletVersion,
   285  							},
   286  						},
   287  					},
   288  					{
   289  						Type:     ftypes.GoBinary,
   290  						FilePath: artifact.Name,
   291  						Libraries: []ftypes.Package{
   292  							{
   293  								Name:    runtimeName,
   294  								Version: runtimeVersion,
   295  							},
   296  						},
   297  					},
   298  				},
   299  			}, scanOptions)
   300  			if err != nil {
   301  				return nil, err
   302  			}
   303  			if results != nil {
   304  				resources = append(resources, report.CreateK8sResource(artifact, results))
   305  			}
   306  		case clusterInfo:
   307  			var cf bom.ClusterInfo
   308  			err := ms.Decode(artifact.RawResource, &cf)
   309  			if err != nil {
   310  				return nil, err
   311  			}
   312  			lang := k8sNamespace(cf.Version, nodeName)
   313  
   314  			results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{
   315  				Applications: []ftypes.Application{
   316  					{
   317  						Type:     ftypes.LangType(lang),
   318  						FilePath: artifact.Name,
   319  						Libraries: []ftypes.Package{
   320  							{
   321  								Name:    cf.Name,
   322  								Version: cf.Version,
   323  							},
   324  						},
   325  					},
   326  				},
   327  			}, scanOptions)
   328  			if err != nil {
   329  				return nil, err
   330  			}
   331  			if results != nil {
   332  				resources = append(resources, report.CreateK8sResource(artifact, results))
   333  			}
   334  		}
   335  	}
   336  	return resources, nil
   337  }
   338  
   339  func findNodeName(allArtifact []*artifacts.Artifact) string {
   340  	for _, artifact := range allArtifact {
   341  		if artifact.Kind != nodeComponents {
   342  			continue
   343  		}
   344  		return artifact.Name
   345  	}
   346  	return ""
   347  }
   348  
   349  func clusterInfoToReportResources(allArtifact []*artifacts.Artifact) (*core.Component, error) {
   350  	var coreComponents []*core.Component
   351  	var cInfo *core.Component
   352  
   353  	// Find the first node name to identify AKS cluster
   354  	var nodeName string
   355  	if nodeName = findNodeName(allArtifact); nodeName == "" {
   356  		return nil, fmt.Errorf("failed to find node name")
   357  	}
   358  
   359  	for _, artifact := range allArtifact {
   360  		switch artifact.Kind {
   361  		case controlPlaneComponents:
   362  			var comp bom.Component
   363  			err := ms.Decode(artifact.RawResource, &comp)
   364  			if err != nil {
   365  				return nil, err
   366  			}
   367  			var imageComponents []*core.Component
   368  			for _, c := range comp.Containers {
   369  				name := fmt.Sprintf("%s/%s", c.Registry, c.Repository)
   370  				cDigest := c.Digest
   371  				if !strings.Contains(c.Digest, string(digest.SHA256)) {
   372  					cDigest = fmt.Sprintf("%s:%s", string(digest.SHA256), cDigest)
   373  				}
   374  				ver := sanitizedVersion(c.Version)
   375  
   376  				imagePURL, err := purl.NewPackageURL(purl.TypeOCI, types.Metadata{
   377  					RepoDigests: []string{
   378  						fmt.Sprintf("%s@%s", name, cDigest),
   379  					},
   380  				}, ftypes.Package{})
   381  
   382  				if err != nil {
   383  					return nil, xerrors.Errorf("failed to create PURL: %w", err)
   384  				}
   385  				imageComponents = append(imageComponents, &core.Component{
   386  					PackageURL: imagePURL,
   387  					Type:       cdx.ComponentTypeContainer,
   388  					Name:       name,
   389  					Version:    cDigest,
   390  					Properties: []core.Property{
   391  						{
   392  							Name:  cyc.PropertyPkgID,
   393  							Value: fmt.Sprintf("%s:%s", name, ver),
   394  						},
   395  						{
   396  							Name:  cyc.PropertyPkgType,
   397  							Value: oci,
   398  						},
   399  					},
   400  				})
   401  			}
   402  			rootComponent := &core.Component{
   403  				Name:       comp.Name,
   404  				Version:    comp.Version,
   405  				Type:       cdx.ComponentTypeApplication,
   406  				Properties: toProperties(comp.Properties, k8sCoreComponentNamespace),
   407  				Components: imageComponents,
   408  				PackageURL: generatePURL(comp.Name, comp.Version, nodeName),
   409  			}
   410  			coreComponents = append(coreComponents, rootComponent)
   411  		case nodeComponents:
   412  			var nf bom.NodeInfo
   413  			err := ms.Decode(artifact.RawResource, &nf)
   414  			if err != nil {
   415  				return nil, err
   416  			}
   417  			coreComponents = append(coreComponents, nodeComponent(nf))
   418  		case clusterInfo:
   419  			var cf bom.ClusterInfo
   420  			err := ms.Decode(artifact.RawResource, &cf)
   421  			if err != nil {
   422  				return nil, err
   423  			}
   424  			cInfo = &core.Component{
   425  				Name:       cf.Name,
   426  				Version:    cf.Version,
   427  				Properties: toProperties(cf.Properties, k8sCoreComponentNamespace),
   428  			}
   429  		default:
   430  			return nil, fmt.Errorf("resource kind %s is not supported", artifact.Kind)
   431  		}
   432  	}
   433  	rootComponent := &core.Component{
   434  		Name:       cInfo.Name,
   435  		Version:    cInfo.Version,
   436  		Type:       cdx.ComponentTypePlatform,
   437  		Properties: cInfo.Properties,
   438  		Components: coreComponents,
   439  		PackageURL: generatePURL(cInfo.Name, cInfo.Version, nodeName),
   440  	}
   441  	return rootComponent, nil
   442  }
   443  
   444  func sanitizedVersion(ver string) string {
   445  	return strings.TrimPrefix(ver, "v")
   446  }
   447  
   448  func osNameVersion(name string) (string, string) {
   449  	var buffer bytes.Buffer
   450  	var v string
   451  	var err error
   452  	parts := strings.Split(name, " ")
   453  	for _, p := range parts {
   454  		_, err = version.Parse(p)
   455  		if err != nil {
   456  			buffer.WriteString(p + " ")
   457  			continue
   458  		}
   459  		v = p
   460  		break
   461  	}
   462  	return strings.ToLower(strings.TrimSpace(buffer.String())), v
   463  }
   464  
   465  func runtimeNameVersion(name string) (string, string) {
   466  	runtime, ver, ok := strings.Cut(name, "://")
   467  	if !ok {
   468  		return "", ""
   469  	}
   470  
   471  	switch runtime {
   472  	case "cri-o":
   473  		name = "github.com/cri-o/cri-o"
   474  	case "containerd":
   475  		name = "github.com/containerd/containerd"
   476  	case "cri-dockerd":
   477  		name = "github.com/Mirantis/cri-dockerd"
   478  	}
   479  	return name, ver
   480  }
   481  
   482  func nodeComponent(nf bom.NodeInfo) *core.Component {
   483  	osName, osVersion := osNameVersion(nf.OsImage)
   484  	runtimeName, runtimeVersion := runtimeNameVersion(nf.ContainerRuntimeVersion)
   485  	kubeletVersion := sanitizedVersion(nf.KubeletVersion)
   486  	properties := toProperties(nf.Properties, "")
   487  	properties = append(properties, toProperties(map[string]string{
   488  		k8sComponentType: k8sComponentNode,
   489  		k8sComponentName: nf.NodeName,
   490  	}, k8sCoreComponentNamespace)...)
   491  	return &core.Component{
   492  		Type:       cdx.ComponentTypePlatform,
   493  		Name:       nf.NodeName,
   494  		Properties: properties,
   495  		Components: []*core.Component{
   496  			{
   497  				Type:    cdx.ComponentTypeOS,
   498  				Name:    osName,
   499  				Version: osVersion,
   500  				Properties: []core.Property{
   501  					{
   502  						Name:  "Class",
   503  						Value: string(types.ClassOSPkg),
   504  					},
   505  					{
   506  						Name:  "Type",
   507  						Value: osName,
   508  					},
   509  				},
   510  			},
   511  			{
   512  				Type: cdx.ComponentTypeApplication,
   513  				Name: nodeCoreComponents,
   514  				Properties: []core.Property{
   515  					{
   516  						Name:  "Class",
   517  						Value: string(types.ClassLangPkg),
   518  					},
   519  					{
   520  						Name:  "Type",
   521  						Value: golang,
   522  					},
   523  				},
   524  				Components: []*core.Component{
   525  					{
   526  						Type:    cdx.ComponentTypeApplication,
   527  						Name:    kubelet,
   528  						Version: kubeletVersion,
   529  						Properties: []core.Property{
   530  							{
   531  								Name:      k8sComponentType,
   532  								Value:     k8sComponentNode,
   533  								Namespace: k8sCoreComponentNamespace,
   534  							},
   535  							{
   536  								Name:      k8sComponentName,
   537  								Value:     kubelet,
   538  								Namespace: k8sCoreComponentNamespace,
   539  							},
   540  						},
   541  						PackageURL: generatePURL(kubelet, kubeletVersion, nf.NodeName),
   542  					},
   543  					{
   544  						Type:    cdx.ComponentTypeApplication,
   545  						Name:    runtimeName,
   546  						Version: runtimeVersion,
   547  						Properties: []core.Property{
   548  							{
   549  								Name:      k8sComponentType,
   550  								Value:     k8sComponentNode,
   551  								Namespace: k8sCoreComponentNamespace,
   552  							},
   553  							{
   554  								Name:      k8sComponentName,
   555  								Value:     runtimeName,
   556  								Namespace: k8sCoreComponentNamespace,
   557  							},
   558  						},
   559  						PackageURL: &purl.PackageURL{
   560  							PackageURL: *packageurl.NewPackageURL(golang, "", runtimeName, runtimeVersion, packageurl.Qualifiers{}, ""),
   561  						},
   562  					},
   563  				},
   564  			},
   565  		},
   566  	}
   567  }
   568  
   569  func toProperties(props map[string]string, namespace string) []core.Property {
   570  	properties := lo.MapToSlice(props, func(k, v string) core.Property {
   571  		return core.Property{
   572  			Name:      k,
   573  			Value:     v,
   574  			Namespace: namespace,
   575  		}
   576  	})
   577  	sort.Slice(properties, func(i, j int) bool {
   578  		return properties[i].Name < properties[j].Name
   579  	})
   580  	return properties
   581  }
   582  
   583  func generatePURL(name, ver, nodeName string) *purl.PackageURL {
   584  
   585  	var namespace string
   586  	// Identify k8s distribution. An empty namespace means upstream.
   587  	if namespace = k8sNamespace(ver, nodeName); namespace == "" {
   588  		return nil
   589  	} else if namespace == "kubernetes" {
   590  		namespace = ""
   591  	}
   592  
   593  	return &purl.PackageURL{
   594  		PackageURL: *packageurl.NewPackageURL(purl.TypeK8s, namespace, name, ver, nil, ""),
   595  	}
   596  }
   597  
   598  func k8sNamespace(ver, nodeName string) string {
   599  	namespace := "kubernetes"
   600  	switch {
   601  	case strings.Contains(ver, "eks"):
   602  		namespace = purl.NamespaceEKS
   603  	case strings.Contains(ver, "gke"):
   604  		namespace = purl.NamespaceGKE
   605  	case strings.Contains(ver, "rke2"):
   606  		namespace = purl.NamespaceRKE
   607  	case strings.Contains(ver, "hotfix"):
   608  		if !strings.Contains(nodeName, "aks") {
   609  			// Unknown k8s distribution
   610  			return ""
   611  		}
   612  		namespace = purl.NamespaceAKS
   613  	case strings.Contains(nodeName, "ocp"):
   614  		namespace = purl.NamespaceOCP
   615  	}
   616  	return namespace
   617  }