Repository: aquasecurity/tfsec
Pull request #1368 (2022-01-20 11:47:10)

feat: custom checks: perform submatch checks in map/object attributes
- making it possible to perform subMatch checks in map/object attributes (previously only possible on blocks).
- implementation leaves space for possible future expansion to other attribute types
- slight reformatting of the processing.go file
- added tests for the above changes

solves #1367

owenrumney: Hey @gabrielleecredera  - not ignoring this - we just need to get v1 out the door then I'll get stuck into this PR
gabrielleecredera: @owenrumney i think you undid the changes to the docs in the merge

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 1609e73bf..ed7667ce2 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,8 +1,8 @@
 ---
 name: Bug report
 about: Create a report to help us improve
-title: ''
-labels: ''
+title: 'bug: '
+labels: 'bug'
 assignees: ''
 
 ---
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index bbcbbe7d6..1cac81dfa 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,8 +1,8 @@
 ---
 name: Feature request
 about: Suggest an idea for this project
-title: ''
-labels: ''
+title: 'feat: '
+labels: 'enhancement'
 assignees: ''
 
 ---
diff --git a/.github/ISSUE_TEMPLATE/new-check-request.md b/.github/ISSUE_TEMPLATE/new-check-request.md
index b040478da..a61556028 100644
--- a/.github/ISSUE_TEMPLATE/new-check-request.md
+++ b/.github/ISSUE_TEMPLATE/new-check-request.md
@@ -1,8 +1,8 @@
 ---
 name: New check request
 about: Request a new tfsec check
-title: "[CHECK]"
-labels: new check
+title: "feat: new check for "
+labels: 'new check', 'enhancement', 'requires-defsec'
 assignees: ''
 
 ---
diff --git a/docs/getting-started/configuration/custom-checks.md b/docs/getting-started/configuration/custom-checks.md
index 3d1b34f2d..e364f9bb5 100644
--- a/docs/getting-started/configuration/custom-checks.md
+++ b/docs/getting-started/configuration/custom-checks.md
@@ -104,7 +104,7 @@ The `MatchSpec` is the what will define the check itself - this is fairly basic
 | action             | The check type - see below for more information                                                                                            |
 | value              | In cases where a value is required, the value to look for, text matching `TFSEC_VAR_{VAR_NAME}` will be replaced with the variable value   |
 | ignoreUndefined    | If the attribute is undefined, ignore and pass the check                                                                                   |
-| subMatch           | A sub MatchSpec block for nested checking - think looking for `enabled` value in a `logging` block                                         |
+| subMatch           | A sub MatchSpec block for nested checking - think looking for `enabled` value in a `logging` block, or checking a tag's value in a `tag` map attribute                                       |
 | predicateMatchSpec | An array of MatchSpec blocks to be logically aggregated by either `and` or `or` actions                                                    |
 | assignVariable     | The name of the "variable" to store the value of the `name` attribute in, has to be in uppercase and start with `TFSEC_VAR_`               |
 
diff --git a/internal/pkg/custom/processing.go b/internal/pkg/custom/processing.go
index 58a5e6c2e..6e987f41c 100644
--- a/internal/pkg/custom/processing.go
+++ b/internal/pkg/custom/processing.go
@@ -3,6 +3,10 @@ package custom
 import (
 	"fmt"
 	"regexp"
+	"strings"
+
+	"github.com/zclconf/go-cty/cty"
+	"github.com/zclconf/go-cty/cty/gocty"
 
 	"github.com/aquasecurity/defsec/provider"
 	"github.com/aquasecurity/defsec/rules"
@@ -13,33 +17,33 @@ import (
 )
 
 var matchFunctions = map[CheckAction]func(*block.Block, *MatchSpec, *customContext) bool{
-	IsPresent: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		return block.HasChild(spec.Name) || spec.IgnoreUndefined
+	IsPresent: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		return b.HasChild(spec.Name) || spec.IgnoreUndefined
 	},
-	NotPresent: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		return !block.HasChild(spec.Name)
+	NotPresent: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		return !b.HasChild(spec.Name)
 	},
-	IsEmpty: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		if block.MissingChild(spec.Name) {
+	IsEmpty: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		if b.MissingChild(spec.Name) {
 			return true
 		}
 
-		attribute := block.GetAttribute(spec.Name)
+		attribute := b.GetAttribute(spec.Name)
 		if attribute != nil {
 			return attribute.IsEmpty()
 		}
-		childBlock := block.GetBlock(spec.Name)
+		childBlock := b.GetBlock(spec.Name)
 		return childBlock.IsEmpty()
 	},
-	StartsWith: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	StartsWith: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
 		return attribute.StartsWith(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	EndsWith: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	EndsWith: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
@@ -52,75 +56,172 @@ var matchFunctions = map[CheckAction]func(*block.Block, *MatchSpec, *customConte
 		}
 		return attribute.Contains(processMatchValueVariables(spec.MatchValue, customCtx.variables), block.IgnoreCase)
 	},
-	NotContains: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	NotContains: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
 		return !attribute.Contains(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	Equals: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	Equals: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
 		return attribute.Equals(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	NotEqual: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	NotEqual: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
 		return attribute.NotEqual(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	LessThan: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	LessThan: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.LessThan(processMatchValueVariables(spec.MatchValue, customCtx.variables))
+		return attribute.LessThan(spec.MatchValue)
 	},
-	LessThanOrEqualTo: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	LessThanOrEqualTo: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.LessThanOrEqualTo(processMatchValueVariables(spec.MatchValue, customCtx.variables))
+		return attribute.LessThanOrEqualTo(spec.MatchValue)
 	},
-	GreaterThan: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	GreaterThan: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.GreaterThan(processMatchValueVariables(spec.MatchValue, customCtx.variables))
+		return attribute.GreaterThan(spec.MatchValue)
 	},
-	GreaterThanOrEqualTo: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	GreaterThanOrEqualTo: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
-		return attribute.GreaterThanOrEqualTo(processMatchValueVariables(spec.MatchValue, customCtx.variables))
+		return attribute.GreaterThanOrEqualTo(spec.MatchValue)
 	},
-	RegexMatches: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	RegexMatches: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
 		return attribute.RegexMatches(processMatchValueVariables(spec.MatchValue, customCtx.variables))
 	},
-	IsAny: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	RequiresPresence: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		return resourceFound(spec, customCtx.module)
+	},
+	IsAny: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		return attribute != nil && attribute.IsAny(unpackInterfaceToInterfaceSlice(processMatchValueVariables(spec.MatchValue, customCtx.variables))...)
 	},
-	IsNone: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		attribute := block.GetAttribute(spec.Name)
+	IsNone: func(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
+		attribute := b.GetAttribute(spec.Name)
 		if attribute.IsNil() {
 			return spec.IgnoreUndefined
 		}
 		return attribute.IsNone(unpackInterfaceToInterfaceSlice(processMatchValueVariables(spec.MatchValue, customCtx.variables))...)
 	},
-	RequiresPresence: func(block *block.Block, spec *MatchSpec, customCtx *customContext) bool {
-		return resourceFound(spec, customCtx.module)
+}
+
+var AttrMatchFunctions = map[CheckAction]func(*block.Attribute, *MatchSpec, *customContext) bool{
+	IsPresent: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		return a.Contains(spec.Name) || spec.IgnoreUndefined
+	},
+	NotPresent: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		return !a.Contains(spec.Name)
+	},
+	StartsWith: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.String) {
+				return false
+			}
+			return strings.HasPrefix(attributeValue.AsString(), fmt.Sprintf("%v", processMatchValueVariables(spec.MatchValue, customCtx.variables)))
+		}
+		return spec.IgnoreUndefined
+	},
+	EndsWith: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); !attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.String) {
+				return false
+			}
+			return strings.HasSuffix(attributeValue.AsString(), fmt.Sprintf("%v", processMatchValueVariables(spec.MatchValue, customCtx.variables)))
+		}
+		return spec.IgnoreUndefined
+	},
+	Equals: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); !attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.String) {
+				return false
+			}
+			return attributeValue.AsString() == processMatchValueVariables(spec.MatchValue, customCtx.variables)
+		}
+		return spec.IgnoreUndefined
+	},
+	NotEqual: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); !attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.String) {
+				return false
+			}
+			return attributeValue.AsString() != processMatchValueVariables(spec.MatchValue, customCtx.variables)
+		}
+		return spec.IgnoreUndefined
+	},
+	LessThan: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); !attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.Number) {
+				return false
+			}
+			if matchValue, err := gocty.ToCtyValue(spec.MatchValue, cty.Number); err != nil {
+				return false
+			} else {
+				return attributeValue.LessThan(matchValue).True()
+			}
+		}
+		return spec.IgnoreUndefined
+	},
+	LessThanOrEqualTo: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); !attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.Number) {
+				return false
+			}
+			if matchValue, err := gocty.ToCtyValue(spec.MatchValue, cty.Number); err != nil {
+				return false
+			} else {
+				return attributeValue.LessThanOrEqualTo(matchValue).True()
+			}
+		}
+		return spec.IgnoreUndefined
+	},
+	GreaterThan: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); !attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.Number) {
+				return false
+			}
+			if matchValue, err := gocty.ToCtyValue(spec.MatchValue, cty.Number); err != nil {
+				return false
+			} else {
+				return attributeValue.GreaterThan(matchValue).True()
+			}
+		}
+		return spec.IgnoreUndefined
+	},
+	GreaterThanOrEqualTo: func(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+		if attributeValue := a.MapValue(spec.Name); !attributeValue.IsNull() {
+			if !attributeValue.Type().Equals(cty.Number) {
+				return false
+			}
+			if matchValue, err := gocty.ToCtyValue(spec.MatchValue, cty.Number); err != nil {
+				return false
+			} else {
+				return attributeValue.GreaterThanOrEqualTo(matchValue).True()
+			}
+		}
+		return spec.IgnoreUndefined
 	},
 }
 
@@ -177,11 +278,6 @@ func evalMatchSpec(b *block.Block, spec *MatchSpec, customCtx *customContext) bo
 	switch spec.Action {
 	case InModule:
 		return b.InModule()
-	case RegexMatches:
-		if !matchFunctions[RegexMatches](b, spec, customCtx) {
-			return spec.IgnoreUnmatched
-		}
-		evalResult = true
 	case HasTag:
 		return checkTags(b, spec, customCtx.module)
 	case OfType:
@@ -189,9 +285,9 @@ func evalMatchSpec(b *block.Block, spec *MatchSpec, customCtx *customContext) bo
 	case Not:
 		return notifyPredicate(b, spec, customCtx)
 	case And:
-		return processAndPredicate(spec, b, customCtx)
+		return processAndPredicate(b, spec, customCtx)
 	case Or:
-		return processOrPredicate(spec, b, customCtx)
+		return processOrPredicate(b, spec, customCtx)
 	default:
 		evalResult = matchFunctions[spec.Action](b, spec, customCtx)
 	}
@@ -200,18 +296,47 @@ func evalMatchSpec(b *block.Block, spec *MatchSpec, customCtx *customContext) bo
 		customCtx.variables[spec.AssignVariable] = b.GetAttribute(spec.Name).AsStringValueOrDefault("", b).Value()
 	}
 
-	if spec.SubMatch != nil {
-		evalResult = processSubMatches(spec, b, customCtx, evalResult)
+	if spec.SubMatch != nil && evalResult {
+		evalResult = processSubMatches(b, spec, customCtx, evalResult)
 	}
 
 	return evalResult
 }
 
+func evalMatchSpecAttr(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+	for _, preCondition := range spec.PreConditions {
+		clone := preCondition
+		if !evalMatchSpecAttr(a, &clone, customCtx) {
+			// precondition not met
+			return true
+		}
+	}
+
+	switch spec.Action {
+	case Not:
+		return notifyPredicateAttr(a, spec, customCtx)
+	case And:
+		return processAndPredicateAttr(a, spec, customCtx)
+	case Or:
+		return processOrPredicateAttr(a, spec, customCtx)
+	default:
+		if matchFunction, ok := AttrMatchFunctions[spec.Action]; ok {
+			return matchFunction(a, spec, customCtx)
+		} else {
+			return false
+		}
+	}
+}
+
 func notifyPredicate(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
 	return !evalMatchSpec(b, &spec.PredicateMatchSpec[0], customCtx)
 }
 
-func processOrPredicate(spec *MatchSpec, b *block.Block, customCtx *customContext) bool {
+func notifyPredicateAttr(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+	return !evalMatchSpecAttr(a, &spec.PredicateMatchSpec[0], customCtx)
+}
+
+func processOrPredicate(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
 	for _, childSpec := range spec.PredicateMatchSpec {
 		clone := childSpec
 		if evalMatchSpec(b, &clone, customCtx) {
@@ -221,35 +346,60 @@ func processOrPredicate(spec *MatchSpec, b *block.Block, customCtx *customContex
 	return false
 }
 
-func processAndPredicate(spec *MatchSpec, b *block.Block, customCtx *customContext) bool {
+func processOrPredicateAttr(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+	for _, childSpec := range spec.PredicateMatchSpec {
+		clone := childSpec
+		if evalMatchSpecAttr(a, &clone, customCtx) {
+			return true
+		}
+	}
+	return false
+}
+
+func processAndPredicate(b *block.Block, spec *MatchSpec, customCtx *customContext) bool {
 	set := make(map[bool]bool)
 
 	for _, childSpec := range spec.PredicateMatchSpec {
 		clone := childSpec
 		result := evalMatchSpec(b, &clone, customCtx)
 		set[result] = true
+	}
+
+	return len(set) == 1 && set[true]
+}
 
+func processAndPredicateAttr(a *block.Attribute, spec *MatchSpec, customCtx *customContext) bool {
+	set := make(map[bool]bool)
+
+	for _, childSpec := range spec.PredicateMatchSpec {
+		clone := childSpec
+		result := evalMatchSpecAttr(a, &clone, customCtx)
+		set[result] = true
 	}
 
 	return len(set) == 1 && set[true]
 }
 
-func processSubMatches(spec *MatchSpec, b *block.Block, customCtx *customContext, evalResult bool) bool {
-	var subMatchTargets block.Blocks
+func processSubMatches(b *block.Block, spec *MatchSpec, customCtx *customContext, evalResult bool) bool {
+	var subMatchTargetBlocks block.Blocks
 	switch spec.Action {
 	case RequiresPresence:
-		subMatchTargets = customCtx.module.GetResourcesByType(spec.Name)
+		subMatchTargetBlocks = customCtx.module.GetResourcesByType(spec.Name)
 	default:
-		subMatchTargets = b.GetBlocks(spec.Name)
+		subMatchTargetBlocks = b.GetBlocks(spec.Name)
+		if targetAttribute := b.GetAttribute(spec.Name); targetAttribute.IsNotNil() {
+			if !evalMatchSpecAttr(targetAttribute, spec.SubMatch, customCtx) {
+				return false
+			}
+		}
 	}
-	for _, b := range subMatchTargets {
-		evalResult = evalMatchSpec(b, spec.SubMatch, customCtx)
-		if !evalResult {
-			break
+	for _, b := range subMatchTargetBlocks {
+		if !evalMatchSpec(b, spec.SubMatch, customCtx) {
+			return false
 		}
 	}
 
-	return evalResult
+	return true
 }
 
 func processMatchValueVariables(matchValue interface{}, variables map[string]string) interface{} {


Test patch:
diff --git a/internal/pkg/custom/processing_test.go b/internal/pkg/custom/processing_test.go
index 155b954c5..ca3033266 100644
--- a/internal/pkg/custom/processing_test.go
+++ b/internal/pkg/custom/processing_test.go
@@ -534,6 +534,321 @@ resource "google_compute_instance" "default" {
 	}
 }
 
+func TestAttributeSubMatches(t *testing.T) {
+	var tests = []struct {
+		name      string
+		source    string
+		matchSpec MatchSpec
+		expected  bool
+	}{
+		{
+			name: "check that lack of subMatches should pass",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "tf-example"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+			},
+			expected: true,
+		},
+		{
+			name: "check that a true `isPresent` map attribute subMatch should pass",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "tf-example"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:   "Name",
+					Action: "isPresent",
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "check that a true `equals` map attribute subMatch should pass",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "tf-example"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:       "Name",
+					Action:     "equals",
+					MatchValue: "tf-example",
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "check that a false map attribute subMatch should fail",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "tf-example"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:   "WrongName",
+					Action: "isPresent",
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "check that a not supported attribute subMatch should fail",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "tf-example"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:   "Name",
+					Action: "emptyAction",
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "check that a truey numeric comparison in attribute subMatch should pass",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    InvalidButForTestingTag = 30
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:       "InvalidButForTestingTag",
+					Action:     "lessThan",
+					MatchValue: 10000,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "check that a fasley numeric comparison in attribute subMatch should fail",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    InvalidButForTestingTag = 30
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:       "InvalidButForTestingTag",
+					Action:     "greaterThan",
+					MatchValue: 10000,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "check that a truey `not` action in attribute subMatch should pass",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "Creator"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Action: "not",
+					PredicateMatchSpec: []MatchSpec{
+						{
+							Name:       "Name",
+							Action:     "equals",
+							MatchValue: "Blah",
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "check that a falsey `not` action in attribute subMatch should fail",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "Creator"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Action: "not",
+					PredicateMatchSpec: []MatchSpec{
+						{
+							Name:       "Name",
+							Action:     "equals",
+							MatchValue: "Creator",
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "check that a truey `and` action in attribute subMatch should pass",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "Creator"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Action: "and",
+					PredicateMatchSpec: []MatchSpec{
+						{
+							Name:       "Name",
+							Action:     "equals",
+							MatchValue: "Creator",
+						},
+						{
+							Name:       "Name",
+							Action:     "notEqual",
+							MatchValue: "WrongValue",
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "check that a falsey `and` action in attribute subMatch should fail",
+			source: `
+resource "aws_instance" "foo" {
+  tags = {
+    Name = "Creator"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Action: "and",
+					PredicateMatchSpec: []MatchSpec{
+						{
+							Name:       "Name",
+							Action:     "equals",
+							MatchValue: "Creator",
+						},
+						{
+							Name:       "Name",
+							Action:     "equals",
+							MatchValue: "WrongValue",
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "check that a false attribute subMatch with non-matching preconditions should pass",
+			source: `
+resource "aws_instance" "foo" {
+  name = "just-a-name"
+  tags = {
+    Name = "tf-example"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:       "Name",
+					Action:     "equals",
+					MatchValue: "wrong-value",
+				},
+				PreConditions: []MatchSpec{
+					{
+						Name:       "name",
+						Action:     "equals",
+						MatchValue: "another-name",
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "check that a false attribute subMatch with matching preconditions should fail",
+			source: `
+resource "aws_instance" "foo" {
+  name = "just-a-name"
+  tags = {
+    Name = "tf-example"
+  }
+}
+`,
+			matchSpec: MatchSpec{
+				Name:   "tags",
+				Action: "isPresent",
+				SubMatch: &MatchSpec{
+					Name:       "Name",
+					Action:     "equals",
+					MatchValue: "wrong-value",
+				},
+				PreConditions: []MatchSpec{
+					{
+						Name:       "name",
+						Action:     "equals",
+						MatchValue: "just-a-name",
+					},
+				},
+			},
+			expected: false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			block := ParseFromSource(test.source)[0].GetBlocks()[0]
+			result := evalMatchSpec(block, &test.matchSpec, NewEmptyCustomContext())
+			assert.Equal(t, result, test.expected, "subMatch evaluation function for attributes behaving incorrectly.")
+		})
+	}
+}
+
 func givenCheck(jsonContent string) {
 	var checksfile ChecksFile
 	err := json.NewDecoder(strings.NewReader(jsonContent)).Decode(&checksfile)