Repository: tfsec/tfsec
Pull request #580 (2021-02-08 12:06:22)

Allows for multiple submatches that are logically ANDed

owenrumney: Hey @felipesere  - does this mean you're actively using the custom checks? I think this is a great addition :+1:  

Patch:
diff --git a/docs-website/docs/config/custom_checks.md b/docs-website/docs/config/custom_checks.md
index 14c47d93f..19fda5296 100644
--- a/docs-website/docs/config/custom_checks.md
+++ b/docs-website/docs/config/custom_checks.md
@@ -98,6 +98,7 @@ The `MatchSpec` is the what will define the check itself - this is fairly basic
 | value | In cases where a value is required, the value to look for |
 | 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 |
+|subMatches | An array of sub MatchSpec block for nested checking. All sub MatchSpecs must match. |
 
 #### Check Actions
 There are a number of `CheckActions` available which should allow you to quickly put together most checks.
diff --git a/internal/app/tfsec/custom/custom_check.go b/internal/app/tfsec/custom/custom_check.go
index 506865e06..12c5681ce 100644
--- a/internal/app/tfsec/custom/custom_check.go
+++ b/internal/app/tfsec/custom/custom_check.go
@@ -84,6 +84,7 @@ type MatchSpec struct {
 	MatchValue      interface{} `json:"value,omitempty" yaml:"value,omitempty"`
 	Action          CheckAction `json:"action,omitempty" yaml:"action,omitempty"`
 	SubMatch        *MatchSpec  `json:"subMatch,omitempty" yaml:"subMatch,omitempty"`
+	SubMatches      []MatchSpec `json:"subMatches,omitempty" yaml:"subMatches,omitempty"`
 	IgnoreUndefined bool        `json:"ignoreUndefined,omitempty" yaml:"ignoreUndefined,omitempty"`
 }
 
diff --git a/internal/app/tfsec/custom/processing.go b/internal/app/tfsec/custom/processing.go
index dd7aa2905..0f28d3fda 100644
--- a/internal/app/tfsec/custom/processing.go
+++ b/internal/app/tfsec/custom/processing.go
@@ -159,6 +159,17 @@ func evalMatchSpec(block *parser.Block, spec *MatchSpec, ctx *scanner.Context) b
 		}
 		evalResult = evalMatchSpec(block, spec.SubMatch, nil)
 	}
+	if len(spec.SubMatches) > 0 {
+		if block.HasBlock(spec.Name) {
+			block = block.GetBlock(spec.Name)
+		}
+		for i := range spec.SubMatches {
+			evalResult = evalMatchSpec(block, &spec.SubMatches[i], nil)
+			if !evalResult {
+				break
+			}
+		}
+	}
 	return evalResult
 }
 


Test patch:
diff --git a/internal/app/tfsec/custom/multiple_submatches_test.go b/internal/app/tfsec/custom/multiple_submatches_test.go
new file mode 100644
index 000000000..53bcf8a10
--- /dev/null
+++ b/internal/app/tfsec/custom/multiple_submatches_test.go
@@ -0,0 +1,80 @@
+package custom
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func init() {
+	givenCheck(`{
+  "checks": [
+    {
+      "code": "CUSTOM",
+      "description": "Instance Metadata Service V2 is required",
+      "requiredTypes": [
+        "resource"
+      ],
+      "requiredLabels": [
+        "aws_instance"
+      ],
+      "severity": "ERROR",
+      "matchSpec": {
+        "name": "metadata_options",
+        "action": "isPresent",
+        "subMatches": [
+          {
+            "name": "http_endpoint",
+            "action": "equals",
+            "value": "enabled"
+          },
+          {
+            "name": "http_put_response_hop_limit",
+            "action": "equals",
+            "value": 1
+          },
+          {
+            "name": "http_tokens",
+            "action": "equals",
+            "value": "required"
+          }
+        ]
+      }
+    }
+  ]
+}
+    `)
+}
+
+func TestInstanceMetadataEndpointPresent(t *testing.T) {
+	scanResults := scanTerraform(t, `
+resource "aws_instance" "bastion" {
+  metadata_options {
+    http_endpoint               = "enabled"
+    http_put_response_hop_limit = 1
+    http_tokens                 = "required"
+  }
+}
+`)
+	assert.Len(t, scanResults, 0)
+}
+
+func TestInstanceMetadataEndpointMissing(t *testing.T) {
+	scanResults := scanTerraform(t, `
+resource "aws_instance" "bastion" {
+}
+`)
+	assert.Len(t, scanResults, 1)
+}
+
+func TestOneSubmatchHasWrongValue(t *testing.T) {
+	scanResults := scanTerraform(t, `
+resource "aws_instance" "bastion" {
+  metadata_options {
+    http_endpoint               = "enabled"
+    http_put_response_hop_limit = 1
+    http_tokens                 = "definitely-wrong-not-required"
+  }
+}
+`)
+	assert.Len(t, scanResults, 1)
+}