Repository: tfsec/tfsec
Pull request #585 (2021-02-09 16:53:04)

Make contains for strings and lists of strings case insensitive

owenrumney: We have EqualityOptions https://github.com/tfsec/tfsec/blob/19bf7bed6b1ae8a32f0bda256d91f17ece7efc9f/internal/app/tfsec/parser/attribute.go#L112

I wonder if we could change this to `MatchOptions` and use the same approach for contains? What do you think?
felipesere: Oh nice, yeah I'll rework this after #587 lands. 


Patch:
diff --git a/internal/app/tfsec/custom/processing.go b/internal/app/tfsec/custom/processing.go
index 0c65399fa..763e320cf 100644
--- a/internal/app/tfsec/custom/processing.go
+++ b/internal/app/tfsec/custom/processing.go
@@ -7,7 +7,9 @@ import (
 )
 
 var matchFunctions = map[CheckAction]func(*parser.Block, *MatchSpec) bool{
-	IsPresent:  func(block *parser.Block, spec *MatchSpec) bool { return block.HasChild(spec.Name) },
+	IsPresent: func(block *parser.Block, spec *MatchSpec) bool {
+		return block.HasChild(spec.Name) || spec.IgnoreUndefined
+},
 	NotPresent: func(block *parser.Block, spec *MatchSpec) bool { return !block.HasChild(spec.Name) },
 	IsEmpty: func(block *parser.Block, spec *MatchSpec) bool {
 		if block.MissingChild(spec.Name) {
@@ -40,7 +42,7 @@ var matchFunctions = map[CheckAction]func(*parser.Block, *MatchSpec) bool{
 		if attribute == nil {
 			return spec.IgnoreUndefined
 		}
-		return attribute.Contains(spec.MatchValue)
+		return attribute.Contains(spec.MatchValue, parser.IgnoreCase)
 	},
 	NotContains: func(block *parser.Block, spec *MatchSpec) bool {
 		attribute := block.GetAttribute(spec.Name)
diff --git a/internal/app/tfsec/parser/attribute.go b/internal/app/tfsec/parser/attribute.go
index 2e0b89a36..770f7f40d 100644
--- a/internal/app/tfsec/parser/attribute.go
+++ b/internal/app/tfsec/parser/attribute.go
@@ -56,7 +56,13 @@ func (attr *Attribute) Name() string {
 	return attr.hclAttribute.Name
 }
 
-func (attr *Attribute) Contains(checkValue interface{}) bool {
+func (attr *Attribute) Contains(checkValue interface{}, equalityOptions ...EqualityOption) bool {
+	ignoreCase := false
+	for _, option := range equalityOptions {
+		if option == IgnoreCase {
+			ignoreCase = true
+		}
+	}
 	val := attr.Value()
 	if val.IsNull() {
 		return false
@@ -70,23 +76,33 @@ func (attr *Attribute) Contains(checkValue interface{}) bool {
 		}
 		return false
 	}
+	stringToLookFor := fmt.Sprintf("%v", checkValue)
 	if val.Type().IsListType() || val.Type().IsTupleType() {
 		valueSlice := val.AsValueSlice()
 		for _, value := range valueSlice {
 			if value.Type().IsObjectType() || value.Type().IsMapType() {
 				valueMap := value.AsValueMap()
-				if valueMap["key"].AsString() == checkValue {
-					return true
-				}
-				return false
+				return valueMap["key"].AsString() == checkValue
+			}
+			if ignoreCase && containsIgnoreCase(value.AsString(), stringToLookFor) {
+				return true
 			}
-			if value.AsString() == checkValue {
+			if strings.Contains(value.AsString(), stringToLookFor) {
 				return true
 			}
 		}
 		return false
 	}
-	return strings.Contains(val.AsString(), fmt.Sprintf("%v", checkValue))
+
+	if ignoreCase && containsIgnoreCase(val.AsString(), stringToLookFor) {
+		return true
+	}
+
+	return strings.Contains(val.AsString(), stringToLookFor)
+}
+
+func containsIgnoreCase(left, substring string) bool {
+	return strings.Contains(strings.ToLower(left), strings.ToLower(substring))
 }
 
 func (attr *Attribute) StartsWith(prefix interface{}) bool {


Test patch:
diff --git a/internal/app/tfsec/parser/attribute_test.go b/internal/app/tfsec/parser/attribute_test.go
index 6205ef291..18e96f9ed 100644
--- a/internal/app/tfsec/parser/attribute_test.go
+++ b/internal/app/tfsec/parser/attribute_test.go
@@ -132,6 +132,7 @@ func Test_AttributeContains(t *testing.T) {
 		checkAttribute string
 		checkValue     string
 		expectedResult bool
+		ignoreCase     bool
 	}{
 		{
 			name: "bucket name contains Name",
@@ -199,6 +200,27 @@ resource "aws_autoscaling_group" "my-aws_autoscaling_group" {
 			checkValue:     "Name",
 			expectedResult: true,
 		},
+		{
+			name: "contains array of strings ignores case",
+			source: `
+resource "aws_security_group" "my-security_group" {
+	cidr_block = ["Foo", "Bar" ] 
+}`,
+			checkAttribute: "cidr_block",
+			checkValue:     "foo",
+			expectedResult: true,
+			ignoreCase:     true,
+		},
+		{
+			name: "contains array of strings ignores case",
+			source: `
+resource "aws_security_group" "my-security_group" {
+	cidr_block = ["Foo", "Bar" ] 
+}`,
+			checkAttribute: "cidr_block",
+			checkValue:     "foo",
+			expectedResult: false,
+		},
 	}
 
 	for _, test := range tests {
@@ -209,7 +231,11 @@ resource "aws_autoscaling_group" "my-aws_autoscaling_group" {
 					t.Fail()
 				}
 				attr := block.GetAttribute(test.checkAttribute)
-				assert.Equal(t, attr.Contains(test.checkValue), test.expectedResult)
+				if test.ignoreCase {
+					assert.Equal(t, attr.Contains(test.checkValue, IgnoreCase), test.expectedResult)
+				} else {
+					assert.Equal(t, attr.Contains(test.checkValue), test.expectedResult)
+				}
 			}
 		})
 	}