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