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

     1  package misconf
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/samber/lo"
    15  	"golang.org/x/xerrors"
    16  
    17  	"github.com/aquasecurity/defsec/pkg/scan"
    18  	"github.com/aquasecurity/defsec/pkg/scanners/options"
    19  	"github.com/aquasecurity/trivy-iac/pkg/detection"
    20  	"github.com/aquasecurity/trivy-iac/pkg/scanners"
    21  	"github.com/aquasecurity/trivy-iac/pkg/scanners/azure/arm"
    22  	cfscanner "github.com/aquasecurity/trivy-iac/pkg/scanners/cloudformation"
    23  	cfparser "github.com/aquasecurity/trivy-iac/pkg/scanners/cloudformation/parser"
    24  	dfscanner "github.com/aquasecurity/trivy-iac/pkg/scanners/dockerfile"
    25  	"github.com/aquasecurity/trivy-iac/pkg/scanners/helm"
    26  	k8sscanner "github.com/aquasecurity/trivy-iac/pkg/scanners/kubernetes"
    27  	tfscanner "github.com/aquasecurity/trivy-iac/pkg/scanners/terraform"
    28  	tfpscanner "github.com/aquasecurity/trivy-iac/pkg/scanners/terraformplan"
    29  	"github.com/devseccon/trivy/pkg/fanal/types"
    30  	"github.com/devseccon/trivy/pkg/log"
    31  	"github.com/devseccon/trivy/pkg/mapfs"
    32  
    33  	_ "embed"
    34  )
    35  
    36  var enabledDefsecTypes = map[detection.FileType]types.ConfigType{
    37  	detection.FileTypeAzureARM:       types.AzureARM,
    38  	detection.FileTypeCloudFormation: types.CloudFormation,
    39  	detection.FileTypeTerraform:      types.Terraform,
    40  	detection.FileTypeDockerfile:     types.Dockerfile,
    41  	detection.FileTypeKubernetes:     types.Kubernetes,
    42  	detection.FileTypeHelm:           types.Helm,
    43  	detection.FileTypeTerraformPlan:  types.TerraformPlan,
    44  }
    45  
    46  type ScannerOption struct {
    47  	Debug                    bool
    48  	Trace                    bool
    49  	RegoOnly                 bool
    50  	Namespaces               []string
    51  	PolicyPaths              []string
    52  	DataPaths                []string
    53  	DisableEmbeddedPolicies  bool
    54  	DisableEmbeddedLibraries bool
    55  
    56  	HelmValues              []string
    57  	HelmValueFiles          []string
    58  	HelmFileValues          []string
    59  	HelmStringValues        []string
    60  	TerraformTFVars         []string
    61  	CloudFormationParamVars []string
    62  	TfExcludeDownloaded     bool
    63  	K8sVersion              string
    64  }
    65  
    66  func (o *ScannerOption) Sort() {
    67  	sort.Strings(o.Namespaces)
    68  	sort.Strings(o.PolicyPaths)
    69  	sort.Strings(o.DataPaths)
    70  }
    71  
    72  type Scanner struct {
    73  	fileType       detection.FileType
    74  	scanner        scanners.FSScanner
    75  	hasFilePattern bool
    76  }
    77  
    78  func NewAzureARMScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) {
    79  	return newScanner(detection.FileTypeAzureARM, filePatterns, opt)
    80  }
    81  
    82  func NewCloudFormationScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) {
    83  	return newScanner(detection.FileTypeCloudFormation, filePatterns, opt)
    84  }
    85  
    86  func NewDockerfileScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) {
    87  	return newScanner(detection.FileTypeDockerfile, filePatterns, opt)
    88  }
    89  
    90  func NewHelmScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) {
    91  	return newScanner(detection.FileTypeHelm, filePatterns, opt)
    92  }
    93  
    94  func NewKubernetesScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) {
    95  	return newScanner(detection.FileTypeKubernetes, filePatterns, opt)
    96  }
    97  
    98  func NewTerraformScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) {
    99  	return newScanner(detection.FileTypeTerraform, filePatterns, opt)
   100  }
   101  
   102  func NewTerraformPlanScanner(filePatterns []string, opt ScannerOption) (*Scanner, error) {
   103  	return newScanner(detection.FileTypeTerraformPlan, filePatterns, opt)
   104  }
   105  
   106  func newScanner(t detection.FileType, filePatterns []string, opt ScannerOption) (*Scanner, error) {
   107  	opts, err := scannerOptions(t, opt)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	var scanner scanners.FSScanner
   113  	switch t {
   114  	case detection.FileTypeAzureARM:
   115  		scanner = arm.New(opts...)
   116  	case detection.FileTypeCloudFormation:
   117  		scanner = cfscanner.New(opts...)
   118  	case detection.FileTypeDockerfile:
   119  		scanner = dfscanner.NewScanner(opts...)
   120  	case detection.FileTypeHelm:
   121  		scanner = helm.New(opts...)
   122  	case detection.FileTypeKubernetes:
   123  		scanner = k8sscanner.NewScanner(opts...)
   124  	case detection.FileTypeTerraform:
   125  		scanner = tfscanner.New(opts...)
   126  	case detection.FileTypeTerraformPlan:
   127  		scanner = tfpscanner.New(opts...)
   128  	}
   129  
   130  	return &Scanner{
   131  		fileType:       t,
   132  		scanner:        scanner,
   133  		hasFilePattern: hasFilePattern(t, filePatterns),
   134  	}, nil
   135  }
   136  
   137  func (s *Scanner) Scan(ctx context.Context, fsys fs.FS) ([]types.Misconfiguration, error) {
   138  	newfs, err := s.filterFS(fsys)
   139  	if err != nil {
   140  		return nil, xerrors.Errorf("fs filter error: %w", err)
   141  	} else if newfs == nil {
   142  		// Skip scanning if no relevant files are found
   143  		return nil, nil
   144  	}
   145  
   146  	log.Logger.Debugf("Scanning %s files for misconfigurations...", s.scanner.Name())
   147  	results, err := s.scanner.ScanFS(ctx, newfs, ".")
   148  	if err != nil {
   149  		var invalidContentError *cfparser.InvalidContentError
   150  		if errors.As(err, &invalidContentError) {
   151  			log.Logger.Errorf("scan %q was broken with InvalidContentError: %v", s.scanner.Name(), err)
   152  			return nil, nil
   153  		}
   154  		return nil, xerrors.Errorf("scan config error: %w", err)
   155  	}
   156  
   157  	configType := enabledDefsecTypes[s.fileType]
   158  	misconfs := ResultsToMisconf(configType, s.scanner.Name(), results)
   159  
   160  	// Sort misconfigurations
   161  	for _, misconf := range misconfs {
   162  		sort.Sort(misconf.Successes)
   163  		sort.Sort(misconf.Warnings)
   164  		sort.Sort(misconf.Failures)
   165  	}
   166  
   167  	return misconfs, nil
   168  }
   169  
   170  func (s *Scanner) filterFS(fsys fs.FS) (fs.FS, error) {
   171  	mfs, ok := fsys.(*mapfs.FS)
   172  	if !ok {
   173  		// Unable to filter this filesystem
   174  		return fsys, nil
   175  	}
   176  
   177  	var foundRelevantFile bool
   178  	filter := func(path string, d fs.DirEntry) (bool, error) {
   179  		file, err := fsys.Open(path)
   180  		if err != nil {
   181  			return false, err
   182  		}
   183  		rs, ok := file.(io.ReadSeeker)
   184  		if !ok {
   185  			return false, xerrors.Errorf("type assertion error: %w", err)
   186  		}
   187  		defer file.Close()
   188  
   189  		if !s.hasFilePattern && !detection.IsType(path, rs, s.fileType) {
   190  			return true, nil
   191  		}
   192  		foundRelevantFile = true
   193  		return false, nil
   194  	}
   195  	newfs, err := mfs.FilterFunc(filter)
   196  	if err != nil {
   197  		return nil, xerrors.Errorf("fs filter error: %w", err)
   198  	}
   199  	if !foundRelevantFile {
   200  		return nil, nil
   201  	}
   202  	return newfs, nil
   203  }
   204  
   205  func scannerOptions(t detection.FileType, opt ScannerOption) ([]options.ScannerOption, error) {
   206  	opts := []options.ScannerOption{
   207  		options.ScannerWithSkipRequiredCheck(true),
   208  		options.ScannerWithEmbeddedPolicies(!opt.DisableEmbeddedPolicies),
   209  		options.ScannerWithEmbeddedLibraries(!opt.DisableEmbeddedLibraries),
   210  	}
   211  
   212  	policyFS, policyPaths, err := CreatePolicyFS(opt.PolicyPaths)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  	if policyFS != nil {
   217  		opts = append(opts, options.ScannerWithPolicyFilesystem(policyFS))
   218  	}
   219  
   220  	dataFS, dataPaths, err := CreateDataFS(opt.DataPaths, opt.K8sVersion)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	opts = append(opts,
   225  		options.ScannerWithDataDirs(dataPaths...),
   226  		options.ScannerWithDataFilesystem(dataFS),
   227  	)
   228  
   229  	if opt.Debug {
   230  		opts = append(opts, options.ScannerWithDebug(&log.PrefixedLogger{Name: "misconf"}))
   231  	}
   232  
   233  	if opt.Trace {
   234  		opts = append(opts, options.ScannerWithPerResultTracing(true))
   235  	}
   236  
   237  	if opt.RegoOnly {
   238  		opts = append(opts, options.ScannerWithRegoOnly(true))
   239  	}
   240  
   241  	if len(policyPaths) > 0 {
   242  		opts = append(opts, options.ScannerWithPolicyDirs(policyPaths...))
   243  	}
   244  
   245  	if len(opt.DataPaths) > 0 {
   246  		opts = append(opts, options.ScannerWithDataDirs(opt.DataPaths...))
   247  	}
   248  
   249  	if len(opt.Namespaces) > 0 {
   250  		opts = append(opts, options.ScannerWithPolicyNamespaces(opt.Namespaces...))
   251  	}
   252  
   253  	switch t {
   254  	case detection.FileTypeHelm:
   255  		return addHelmOpts(opts, opt), nil
   256  	case detection.FileTypeTerraform:
   257  		return addTFOpts(opts, opt)
   258  	case detection.FileTypeCloudFormation:
   259  		return addCFOpts(opts, opt)
   260  	default:
   261  		return opts, nil
   262  	}
   263  }
   264  
   265  func hasFilePattern(t detection.FileType, filePatterns []string) bool {
   266  	for _, pattern := range filePatterns {
   267  		if strings.HasPrefix(pattern, fmt.Sprintf("%s:", t)) {
   268  			return true
   269  		}
   270  	}
   271  	return false
   272  }
   273  
   274  func addTFOpts(opts []options.ScannerOption, scannerOption ScannerOption) ([]options.ScannerOption, error) {
   275  	if len(scannerOption.TerraformTFVars) > 0 {
   276  		configFS, err := createConfigFS(scannerOption.TerraformTFVars)
   277  		if err != nil {
   278  			return nil, xerrors.Errorf("failed to create Terraform config FS: %w", err)
   279  		}
   280  		opts = append(
   281  			opts,
   282  			tfscanner.ScannerWithTFVarsPaths(scannerOption.TerraformTFVars...),
   283  			tfscanner.ScannerWithConfigsFileSystem(configFS),
   284  		)
   285  	}
   286  
   287  	opts = append(opts,
   288  		tfscanner.ScannerWithAllDirectories(true),
   289  		tfscanner.ScannerWithSkipDownloaded(scannerOption.TfExcludeDownloaded),
   290  	)
   291  
   292  	return opts, nil
   293  }
   294  
   295  func addCFOpts(opts []options.ScannerOption, scannerOption ScannerOption) ([]options.ScannerOption, error) {
   296  	if len(scannerOption.CloudFormationParamVars) > 0 {
   297  		configFS, err := createConfigFS(scannerOption.CloudFormationParamVars)
   298  		if err != nil {
   299  			return nil, xerrors.Errorf("failed to create CloudFormation config FS: %w", err)
   300  		}
   301  		opts = append(
   302  			opts,
   303  			cfscanner.WithParameterFiles(scannerOption.CloudFormationParamVars...),
   304  			cfscanner.WithConfigsFS(configFS),
   305  		)
   306  	}
   307  	return opts, nil
   308  }
   309  
   310  func addHelmOpts(opts []options.ScannerOption, scannerOption ScannerOption) []options.ScannerOption {
   311  	if len(scannerOption.HelmValueFiles) > 0 {
   312  		opts = append(opts, helm.ScannerWithValuesFile(scannerOption.HelmValueFiles...))
   313  	}
   314  
   315  	if len(scannerOption.HelmValues) > 0 {
   316  		opts = append(opts, helm.ScannerWithValues(scannerOption.HelmValues...))
   317  	}
   318  
   319  	if len(scannerOption.HelmFileValues) > 0 {
   320  		opts = append(opts, helm.ScannerWithFileValues(scannerOption.HelmFileValues...))
   321  	}
   322  
   323  	if len(scannerOption.HelmStringValues) > 0 {
   324  		opts = append(opts, helm.ScannerWithStringValues(scannerOption.HelmStringValues...))
   325  	}
   326  
   327  	return opts
   328  }
   329  
   330  func createConfigFS(paths []string) (fs.FS, error) {
   331  	mfs := mapfs.New()
   332  	for _, path := range paths {
   333  		if err := mfs.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) {
   334  			return nil, xerrors.Errorf("create dir error: %w", err)
   335  		}
   336  		if err := mfs.WriteFile(path, path); err != nil {
   337  			return nil, xerrors.Errorf("write file error: %w", err)
   338  		}
   339  	}
   340  	return mfs, nil
   341  }
   342  
   343  func CreatePolicyFS(policyPaths []string) (fs.FS, []string, error) {
   344  	if len(policyPaths) == 0 {
   345  		return nil, nil, nil
   346  	}
   347  
   348  	mfs := mapfs.New()
   349  	for _, p := range policyPaths {
   350  		abs, err := filepath.Abs(p)
   351  		if err != nil {
   352  			return nil, nil, xerrors.Errorf("failed to derive absolute path from '%s': %w", p, err)
   353  		}
   354  		fi, err := os.Stat(abs)
   355  		if errors.Is(err, os.ErrNotExist) {
   356  			return nil, nil, xerrors.Errorf("policy file %q not found", abs)
   357  		} else if err != nil {
   358  			return nil, nil, xerrors.Errorf("file %q stat error: %w", abs, err)
   359  		}
   360  
   361  		if fi.IsDir() {
   362  			if err = mfs.CopyFilesUnder(abs); err != nil {
   363  				return nil, nil, xerrors.Errorf("mapfs file copy error: %w", err)
   364  			}
   365  		} else {
   366  			if err := mfs.MkdirAll(filepath.Dir(abs), os.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) {
   367  				return nil, nil, xerrors.Errorf("mapfs mkdir error: %w", err)
   368  			}
   369  			if err := mfs.WriteFile(abs, abs); err != nil {
   370  				return nil, nil, xerrors.Errorf("mapfs write error: %w", err)
   371  			}
   372  		}
   373  	}
   374  
   375  	// policy paths are no longer needed as fs.FS contains only needed files now.
   376  	policyPaths = []string{"."}
   377  
   378  	return mfs, policyPaths, nil
   379  }
   380  
   381  func CreateDataFS(dataPaths []string, opts ...string) (fs.FS, []string, error) {
   382  	fsys := mapfs.New()
   383  
   384  	// Check if k8sVersion is provided
   385  	if len(opts) > 0 {
   386  		k8sVersion := opts[0]
   387  		if err := fsys.MkdirAll("system", 0700); err != nil {
   388  			return nil, nil, err
   389  		}
   390  		data := []byte(fmt.Sprintf(`{"k8s": {"version": %q}}`, k8sVersion))
   391  		if err := fsys.WriteVirtualFile("system/k8s-version.json", data, 0600); err != nil {
   392  			return nil, nil, err
   393  		}
   394  	}
   395  
   396  	for _, path := range dataPaths {
   397  		if err := fsys.CopyFilesUnder(path); err != nil {
   398  			return nil, nil, err
   399  		}
   400  	}
   401  
   402  	// dataPaths are no longer needed as fs.FS contains only needed files now.
   403  	dataPaths = []string{"."}
   404  
   405  	return fsys, dataPaths, nil
   406  }
   407  
   408  // ResultsToMisconf is exported for trivy-plugin-aqua purposes only
   409  func ResultsToMisconf(configType types.ConfigType, scannerName string, results scan.Results) []types.Misconfiguration {
   410  	misconfs := make(map[string]types.Misconfiguration)
   411  
   412  	for _, result := range results {
   413  		flattened := result.Flatten()
   414  
   415  		query := fmt.Sprintf("data.%s.%s", result.RegoNamespace(), result.RegoRule())
   416  
   417  		ruleID := result.Rule().AVDID
   418  		if result.RegoNamespace() != "" && len(result.Rule().Aliases) > 0 {
   419  			ruleID = result.Rule().Aliases[0]
   420  		}
   421  
   422  		cause := NewCauseWithCode(result)
   423  
   424  		misconfResult := types.MisconfResult{
   425  			Namespace: result.RegoNamespace(),
   426  			Query:     query,
   427  			Message:   flattened.Description,
   428  			PolicyMetadata: types.PolicyMetadata{
   429  				ID:                 ruleID,
   430  				AVDID:              result.Rule().AVDID,
   431  				Type:               fmt.Sprintf("%s Security Check", scannerName),
   432  				Title:              result.Rule().Summary,
   433  				Description:        result.Rule().Explanation,
   434  				Severity:           string(flattened.Severity),
   435  				RecommendedActions: flattened.Resolution,
   436  				References:         flattened.Links,
   437  			},
   438  			CauseMetadata: cause,
   439  			Traces:        result.Traces(),
   440  		}
   441  
   442  		filePath := flattened.Location.Filename
   443  		misconf, ok := misconfs[filePath]
   444  		if !ok {
   445  			misconf = types.Misconfiguration{
   446  				FileType: configType,
   447  				FilePath: filepath.ToSlash(filePath), // defsec return OS-aware path
   448  			}
   449  		}
   450  
   451  		if flattened.Warning {
   452  			misconf.Warnings = append(misconf.Warnings, misconfResult)
   453  		} else {
   454  			switch flattened.Status {
   455  			case scan.StatusPassed:
   456  				misconf.Successes = append(misconf.Successes, misconfResult)
   457  			case scan.StatusIgnored:
   458  				misconf.Exceptions = append(misconf.Exceptions, misconfResult)
   459  			case scan.StatusFailed:
   460  				misconf.Failures = append(misconf.Failures, misconfResult)
   461  			}
   462  		}
   463  		misconfs[filePath] = misconf
   464  	}
   465  
   466  	return types.ToMisconfigurations(misconfs)
   467  }
   468  
   469  func NewCauseWithCode(underlying scan.Result) types.CauseMetadata {
   470  	flat := underlying.Flatten()
   471  	cause := types.CauseMetadata{
   472  		Resource:  flat.Resource,
   473  		Provider:  flat.RuleProvider.DisplayName(),
   474  		Service:   flat.RuleService,
   475  		StartLine: flat.Location.StartLine,
   476  		EndLine:   flat.Location.EndLine,
   477  	}
   478  	for _, o := range flat.Occurrences {
   479  		cause.Occurrences = append(cause.Occurrences, types.Occurrence{
   480  			Resource: o.Resource,
   481  			Filename: o.Filename,
   482  			Location: types.Location{
   483  				StartLine: o.StartLine,
   484  				EndLine:   o.EndLine,
   485  			},
   486  		})
   487  	}
   488  	if code, err := underlying.GetCode(); err == nil {
   489  		cause.Code = types.Code{
   490  			Lines: lo.Map(code.Lines, func(l scan.Line, i int) types.Line {
   491  				return types.Line{
   492  					Number:      l.Number,
   493  					Content:     l.Content,
   494  					IsCause:     l.IsCause,
   495  					Annotation:  l.Annotation,
   496  					Truncated:   l.Truncated,
   497  					Highlighted: l.Highlighted,
   498  					FirstCause:  l.FirstCause,
   499  					LastCause:   l.LastCause,
   500  				}
   501  			}),
   502  		}
   503  	}
   504  	return cause
   505  }