Repository: tfsec/tfsec
Pull request #626 (2021-02-24 11:10:30)

Add wildcard support to requiredLabels


Patch:
diff --git a/docs-website/docs/config/custom_checks.md b/docs-website/docs/config/custom_checks.md
index ba163fd12..1c1d07edd 100644
--- a/docs-website/docs/config/custom_checks.md
+++ b/docs-website/docs/config/custom_checks.md
@@ -82,7 +82,7 @@ The check contains up of the following attributes;
 | code | The custom code that your check will be known as |
 | description | A description for the code that will be included in the output|
 |requiredTypes | The block types to apply the check to - resource, data, module, variable |
-|requiredLabels | The resource type - aws_ec2_instance for example |
+|requiredLabels | The resource type - aws_ec2_instance for example. This also supports wildcards using `*`, e.g. `aws_*` |
 |severity | How severe is the check |
 |matchSpec | See below for the MatchSpec attributes |
 |errorMessage | The error message that should be displayed in cases where the check fails |
diff --git a/docs-website/docs/config/tagged.md b/docs-website/docs/config/tagged.md
index 7b7855220..fc5a40fa5 100644
--- a/docs-website/docs/config/tagged.md
+++ b/docs-website/docs/config/tagged.md
@@ -8,7 +8,7 @@ parent: Config
 
 Tag checking allows us to ensure that Terraform confirms to company compliance requirements.
 
-Take, for example, a requirement that all assets must be tagged as with a `CostCentre` for tracking expenditure.
+Take, for example, a requirement that all assets must be tagged with a `CostCentre` for tracking expenditure.
 
 The custom check below can be used;
 
@@ -33,4 +33,6 @@ The custom check below can be used;
     "http://internal.acmecorp.com/standards/aws/tagging.html"
   ]
 }
-```
\ No newline at end of file
+```
+
+If all AWS resources should be covered, use a value of `aws_*` in the `requiredLabels` list.
diff --git a/internal/app/tfsec/scanner/check.go b/internal/app/tfsec/scanner/check.go
index baca71554..167dffffe 100644
--- a/internal/app/tfsec/scanner/check.go
+++ b/internal/app/tfsec/scanner/check.go
@@ -108,7 +108,7 @@ func (check *Check) IsRequiredForBlock(block *parser.Block) bool {
 	if len(check.RequiredLabels) > 0 {
 		var found bool
 		for _, requiredLabel := range check.RequiredLabels {
-			if requiredLabel == "*" || (len(block.Labels()) > 0 && block.TypeLabel() == requiredLabel) {
+			if requiredLabel == "*" || (len(block.Labels()) > 0 && wildcardMatch(requiredLabel, block.TypeLabel())) {
 				found = true
 				break
 			}
@@ -121,6 +121,35 @@ func (check *Check) IsRequiredForBlock(block *parser.Block) bool {
 	return true
 }
 
+func wildcardMatch(pattern string, subject string) bool {
+	if pattern == "" {
+		return false
+	}
+	parts := strings.Split(pattern, "*")
+	var lastIndex int
+	for i, part := range parts {
+		if part == "" {
+			continue
+		}
+		if i == 0 {
+			if !strings.HasPrefix(subject, part) {
+				return false
+			}
+		}
+		if i == len(parts)-1 {
+			if !strings.HasSuffix(subject, part) {
+				return false
+			}
+		}
+		newIndex := strings.Index(subject, part)
+		if newIndex < lastIndex {
+			return false
+		}
+		lastIndex = newIndex
+	}
+	return true
+}
+
 // NewResult creates a new Result, containing the given description and range
 func (check *Check) NewResult(description string, r parser.Range, severity Severity) Result {
 	return Result{


Test patch:
diff --git a/internal/app/tfsec/test/wildcard_test.go b/internal/app/tfsec/test/wildcard_test.go
new file mode 100644
index 000000000..464d6db22
--- /dev/null
+++ b/internal/app/tfsec/test/wildcard_test.go
@@ -0,0 +1,77 @@
+package test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/parser"
+	"github.com/tfsec/tfsec/internal/app/tfsec/scanner"
+)
+
+func Test_WildcardMatchingOnRequiredLabels(t *testing.T) {
+
+	tests := []struct {
+		input           string
+		pattern         string
+		expectedFailure bool
+	}{
+		{
+			pattern:         "aws_*",
+			input:           `resource "aws_instance" "blah" {}`,
+			expectedFailure: true,
+		},
+		{
+			pattern:         "gcp_*",
+			input:           `resource "aws_instance" "blah" {}`,
+			expectedFailure: false,
+		},
+		{
+			pattern:         "x_aws_*",
+			input:           `resource "aws_instance" "blah" {}`,
+			expectedFailure: false,
+		},
+		{
+			pattern:         "aws_security_group*",
+			input:           `resource "aws_security_group" "blah" {}`,
+			expectedFailure: true,
+		},
+		{
+			pattern:         "aws_security_group*",
+			input:           `resource "aws_security_group_rule" "blah" {}`,
+			expectedFailure: true,
+		},
+	}
+
+	for i, test := range tests {
+
+		code := scanner.RuleCode(fmt.Sprintf("WILD%d", i))
+
+		scanner.RegisterCheck(scanner.Check{
+			Code: code,
+			Documentation: scanner.CheckDocumentation{
+				Summary: "blah",
+			},
+			Provider:       "custom",
+			RequiredTypes:  []string{"resource"},
+			RequiredLabels: []string{test.pattern},
+			CheckFunc: func(check *scanner.Check, rootBlock *parser.Block, ctx *scanner.Context) []scanner.Result {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Custom check failed for resource %s.", rootBlock.FullName()),
+						rootBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			},
+		})
+
+		results := scanSource(test.input)
+
+		if test.expectedFailure {
+			assertCheckCode(t, code, "", results)
+		} else {
+			assertCheckCode(t, "", code, results)
+		}
+	}
+
+}