Repository: aquasecurity/tfsec (go)

Issue:
Ignore checks with variable context
**Is your feature request related to a problem? Please describe.**
Hey! I have complex TF code with dynamic resources based on variables. Trying to use tfsec and it works great! Thank you!

But I want to skip some checks for part of variable and haven't seen possibility to do this.

example:
```
locals {
  rules = {
    http = 80
    https = 443
  }
}

resource "aws_security_group_rule" "this" {
      for_each = local.rules
      type = "ingress"
      description     = "test"
      from_port       = each.value
      to_port         = each.value
      protocol        = "tcp"
      cidr_blocks     = ["0.0.0.0/0"]

}
```

the aim is ignore `aws-vpc-no-public-ingress-sgr` for port 443, but still have an error for port 80

**Describe the solution you'd like**
Ignore checks for parts of variables

```
locals {
  rules = {
    http = 80
    #tfsec:ignore:aws-vpc-no-public-ingress-sgr
    https = 443
  }
}
```

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Some kind related to https://github.com/aquasecurity/tfsec/issues/493


Pull request:
Add support for allowed values in ignores
- ignore must be on the resource, inline not supported
- add tests

Resolves #1119


Patch:
diff --git a/pkg/result/ignores.go b/pkg/result/ignores.go
index 415fce797..803b8894a 100644
--- a/pkg/result/ignores.go
+++ b/pkg/result/ignores.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
+	"github.com/zclconf/go-cty/cty"
 )
 
 func (res *Result) IsIgnored(workspace string) bool {
@@ -20,14 +21,51 @@ func (res *Result) IsIgnored(workspace string) bool {
 		if annotation.Expiry != nil && time.Now().After(*annotation.Expiry) {
 			continue
 		}
+		if len(annotation.AllowedValues) > 0 {
+			if checkAllowedValueIgnores(annotation, res) {
+				return true
+			}
+		} else {
+			// ignore rule matches!
+			return true
+		}
 
-		// ignore rule matches!
-		return true
 	}
 	// no ignore rule found for this result
 	return false
 }
 
+func checkAllowedValueIgnores(annotation Annotation, res *Result) bool {
+	for _, allowedValue := range annotation.AllowedValues {
+		parts := strings.Split(allowedValue, "=")
+		if len(parts) != 2 {
+			continue
+		}
+		attribute := parts[0]
+		value := parts[1]
+
+		for _, b := range res.Blocks() {
+			if b.HasChild(attribute) {
+				attr := b.GetAttribute(attribute)
+				switch attr.Type() {
+				case cty.Number:
+					val64, _ := attr.Value().AsBigFloat().Float64()
+					attrValueAsString := fmt.Sprintf("%d", int(val64))
+					if attrValueAsString == value {
+						return true
+					}
+				case cty.String:
+					if attr.Value().AsString() == value {
+						return true
+					}
+				}
+
+			}
+		}
+	}
+	return false
+}
+
 func (res *Result) Annotations() []Annotation {
 	var annotations []Annotation
 	for _, block := range res.Blocks() {
@@ -75,9 +113,10 @@ func traverseModuleTreeForAnnotations(b block.Block) (annotations []Annotation)
 }
 
 type Annotation struct {
-	IgnoreRuleID string
-	Expiry       *time.Time
-	Workspace    string
+	IgnoreRuleID  string
+	Expiry        *time.Time
+	Workspace     string
+	AllowedValues []string
 }
 
 func findAnnotations(input string) []Annotation {
@@ -118,7 +157,7 @@ func newAnnotation(input string) (Annotation, error) {
 		val := segments[i+1]
 		switch key {
 		case "ignore":
-			annotation.IgnoreRuleID = val
+			extractAllowedValues(&annotation, val)
 		case "exp":
 			parsed, err := time.Parse("2006-01-02", val)
 			if err != nil {
@@ -132,3 +171,12 @@ func newAnnotation(input string) (Annotation, error) {
 
 	return annotation, nil
 }
+
+func extractAllowedValues(annotation *Annotation, val string) {
+	if !strings.HasSuffix(val, "]") && !strings.Contains(val, "[") {
+		annotation.IgnoreRuleID = val
+		return
+	}
+	annotation.IgnoreRuleID = val[:strings.Index(val, "[")]
+	annotation.AllowedValues = strings.Split(strings.Split(strings.TrimSuffix(val, "]"), "[")[1], ",")
+}


Test patch:
diff --git a/internal/app/tfsec/test/ignore_test.go b/internal/app/tfsec/test/ignore_test.go
index 756e4f11b..861a3673a 100644
--- a/internal/app/tfsec/test/ignore_test.go
+++ b/internal/app/tfsec/test/ignore_test.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/parser"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
 
 	"github.com/aquasecurity/tfsec/pkg/provider"
@@ -332,3 +333,169 @@ func TestInlineIgnoresForAllRules(t *testing.T) {
 		}
 	}
 }
+
+func Test_AllowedValue_Ignores(t *testing.T) {
+	example := `
+locals {
+  rules = {
+    http = 80
+    https = 443 
+  }
+}
+
+#tfsec:ignore:aws-vpc-no-public-ingress-sgr[from_port=443]
+resource "aws_security_group_rule" "this" {
+      for_each = local.rules
+      type = "ingress"
+      description     = "test"
+      from_port       = each.value 
+      to_port         = each.value
+      protocol        = "tcp"
+      cidr_blocks     = ["0.0.0.0/0"]
+
+}
+`
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", example))
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New().Scan(blocks)
+	assert.Len(t, results, 1)
+	testutil.AssertCheckCode(t, "aws-vpc-no-public-ingress-sgr", "", results)
+}
+
+func Test_AllowedValue_Ignores_In_Multiple(t *testing.T) {
+	example := `
+locals {
+  rules = {
+    http = 80
+    https = 443 
+	https2 = 443
+  }
+}
+
+#tfsec:ignore:aws-vpc-no-public-ingress-sgr[from_port=443]
+resource "aws_security_group_rule" "this" {
+      for_each = local.rules
+      type = "ingress"
+      description     = "test"
+      from_port       = each.value 
+      to_port         = each.value
+      protocol        = "tcp"
+      cidr_blocks     = ["0.0.0.0/0"]
+
+}
+`
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", example))
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New().Scan(blocks)
+	assert.Len(t, results, 1)
+	testutil.AssertCheckCode(t, "aws-vpc-no-public-ingress-sgr", "", results)
+}
+
+func Test_AllowedValue_Ignores_Complex(t *testing.T) {
+	example := `
+locals {
+  rules = {
+    http = 80
+    https = 443 
+  }
+}
+
+#tfsec:ignore:aws-vpc-no-public-ingress-sgr[from_port=443,to_port=80]
+resource "aws_security_group_rule" "this" {
+      for_each = local.rules
+      type = "ingress"
+      description     = "test"
+      from_port       = each.value 
+      to_port         = each.value
+      protocol        = "tcp"
+      cidr_blocks     = ["0.0.0.0/0"]
+
+}
+`
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", example))
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New().Scan(blocks)
+	assert.Len(t, results, 0)
+	testutil.AssertCheckCode(t, "", "aws-vpc-no-public-ingress-sgr", results)
+}
+
+func Test_AllowedValue_Ignores_With_Junk(t *testing.T) {
+	example := `
+locals {
+  rules = {
+    http = 80
+    https = 443 
+  }
+}
+
+#tfsec:ignore:aws-vpc-no-public-ingress-sgr[from_port=abc]
+resource "aws_security_group_rule" "this" {
+      for_each = local.rules
+      type = "ingress"
+      description     = "test"
+      from_port       = each.value 
+      to_port         = each.value
+      protocol        = "tcp"
+      cidr_blocks     = ["0.0.0.0/0"]
+
+}
+`
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", example))
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New().Scan(blocks)
+	assert.Len(t, results, 2)
+	testutil.AssertCheckCode(t, "aws-vpc-no-public-ingress-sgr", "", results)
+}
+
+func Test_AllowedValue_Ignores_With_More_Junk(t *testing.T) {
+	example := `
+locals {
+  rules = {
+    http = 80
+    https = 443 
+  }
+}
+
+#tfsec:ignore:aws-vpc-no-public-ingress-sgr[from_portabc]
+resource "aws_security_group_rule" "this" {
+      for_each = local.rules
+      type = "ingress"
+      description     = "test"
+      from_port       = each.value 
+      to_port         = each.value
+      protocol        = "tcp"
+      cidr_blocks     = ["0.0.0.0/0"]
+
+}
+`
+	fs, err := testutil.NewFilesystem()
+	require.NoError(t, err)
+	defer fs.Close()
+
+	require.NoError(t, fs.WriteTextFile("project/main.tf", example))
+	blocks, err := parser.New(fs.RealPath("project/"), parser.OptionStopOnHCLError()).ParseDirectory()
+	require.NoError(t, err)
+	results := scanner.New().Scan(blocks)
+	assert.Len(t, results, 2)
+	testutil.AssertCheckCode(t, "aws-vpc-no-public-ingress-sgr", "", results)
+}