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

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/google/wire"
    11  	"github.com/samber/lo"
    12  	"golang.org/x/exp/slices"
    13  	"golang.org/x/xerrors"
    14  
    15  	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
    16  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    17  	"github.com/devseccon/trivy/pkg/fanal/applier"
    18  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    19  	"github.com/devseccon/trivy/pkg/licensing"
    20  	"github.com/devseccon/trivy/pkg/log"
    21  	"github.com/devseccon/trivy/pkg/scanner/langpkg"
    22  	"github.com/devseccon/trivy/pkg/scanner/ospkg"
    23  	"github.com/devseccon/trivy/pkg/scanner/post"
    24  	"github.com/devseccon/trivy/pkg/types"
    25  	"github.com/devseccon/trivy/pkg/vulnerability"
    26  
    27  	_ "github.com/devseccon/trivy/pkg/fanal/analyzer/all"
    28  	_ "github.com/devseccon/trivy/pkg/fanal/handler/all"
    29  )
    30  
    31  // SuperSet binds dependencies for Local scan
    32  var SuperSet = wire.NewSet(
    33  	vulnerability.SuperSet,
    34  	applier.NewApplier,
    35  	ospkg.NewScanner,
    36  	langpkg.NewScanner,
    37  	NewScanner,
    38  )
    39  
    40  // Scanner implements the OspkgDetector and LibraryDetector
    41  type Scanner struct {
    42  	applier        applier.Applier
    43  	osPkgScanner   ospkg.Scanner
    44  	langPkgScanner langpkg.Scanner
    45  	vulnClient     vulnerability.Client
    46  }
    47  
    48  // NewScanner is the factory method for Scanner
    49  func NewScanner(a applier.Applier, osPkgScanner ospkg.Scanner, langPkgScanner langpkg.Scanner,
    50  	vulnClient vulnerability.Client) Scanner {
    51  	return Scanner{
    52  		applier:        a,
    53  		osPkgScanner:   osPkgScanner,
    54  		langPkgScanner: langPkgScanner,
    55  		vulnClient:     vulnClient,
    56  	}
    57  }
    58  
    59  // Scan scans the artifact and return results.
    60  func (s Scanner) Scan(ctx context.Context, targetName, artifactKey string, blobKeys []string, options types.ScanOptions) (
    61  	types.Results, ftypes.OS, error) {
    62  	detail, err := s.applier.ApplyLayers(artifactKey, blobKeys)
    63  	switch {
    64  	case errors.Is(err, analyzer.ErrUnknownOS):
    65  		log.Logger.Debug("OS is not detected.")
    66  
    67  		// Packages may contain OS-independent binary information even though OS is not detected.
    68  		if len(detail.Packages) != 0 {
    69  			detail.OS = ftypes.OS{Family: "none"}
    70  		}
    71  
    72  		// If OS is not detected and repositories are detected, we'll try to use repositories as OS.
    73  		if detail.Repository != nil {
    74  			log.Logger.Debugf("Package repository: %s %s", detail.Repository.Family, detail.Repository.Release)
    75  			log.Logger.Debugf("Assuming OS is %s %s.", detail.Repository.Family, detail.Repository.Release)
    76  			detail.OS = ftypes.OS{
    77  				Family: detail.Repository.Family,
    78  				Name:   detail.Repository.Release,
    79  			}
    80  		}
    81  	case errors.Is(err, analyzer.ErrNoPkgsDetected):
    82  		log.Logger.Warn("No OS package is detected. Make sure you haven't deleted any files that contain information about the installed packages.")
    83  		log.Logger.Warn(`e.g. files under "/lib/apk/db/", "/var/lib/dpkg/" and "/var/lib/rpm"`)
    84  	case err != nil:
    85  		return nil, ftypes.OS{}, xerrors.Errorf("failed to apply layers: %w", err)
    86  	}
    87  
    88  	target := types.ScanTarget{
    89  		Name:              targetName,
    90  		OS:                detail.OS,
    91  		Repository:        detail.Repository,
    92  		Packages:          mergePkgs(detail.Packages, detail.ImageConfig.Packages, options),
    93  		Applications:      detail.Applications,
    94  		Misconfigurations: mergeMisconfigurations(targetName, detail),
    95  		Secrets:           mergeSecrets(targetName, detail),
    96  		Licenses:          detail.Licenses,
    97  		CustomResources:   detail.CustomResources,
    98  	}
    99  
   100  	return s.ScanTarget(ctx, target, options)
   101  }
   102  
   103  func (s Scanner) ScanTarget(ctx context.Context, target types.ScanTarget, options types.ScanOptions) (types.Results, ftypes.OS, error) {
   104  	var eosl bool
   105  	var results, pkgResults types.Results
   106  	var err error
   107  
   108  	// By default, we need to remove dev dependencies from the result
   109  	// IncludeDevDeps option allows you not to remove them
   110  	excludeDevDeps(target.Applications, options.IncludeDevDeps)
   111  
   112  	// Fill OS packages and language-specific packages
   113  	if options.ListAllPackages {
   114  		if res := s.osPkgScanner.Packages(target, options); len(res.Packages) != 0 {
   115  			pkgResults = append(pkgResults, res)
   116  		}
   117  		pkgResults = append(pkgResults, s.langPkgScanner.Packages(target, options)...)
   118  	}
   119  
   120  	// Scan packages for vulnerabilities
   121  	if options.Scanners.Enabled(types.VulnerabilityScanner) {
   122  		var vulnResults types.Results
   123  		vulnResults, eosl, err = s.scanVulnerabilities(target, options)
   124  		if err != nil {
   125  			return nil, ftypes.OS{}, xerrors.Errorf("failed to detect vulnerabilities: %w", err)
   126  		}
   127  		target.OS.Eosl = eosl
   128  
   129  		// Merge package results into vulnerability results
   130  		mergedResults := s.fillPkgsInVulns(pkgResults, vulnResults)
   131  
   132  		results = append(results, mergedResults...)
   133  	} else {
   134  		// If vulnerability scanning is not enabled, it just adds package results.
   135  		results = append(results, pkgResults...)
   136  	}
   137  
   138  	// Store misconfigurations
   139  	results = append(results, s.misconfsToResults(target.Misconfigurations, options)...)
   140  
   141  	// Store secrets
   142  	results = append(results, s.secretsToResults(target.Secrets, options)...)
   143  
   144  	// Scan licenses
   145  	results = append(results, s.scanLicenses(target, options)...)
   146  
   147  	// For WASM plugins and custom analyzers
   148  	if len(target.CustomResources) != 0 {
   149  		results = append(results, types.Result{
   150  			Class:           types.ClassCustom,
   151  			CustomResources: target.CustomResources,
   152  		})
   153  	}
   154  
   155  	for i := range results {
   156  		// Fill vulnerability details
   157  		s.vulnClient.FillInfo(results[i].Vulnerabilities)
   158  	}
   159  
   160  	// Post scanning
   161  	results, err = post.Scan(ctx, results)
   162  	if err != nil {
   163  		return nil, ftypes.OS{}, xerrors.Errorf("post scan error: %w", err)
   164  	}
   165  
   166  	return results, target.OS, nil
   167  }
   168  
   169  func (s Scanner) scanVulnerabilities(target types.ScanTarget, options types.ScanOptions) (
   170  	types.Results, bool, error) {
   171  	var eosl bool
   172  	var results types.Results
   173  
   174  	if slices.Contains(options.VulnType, types.VulnTypeOS) {
   175  		vuln, detectedEOSL, err := s.osPkgScanner.Scan(target, options)
   176  		if err != nil {
   177  			return nil, false, xerrors.Errorf("unable to scan OS packages: %w", err)
   178  		} else if vuln.Target != "" {
   179  			results = append(results, vuln)
   180  		}
   181  		eosl = detectedEOSL
   182  	}
   183  
   184  	if slices.Contains(options.VulnType, types.VulnTypeLibrary) {
   185  		vulns, err := s.langPkgScanner.Scan(target, options)
   186  		if err != nil {
   187  			return nil, false, xerrors.Errorf("failed to scan application libraries: %w", err)
   188  		}
   189  		results = append(results, vulns...)
   190  	}
   191  
   192  	return results, eosl, nil
   193  }
   194  
   195  func (s Scanner) fillPkgsInVulns(pkgResults, vulnResults types.Results) types.Results {
   196  	var results types.Results
   197  	if len(pkgResults) == 0 { // '--list-all-pkgs' == false or packages not found
   198  		return vulnResults
   199  	}
   200  	for _, result := range pkgResults {
   201  		if r, found := lo.Find(vulnResults, func(r types.Result) bool {
   202  			return r.Class == result.Class && r.Target == result.Target && r.Type == result.Type
   203  		}); found {
   204  			r.Packages = result.Packages
   205  			results = append(results, r)
   206  		} else { // when package result has no vulnerabilities we still need to add it to result(for 'list-all-pkgs')
   207  			results = append(results, result)
   208  		}
   209  	}
   210  	return results
   211  }
   212  
   213  func (s Scanner) misconfsToResults(misconfs []ftypes.Misconfiguration, options types.ScanOptions) types.Results {
   214  	if !ShouldScanMisconfigOrRbac(options.Scanners) {
   215  		return nil
   216  	}
   217  
   218  	return s.MisconfsToResults(misconfs)
   219  }
   220  
   221  // MisconfsToResults is exported for trivy-plugin-aqua purposes only
   222  func (s Scanner) MisconfsToResults(misconfs []ftypes.Misconfiguration) types.Results {
   223  	log.Logger.Infof("Detected config files: %d", len(misconfs))
   224  	var results types.Results
   225  	for _, misconf := range misconfs {
   226  		log.Logger.Debugf("Scanned config file: %s", misconf.FilePath)
   227  
   228  		var detected []types.DetectedMisconfiguration
   229  
   230  		for _, f := range misconf.Failures {
   231  			detected = append(detected, toDetectedMisconfiguration(f, dbTypes.SeverityCritical, types.StatusFailure, misconf.Layer))
   232  		}
   233  		for _, w := range misconf.Warnings {
   234  			detected = append(detected, toDetectedMisconfiguration(w, dbTypes.SeverityMedium, types.StatusFailure, misconf.Layer))
   235  		}
   236  		for _, w := range misconf.Successes {
   237  			detected = append(detected, toDetectedMisconfiguration(w, dbTypes.SeverityUnknown, types.StatusPassed, misconf.Layer))
   238  		}
   239  		for _, w := range misconf.Exceptions {
   240  			detected = append(detected, toDetectedMisconfiguration(w, dbTypes.SeverityUnknown, types.StatusException, misconf.Layer))
   241  		}
   242  
   243  		results = append(results, types.Result{
   244  			Target:            misconf.FilePath,
   245  			Class:             types.ClassConfig,
   246  			Type:              misconf.FileType,
   247  			Misconfigurations: detected,
   248  		})
   249  	}
   250  
   251  	sort.Slice(results, func(i, j int) bool {
   252  		return results[i].Target < results[j].Target
   253  	})
   254  
   255  	return results
   256  }
   257  
   258  func (s Scanner) secretsToResults(secrets []ftypes.Secret, options types.ScanOptions) types.Results {
   259  	if !options.Scanners.Enabled(types.SecretScanner) {
   260  		return nil
   261  	}
   262  
   263  	var results types.Results
   264  	for _, secret := range secrets {
   265  		log.Logger.Debugf("Secret file: %s", secret.FilePath)
   266  
   267  		results = append(results, types.Result{
   268  			Target:  secret.FilePath,
   269  			Class:   types.ClassSecret,
   270  			Secrets: secret.Findings,
   271  		})
   272  	}
   273  	return results
   274  }
   275  
   276  func (s Scanner) scanLicenses(target types.ScanTarget, options types.ScanOptions) types.Results {
   277  	if !options.Scanners.Enabled(types.LicenseScanner) {
   278  		return nil
   279  	}
   280  
   281  	var results types.Results
   282  	scanner := licensing.NewScanner(options.LicenseCategories)
   283  
   284  	// License - OS packages
   285  	var osPkgLicenses []types.DetectedLicense
   286  	for _, pkg := range target.Packages {
   287  		for _, license := range pkg.Licenses {
   288  			category, severity := scanner.Scan(license)
   289  			osPkgLicenses = append(osPkgLicenses, types.DetectedLicense{
   290  				Severity:   severity,
   291  				Category:   category,
   292  				PkgName:    pkg.Name,
   293  				Name:       license,
   294  				Confidence: 1.0,
   295  			})
   296  		}
   297  
   298  	}
   299  	results = append(results, types.Result{
   300  		Target:   "OS Packages",
   301  		Class:    types.ClassLicense,
   302  		Licenses: osPkgLicenses,
   303  	})
   304  
   305  	// License - language-specific packages
   306  	for _, app := range target.Applications {
   307  		var langLicenses []types.DetectedLicense
   308  		for _, lib := range app.Libraries {
   309  			for _, license := range lib.Licenses {
   310  				category, severity := scanner.Scan(license)
   311  				langLicenses = append(langLicenses, types.DetectedLicense{
   312  					Severity:   severity,
   313  					Category:   category,
   314  					PkgName:    lib.Name,
   315  					Name:       license,
   316  					Confidence: 1.0,
   317  				})
   318  			}
   319  		}
   320  
   321  		targetName := app.FilePath
   322  		if t, ok := langpkg.PkgTargets[app.Type]; ok && targetName == "" {
   323  			// When the file path is empty, we will overwrite it with the pre-defined value.
   324  			targetName = t
   325  		}
   326  		results = append(results, types.Result{
   327  			Target:   targetName,
   328  			Class:    types.ClassLicense,
   329  			Licenses: langLicenses,
   330  		})
   331  	}
   332  
   333  	// License - file header or license file
   334  	var fileLicenses []types.DetectedLicense
   335  	for _, license := range target.Licenses {
   336  		for _, finding := range license.Findings {
   337  			category, severity := scanner.Scan(finding.Name)
   338  			fileLicenses = append(fileLicenses, types.DetectedLicense{
   339  				Severity:   severity,
   340  				Category:   category,
   341  				FilePath:   license.FilePath,
   342  				Name:       finding.Name,
   343  				Confidence: finding.Confidence,
   344  				Link:       finding.Link,
   345  			})
   346  
   347  		}
   348  	}
   349  	results = append(results, types.Result{
   350  		Target:   "Loose File License(s)",
   351  		Class:    types.ClassLicenseFile,
   352  		Licenses: fileLicenses,
   353  	})
   354  
   355  	return results
   356  }
   357  
   358  func toDetectedMisconfiguration(res ftypes.MisconfResult, defaultSeverity dbTypes.Severity,
   359  	status types.MisconfStatus, layer ftypes.Layer) types.DetectedMisconfiguration {
   360  
   361  	severity := defaultSeverity
   362  	sev, err := dbTypes.NewSeverity(res.Severity)
   363  	if err != nil {
   364  		log.Logger.Warnf("severity must be %s, but %s", dbTypes.SeverityNames, res.Severity)
   365  	} else {
   366  		severity = sev
   367  	}
   368  
   369  	msg := strings.TrimSpace(res.Message)
   370  	if msg == "" {
   371  		msg = "No issues found"
   372  	}
   373  
   374  	var primaryURL string
   375  
   376  	// empty namespace implies a go rule from defsec, "builtin" refers to a built-in rego rule
   377  	// this ensures we don't generate bad links for custom policies
   378  	if res.Namespace == "" || strings.HasPrefix(res.Namespace, "builtin.") {
   379  		primaryURL = fmt.Sprintf("https://avd.aquasec.com/misconfig/%s", strings.ToLower(res.ID))
   380  		res.References = append(res.References, primaryURL)
   381  	}
   382  
   383  	if primaryURL == "" && len(res.References) > 0 {
   384  		primaryURL = res.References[0]
   385  	}
   386  
   387  	return types.DetectedMisconfiguration{
   388  		ID:          res.ID,
   389  		AVDID:       res.AVDID,
   390  		Type:        res.Type,
   391  		Title:       res.Title,
   392  		Description: res.Description,
   393  		Message:     msg,
   394  		Resolution:  res.RecommendedActions,
   395  		Namespace:   res.Namespace,
   396  		Query:       res.Query,
   397  		Severity:    severity.String(),
   398  		PrimaryURL:  primaryURL,
   399  		References:  res.References,
   400  		Status:      status,
   401  		Layer:       layer,
   402  		Traces:      res.Traces,
   403  		CauseMetadata: ftypes.CauseMetadata{
   404  			Resource:    res.Resource,
   405  			Provider:    res.Provider,
   406  			Service:     res.Service,
   407  			StartLine:   res.StartLine,
   408  			EndLine:     res.EndLine,
   409  			Code:        res.Code,
   410  			Occurrences: res.Occurrences,
   411  		},
   412  	}
   413  }
   414  
   415  func ShouldScanMisconfigOrRbac(scanners types.Scanners) bool {
   416  	return scanners.AnyEnabled(types.MisconfigScanner, types.RBACScanner)
   417  }
   418  
   419  // excludeDevDeps removes development dependencies from the list of applications
   420  func excludeDevDeps(apps []ftypes.Application, include bool) {
   421  	if include {
   422  		return
   423  	}
   424  	for i := range apps {
   425  		apps[i].Libraries = lo.Filter(apps[i].Libraries, func(lib ftypes.Package, index int) bool {
   426  			return !lib.Dev
   427  		})
   428  	}
   429  }
   430  
   431  func mergePkgs(pkgs, pkgsFromCommands []ftypes.Package, options types.ScanOptions) []ftypes.Package {
   432  	if !options.ScanRemovedPackages || len(pkgsFromCommands) == 0 {
   433  		return pkgs
   434  	}
   435  
   436  	// pkg has priority over pkgsFromCommands
   437  	uniqPkgs := make(map[string]struct{})
   438  	for _, pkg := range pkgs {
   439  		uniqPkgs[pkg.Name] = struct{}{}
   440  	}
   441  	for _, pkg := range pkgsFromCommands {
   442  		if _, ok := uniqPkgs[pkg.Name]; ok {
   443  			continue
   444  		}
   445  		pkgs = append(pkgs, pkg)
   446  	}
   447  	return pkgs
   448  }
   449  
   450  // mergeMisconfigurations merges misconfigurations on container image config
   451  func mergeMisconfigurations(targetName string, detail ftypes.ArtifactDetail) []ftypes.Misconfiguration {
   452  	if detail.ImageConfig.Misconfiguration == nil {
   453  		return detail.Misconfigurations
   454  	}
   455  
   456  	// Append misconfigurations on container image config
   457  	misconf := detail.ImageConfig.Misconfiguration
   458  	misconf.FilePath = targetName // Set the target name to the file path as container image config is not a real file.
   459  	return append(detail.Misconfigurations, *misconf)
   460  }
   461  
   462  // mergeSecrets merges secrets on container image config.
   463  func mergeSecrets(targetName string, detail ftypes.ArtifactDetail) []ftypes.Secret {
   464  	if detail.ImageConfig.Secret == nil {
   465  		return detail.Secrets
   466  	}
   467  
   468  	// Append secrets on container image config
   469  	secret := detail.ImageConfig.Secret
   470  	secret.FilePath = targetName // Set the target name to the file path as container image config is not a real file.
   471  	return append(detail.Secrets, *secret)
   472  }