Repository: Checkmarx/kics (language: python)

Issue:
Ignore .terra* cache directories by default globally
**Is your feature request related to a problem? Please describe.**
When running KICS the Terraform module cache directories are scanned by default. Most consumers of modules have little to no ability to change a 3rd part module. 

**Describe the solution you'd like**
By default KICS should ignore `**/.terra*` when executing a `scan`. This would ignore both .terraform and .terragrunt cache directories that are present in module Terraform deployment modules.

**Describe alternatives you've considered**
Currently using `--exclude-paths ".terra*, ./.terra*, ./**/.terra*, .terra*/*, ./.terra*/*, ./**/.terra*/*", ...". This is functional but contrived.

**Additional context**

The full command:

```
kics scan \
        --cloud-provider "aws" \
        --exclude-paths ".terra*, **/.terra*" \
        --no-color \
        --path "." \
        --queries-path "$PATH_TO/assets/queries" \
        --type "Terraform" \
        --verbose
```


Discussion / hints:
@cxMiguelSilva is it possible to be assigned at this feature ? Always a pleasure to do Go ☺️
Hi @dim-ops, 
Of course! Take the lead! 
In case you need any help do not hesitate to ask!
  

Fix patch:
diff --git a/pkg/engine/provider/filesystem.go b/pkg/engine/provider/filesystem.go
index eaab81da3db..5b56cc04082 100644
--- a/pkg/engine/provider/filesystem.go
+++ b/pkg/engine/provider/filesystem.go
@@ -6,6 +6,7 @@ import (
 	ioFs "io/fs"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 	"syscall"
@@ -26,8 +27,11 @@ type FileSystemSourceProvider struct {
 	mu       sync.RWMutex
 }
 
-// ErrNotSupportedFile - error representing when a file format is not supported by KICS
-var ErrNotSupportedFile = errors.New("invalid file format")
+var (
+	queryRegexExcludeTerraCache = regexp.MustCompile(fmt.Sprintf(`^(.*?%s)?\.terra.*`, regexp.QuoteMeta(string(os.PathSeparator))))
+	// ErrNotSupportedFile - error representing when a file format is not supported by KICS
+	ErrNotSupportedFile = errors.New("invalid file format")
+)
 
 // NewFileSystemSourceProvider initializes a FileSystemSourceProvider with path and files that will be ignored
 func NewFileSystemSourceProvider(paths, excludes []string) (*FileSystemSourceProvider, error) {
@@ -230,7 +234,18 @@ func (s *FileSystemSourceProvider) checkConditions(info os.FileInfo, extensions
 	path string, resolved bool) (bool, error) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
+
 	if info.IsDir() {
+		// exclude terraform cache folders
+		if queryRegexExcludeTerraCache.MatchString(path) {
+			log.Info().Msgf("Directory ignored: %s", path)
+
+			err := s.AddExcluded([]string{info.Name()})
+			if err != nil {
+				return true, err
+			}
+			return true, filepath.SkipDir
+		}
 		if f, ok := s.excludes[info.Name()]; ok && containsFile(f, info) {
 			log.Info().Msgf("Directory ignored: %s", path)
 			return true, filepath.SkipDir


Test patch:
diff --git a/pkg/engine/provider/filesystem_test.go b/pkg/engine/provider/filesystem_test.go
index 7f439da60fb..3b3fe843aae 100644
--- a/pkg/engine/provider/filesystem_test.go
+++ b/pkg/engine/provider/filesystem_test.go
@@ -277,6 +277,13 @@ func TestFileSystemSourceProvider_checkConditions(t *testing.T) {
 	}
 	infoHelm, errHelm := os.Stat(filepath.FromSlash("test/fixtures/test_helm"))
 	checkStatErr(t, errHelm)
+	infoHelmTerra, errHelmTerra := os.Stat(filepath.FromSlash("test/fixtures/terra/test_helm"))
+	checkStatErr(t, errHelmTerra)
+	infoTerraCache, errTerraCache := os.Stat(filepath.FromSlash("test/fixtures/test_terra_cache"))
+	checkStatErr(t, errTerraCache)
+	infoTerraCacheFolder, errTerraCacheFolder := os.Stat(filepath.FromSlash("test/fixtures/test_terra_cache/.terraform"))
+	checkStatErr(t, errTerraCacheFolder)
+
 	type fields struct {
 		paths    []string
 		excludes map[string][]os.FileInfo
@@ -330,6 +337,22 @@ func TestFileSystemSourceProvider_checkConditions(t *testing.T) {
 				err: nil,
 			},
 		},
+		{
+			name: "check_conditions_chart, with terra on path not skip",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("test/fixtures/terra/test_helm")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoHelmTerra,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("test/fixtures/terra/test_helm"),
+			},
+			want: want{
+				got: false,
+				err: nil,
+			},
+		},
 		{
 			name: "should_skip_folder",
 			fields: fields{
@@ -350,6 +373,246 @@ func TestFileSystemSourceProvider_checkConditions(t *testing.T) {
 				err: filepath.SkipDir,
 			},
 		},
+		{
+			name: "check_condition_ignore_terra_cache for .terra",
+			fields: fields{
+				paths:    []string{filepath.FromSlash(".terra")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash(".terra"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terragrunt_cache for .terragrunt-cache",
+			fields: fields{
+				paths:    []string{filepath.FromSlash(".terragrunt-cache")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash(".terragrunt-cache"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for terra, exclude by missing chart.yaml",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("terra")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("terra"),
+			},
+			want: want{
+				got: true,
+				err: nil,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for .terraform",
+			fields: fields{
+				paths:    []string{filepath.FromSlash(".terraform")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash(".terraform"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for .terraform, exclude by missing chart.yaml",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("terraform")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("terraform"),
+			},
+			want: want{
+				got: true,
+				err: nil,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for .terra/lalala",
+			fields: fields{
+				paths:    []string{filepath.FromSlash(".terra/lalala")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash(".terra/lalala"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terragrunt_cache for .terragrunt-cache/lalala",
+			fields: fields{
+				paths:    []string{filepath.FromSlash(".terragrunt-cache/lalala")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash(".terragrunt-cache/lalala"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for .terraform/lalala",
+			fields: fields{
+				paths:    []string{filepath.FromSlash(".terraform/lalala")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash(".terraform/lalala"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for /.terra",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("/.terra")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("/.terra"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for /.terraform",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("/.terraform")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("/.terraform"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terragrunt_cache for /.terragrunt-cache",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("/.terragrunt-cache")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("/.terragrunt-cache"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for /.terra/lalala",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("/.terra/lalala")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("/.terra/lalala"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terragrunt_cache for /.terragrunt-cache/lalala",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("/.terragrunt-cache/lalala")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("/.terragrunt-cache/lalala"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "check_condition_ignore_terra_cache for /.terraform/lalala",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("/.terraform/lalala")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCache,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("/.terraform/lalala"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
+		{
+			name: "should_skip_terra_cache_folder",
+			fields: fields{
+				paths:    []string{filepath.FromSlash("test/fixtures/test_terra_cache/.terraform")},
+				excludes: nil,
+			},
+			args: args{
+				info:       infoTerraCacheFolder,
+				extensions: model.Extensions{},
+				path:       filepath.FromSlash("test/fixtures/test_terra_cache/.terraform"),
+			},
+			want: want{
+				got: true,
+				err: filepath.SkipDir,
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/test/fixtures/terra/test_helm/.helmignore b/test/fixtures/terra/test_helm/.helmignore
new file mode 100644
index 00000000000..0e8a0eb36f4
--- /dev/null
+++ b/test/fixtures/terra/test_helm/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/test/fixtures/terra/test_helm/Chart.yaml b/test/fixtures/terra/test_helm/Chart.yaml
new file mode 100644
index 00000000000..b119ca38b91
--- /dev/null
+++ b/test/fixtures/terra/test_helm/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: test_helm
+description: A Helm chart for Kubernetes
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "1.16.0"
diff --git a/test/fixtures/terra/test_helm/templates/NOTES.txt b/test/fixtures/terra/test_helm/templates/NOTES.txt
new file mode 100644
index 00000000000..cb6a2f56ec7
--- /dev/null
+++ b/test/fixtures/terra/test_helm/templates/NOTES.txt
@@ -0,0 +1,22 @@
+1. Get the application URL by running these commands:
+{{- if .Values.ingress.enabled }}
+{{- range $host := .Values.ingress.hosts }}
+  {{- range .paths }}
+  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
+  {{- end }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "test_helm.fullname" . }})
+  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+  echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "test_helm.fullname" . }}'
+  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "test_helm.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
+  echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "test_helm.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
+  echo "Visit http://127.0.0.1:8080 to use your application"
+  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
+{{- end }}
diff --git a/test/fixtures/terra/test_helm/templates/_helpers.tpl b/test/fixtures/terra/test_helm/templates/_helpers.tpl
new file mode 100644
index 00000000000..b1df7b94b7d
--- /dev/null
+++ b/test/fixtures/terra/test_helm/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "test_helm.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "test_helm.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "test_helm.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "test_helm.labels" -}}
+helm.sh/chart: {{ include "test_helm.chart" . }}
+{{ include "test_helm.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "test_helm.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "test_helm.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "test_helm.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "test_helm.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/test/fixtures/terra/test_helm/templates/service.yaml b/test/fixtures/terra/test_helm/templates/service.yaml
new file mode 100644
index 00000000000..0d6180b16ac
--- /dev/null
+++ b/test/fixtures/terra/test_helm/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "test_helm.fullname" . }}
+  labels:
+    {{- include "test_helm.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.port }}
+      targetPort: http
+      protocol: TCP
+      name: http
+  selector:
+    {{- include "test_helm.selectorLabels" . | nindent 4 }}
diff --git a/test/fixtures/terra/test_helm/values.yaml b/test/fixtures/terra/test_helm/values.yaml
new file mode 100644
index 00000000000..56b80041d4d
--- /dev/null
+++ b/test/fixtures/terra/test_helm/values.yaml
@@ -0,0 +1,79 @@
+# Default values for test_helm.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+replicaCount: 1
+
+image:
+  repository: nginx
+  pullPolicy: IfNotPresent
+  # Overrides the image tag whose default is the chart appVersion.
+  tag: ""
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+serviceAccount:
+  # Specifies whether a service account should be created
+  create: true
+  # Annotations to add to the service account
+  annotations: {}
+  # The name of the service account to use.
+  # If not set and create is true, a name is generated using the fullname template
+  name: ""
+
+podAnnotations: {}
+
+podSecurityContext: {}
+  # fsGroup: 2000
+
+securityContext: {}
+  # capabilities:
+  #   drop:
+  #   - ALL
+  # readOnlyRootFilesystem: true
+  # runAsNonRoot: true
+  # runAsUser: 1000
+
+service:
+  type: ClusterIP
+  port: 80
+
+ingress:
+  enabled: false
+  annotations: {}
+    # kubernetes.io/ingress.class: nginx
+    # kubernetes.io/tls-acme: "true"
+  hosts:
+    - host: chart-example.local
+      paths: []
+  tls: []
+  #  - secretName: chart-example-tls
+  #    hosts:
+  #      - chart-example.local
+
+resources: {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #   cpu: 100m
+  #   memory: 128Mi
+  # requests:
+  #   cpu: 100m
+  #   memory: 128Mi
+
+autoscaling:
+  enabled: false
+  minReplicas: 1
+  maxReplicas: 100
+  targetCPUUtilizationPercentage: 80
+  # targetMemoryUtilizationPercentage: 80
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/test/fixtures/test_terra_cache/.terraform/positive.tf b/test/fixtures/test_terra_cache/.terraform/positive.tf
new file mode 100644
index 00000000000..8d746db43f1
--- /dev/null
+++ b/test/fixtures/test_terra_cache/.terraform/positive.tf
@@ -0,0 +1,8 @@
+resource "aws_security_group_rule" "positive1" {
+  type              = "ingress"
+  from_port         = 3306
+  to_port           = 3306
+  protocol          = "tcp"
+  cidr_blocks       = ["0.0.0.0/0"]
+  security_group_id = aws_security_group.default.id
+}
diff --git a/test/fixtures/test_terra_cache/negative.tf b/test/fixtures/test_terra_cache/negative.tf
new file mode 100644
index 00000000000..4577f9a083e
--- /dev/null
+++ b/test/fixtures/test_terra_cache/negative.tf
@@ -0,0 +1,9 @@
+resource "aws_security_group_rule" "negative1" {
+  type              = "ingress"
+  from_port         = 3306
+  to_port           = 3306
+  protocol          = "tcp"
+  cidr_blocks       = ["0.0.2.0/0"]
+  security_group_id = aws_security_group.default.id
+}
+