github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/terraform/scanner.go (about) 1 package terraform 2 3 import ( 4 "context" 5 "io" 6 "io/fs" 7 "path/filepath" 8 "sort" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/aquasecurity/defsec/pkg/debug" 14 "github.com/aquasecurity/defsec/pkg/framework" 15 "github.com/aquasecurity/defsec/pkg/scan" 16 "github.com/aquasecurity/defsec/pkg/scanners/options" 17 "github.com/aquasecurity/defsec/pkg/terraform" 18 "github.com/aquasecurity/defsec/pkg/types" 19 "golang.org/x/exp/slices" 20 21 "github.com/aquasecurity/defsec/pkg/rego" 22 "github.com/aquasecurity/trivy-iac/pkg/extrafs" 23 "github.com/aquasecurity/trivy-iac/pkg/scanners" 24 "github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/executor" 25 "github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/parser" 26 "github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/parser/resolvers" 27 ) 28 29 var _ scanners.FSScanner = (*Scanner)(nil) 30 var _ options.ConfigurableScanner = (*Scanner)(nil) 31 var _ ConfigurableTerraformScanner = (*Scanner)(nil) 32 33 type Scanner struct { 34 sync.Mutex 35 options []options.ScannerOption 36 parserOpt []options.ParserOption 37 executorOpt []executor.Option 38 dirs map[string]struct{} 39 forceAllDirs bool 40 policyDirs []string 41 policyReaders []io.Reader 42 regoScanner *rego.Scanner 43 execLock sync.RWMutex 44 debug debug.Logger 45 frameworks []framework.Framework 46 spec string 47 loadEmbeddedLibraries bool 48 loadEmbeddedPolicies bool 49 } 50 51 func (s *Scanner) SetSpec(spec string) { 52 s.spec = spec 53 } 54 55 func (s *Scanner) SetRegoOnly(regoOnly bool) { 56 s.executorOpt = append(s.executorOpt, executor.OptionWithRegoOnly(regoOnly)) 57 } 58 59 func (s *Scanner) SetFrameworks(frameworks []framework.Framework) { 60 s.frameworks = frameworks 61 } 62 63 func (s *Scanner) SetUseEmbeddedPolicies(b bool) { 64 s.loadEmbeddedPolicies = b 65 } 66 67 func (s *Scanner) SetUseEmbeddedLibraries(b bool) { 68 s.loadEmbeddedLibraries = b 69 } 70 71 func (s *Scanner) Name() string { 72 return "Terraform" 73 } 74 75 func (s *Scanner) SetForceAllDirs(b bool) { 76 s.forceAllDirs = b 77 } 78 79 func (s *Scanner) AddParserOptions(options ...options.ParserOption) { 80 s.parserOpt = append(s.parserOpt, options...) 81 } 82 83 func (s *Scanner) AddExecutorOptions(options ...executor.Option) { 84 s.executorOpt = append(s.executorOpt, options...) 85 } 86 87 func (s *Scanner) SetPolicyReaders(readers []io.Reader) { 88 s.policyReaders = readers 89 } 90 91 func (s *Scanner) SetSkipRequiredCheck(skip bool) { 92 s.parserOpt = append(s.parserOpt, options.ParserWithSkipRequiredCheck(skip)) 93 } 94 95 func (s *Scanner) SetDebugWriter(writer io.Writer) { 96 s.parserOpt = append(s.parserOpt, options.ParserWithDebug(writer)) 97 s.executorOpt = append(s.executorOpt, executor.OptionWithDebugWriter(writer)) 98 s.debug = debug.New(writer, "terraform", "scanner") 99 } 100 101 func (s *Scanner) SetTraceWriter(_ io.Writer) { 102 } 103 104 func (s *Scanner) SetPerResultTracingEnabled(_ bool) { 105 } 106 107 func (s *Scanner) SetPolicyDirs(dirs ...string) { 108 s.policyDirs = dirs 109 } 110 111 func (s *Scanner) SetDataDirs(_ ...string) {} 112 func (s *Scanner) SetPolicyNamespaces(_ ...string) {} 113 114 func (s *Scanner) SetPolicyFilesystem(_ fs.FS) { 115 // handled by rego when option is passed on 116 } 117 118 func (s *Scanner) SetDataFilesystem(_ fs.FS) { 119 // handled by rego when option is passed on 120 } 121 func (s *Scanner) SetRegoErrorLimit(_ int) {} 122 123 type Metrics struct { 124 Parser parser.Metrics 125 Executor executor.Metrics 126 Timings struct { 127 Total time.Duration 128 } 129 } 130 131 func New(options ...options.ScannerOption) *Scanner { 132 s := &Scanner{ 133 dirs: make(map[string]struct{}), 134 options: options, 135 } 136 for _, opt := range options { 137 opt(s) 138 } 139 return s 140 } 141 142 func (s *Scanner) ScanFS(ctx context.Context, target fs.FS, dir string) (scan.Results, error) { 143 results, _, err := s.ScanFSWithMetrics(ctx, target, dir) 144 return results, err 145 } 146 147 func (s *Scanner) initRegoScanner(srcFS fs.FS) (*rego.Scanner, error) { 148 s.Lock() 149 defer s.Unlock() 150 if s.regoScanner != nil { 151 return s.regoScanner, nil 152 } 153 regoScanner := rego.NewScanner(types.SourceCloud, s.options...) 154 regoScanner.SetParentDebugLogger(s.debug) 155 156 if err := regoScanner.LoadPolicies(s.loadEmbeddedLibraries, s.loadEmbeddedPolicies, srcFS, s.policyDirs, s.policyReaders); err != nil { 157 return nil, err 158 } 159 s.regoScanner = regoScanner 160 return regoScanner, nil 161 } 162 163 // terraformRootModule represents the module to be used as the root module for Terraform deployment. 164 type terraformRootModule struct { 165 rootPath string 166 childs terraform.Modules 167 fsMap map[string]fs.FS 168 } 169 170 func excludeNonRootModules(modules []terraformRootModule) []terraformRootModule { 171 var result []terraformRootModule 172 var childPaths []string 173 174 for _, module := range modules { 175 childPaths = append(childPaths, module.childs.ChildModulesPaths()...) 176 } 177 178 for _, module := range modules { 179 // if the path of the root module matches the path of the child module, 180 // then we should not scan it 181 if !slices.Contains(childPaths, module.rootPath) { 182 result = append(result, module) 183 } 184 } 185 return result 186 } 187 188 func (s *Scanner) ScanFSWithMetrics(ctx context.Context, target fs.FS, dir string) (scan.Results, Metrics, error) { 189 190 var metrics Metrics 191 192 s.debug.Log("Scanning [%s] at '%s'...", target, dir) 193 194 // find directories which directly contain tf files (and have no parent containing tf files) 195 rootDirs := s.findRootModules(target, dir, dir) 196 sort.Strings(rootDirs) 197 198 if len(rootDirs) == 0 { 199 s.debug.Log("no root modules found") 200 return nil, metrics, nil 201 } 202 203 regoScanner, err := s.initRegoScanner(target) 204 if err != nil { 205 return nil, metrics, err 206 } 207 208 s.execLock.Lock() 209 s.executorOpt = append(s.executorOpt, executor.OptionWithRegoScanner(regoScanner), executor.OptionWithFrameworks(s.frameworks...)) 210 s.execLock.Unlock() 211 212 var allResults scan.Results 213 214 // parse all root module directories 215 var rootModules []terraformRootModule 216 for _, dir := range rootDirs { 217 218 s.debug.Log("Scanning root module '%s'...", dir) 219 220 p := parser.New(target, "", s.parserOpt...) 221 222 if err := p.ParseFS(ctx, dir); err != nil { 223 return nil, metrics, err 224 } 225 226 modules, _, err := p.EvaluateAll(ctx) 227 if err != nil { 228 return nil, metrics, err 229 } 230 231 parserMetrics := p.Metrics() 232 metrics.Parser.Counts.Blocks += parserMetrics.Counts.Blocks 233 metrics.Parser.Counts.Modules += parserMetrics.Counts.Modules 234 metrics.Parser.Counts.Files += parserMetrics.Counts.Files 235 metrics.Parser.Timings.DiskIODuration += parserMetrics.Timings.DiskIODuration 236 metrics.Parser.Timings.ParseDuration += parserMetrics.Timings.ParseDuration 237 238 rootModules = append(rootModules, terraformRootModule{ 239 rootPath: dir, 240 childs: modules, 241 fsMap: p.GetFilesystemMap(), 242 }) 243 } 244 245 rootModules = excludeNonRootModules(rootModules) 246 for _, module := range rootModules { 247 s.execLock.RLock() 248 e := executor.New(s.executorOpt...) 249 s.execLock.RUnlock() 250 results, execMetrics, err := e.Execute(module.childs) 251 if err != nil { 252 return nil, metrics, err 253 } 254 255 for i, result := range results { 256 if result.Metadata().Range().GetFS() != nil { 257 continue 258 } 259 key := result.Metadata().Range().GetFSKey() 260 if key == "" { 261 continue 262 } 263 if filesystem, ok := module.fsMap[key]; ok { 264 override := scan.Results{ 265 result, 266 } 267 override.SetSourceAndFilesystem(result.Range().GetSourcePrefix(), filesystem, false) 268 results[i] = override[0] 269 } 270 } 271 272 metrics.Executor.Counts.Passed += execMetrics.Counts.Passed 273 metrics.Executor.Counts.Failed += execMetrics.Counts.Failed 274 metrics.Executor.Counts.Ignored += execMetrics.Counts.Ignored 275 metrics.Executor.Counts.Critical += execMetrics.Counts.Critical 276 metrics.Executor.Counts.High += execMetrics.Counts.High 277 metrics.Executor.Counts.Medium += execMetrics.Counts.Medium 278 metrics.Executor.Counts.Low += execMetrics.Counts.Low 279 metrics.Executor.Timings.Adaptation += execMetrics.Timings.Adaptation 280 metrics.Executor.Timings.RunningChecks += execMetrics.Timings.RunningChecks 281 282 allResults = append(allResults, results...) 283 } 284 285 metrics.Parser.Counts.ModuleDownloads = resolvers.Remote.GetDownloadCount() 286 287 metrics.Timings.Total += metrics.Parser.Timings.DiskIODuration 288 metrics.Timings.Total += metrics.Parser.Timings.ParseDuration 289 metrics.Timings.Total += metrics.Executor.Timings.Adaptation 290 metrics.Timings.Total += metrics.Executor.Timings.RunningChecks 291 292 return allResults, metrics, nil 293 } 294 295 func (s *Scanner) removeNestedDirs(dirs []string) []string { 296 if s.forceAllDirs { 297 return dirs 298 } 299 var clean []string 300 for _, dirA := range dirs { 301 dirOK := true 302 for _, dirB := range dirs { 303 if dirA == dirB { 304 continue 305 } 306 if str, err := filepath.Rel(dirB, dirA); err == nil && !strings.HasPrefix(str, "..") { 307 dirOK = false 308 break 309 } 310 } 311 if dirOK { 312 clean = append(clean, dirA) 313 } 314 } 315 return clean 316 } 317 318 func (s *Scanner) findRootModules(target fs.FS, scanDir string, dirs ...string) []string { 319 320 var roots []string 321 var others []string 322 323 for _, dir := range dirs { 324 if s.isRootModule(target, dir) { 325 roots = append(roots, dir) 326 if !s.forceAllDirs { 327 continue 328 } 329 } 330 331 // if this isn't a root module, look at directories inside it 332 files, err := fs.ReadDir(target, filepath.ToSlash(dir)) 333 if err != nil { 334 continue 335 } 336 for _, file := range files { 337 realPath := filepath.Join(dir, file.Name()) 338 if symFS, ok := target.(extrafs.ReadLinkFS); ok { 339 realPath, err = symFS.ResolveSymlink(realPath, scanDir) 340 if err != nil { 341 s.debug.Log("failed to resolve symlink '%s': %s", file.Name(), err) 342 continue 343 } 344 } 345 if file.IsDir() { 346 others = append(others, realPath) 347 } else if statFS, ok := target.(fs.StatFS); ok { 348 info, err := statFS.Stat(filepath.ToSlash(realPath)) 349 if err != nil { 350 continue 351 } 352 if info.IsDir() { 353 others = append(others, realPath) 354 } 355 } 356 } 357 } 358 359 if (len(roots) == 0 || s.forceAllDirs) && len(others) > 0 { 360 roots = append(roots, s.findRootModules(target, scanDir, others...)...) 361 } 362 363 return s.removeNestedDirs(roots) 364 } 365 366 func (s *Scanner) isRootModule(target fs.FS, dir string) bool { 367 files, err := fs.ReadDir(target, filepath.ToSlash(dir)) 368 if err != nil { 369 s.debug.Log("failed to read dir '%s' from filesystem [%s]: %s", dir, target, err) 370 return false 371 } 372 for _, file := range files { 373 if strings.HasSuffix(file.Name(), ".tf") || strings.HasSuffix(file.Name(), ".tf.json") { 374 return true 375 } 376 } 377 return false 378 }