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 }