Repository: liamg/tfsec
Pull request #158 (2020-09-07 14:21:46)

feat: add checks for encryption at rest and in transit for Elasticache
This PR adds checks for ensuring that encryption at rest and encryption in transit are set for Elasticache Replication Groups.


Patch:
diff --git a/README.md b/README.md
index 0d7499ae6..61d0b086c 100644
--- a/README.md
+++ b/README.md
@@ -161,6 +161,8 @@ there are also checks which are provider agnostic.
 | AWS032  | aws      | Elasticsearch domain uses plaintext traffic for node to node communication.
 | AWS033  | aws      | Elasticsearch doesn't enforce HTTPS traffic.
 | AWS034  | aws      | Elasticsearch domain endpoint is using outdated TLS policy.
+| AWS035  | aws      | Unencrypted Elasticache Replication Group.
+| AWS036  | aws      | Elasticache Replication Group uses unencrypted traffic.
 | AZU001  | azurerm  | An inbound network security rule allows traffic from `/0`.
 | AZU002  | azurerm  | An outbound network security rule allows traffic to `/0`.
 | AZU003  | azurerm  | Unencrypted managed disk.
diff --git a/internal/app/tfsec/checks/aws_unencrypted_at_rest_elasticache_replication_group.go b/internal/app/tfsec/checks/aws_unencrypted_at_rest_elasticache_replication_group.go
new file mode 100644
index 000000000..27f377ca0
--- /dev/null
+++ b/internal/app/tfsec/checks/aws_unencrypted_at_rest_elasticache_replication_group.go
@@ -0,0 +1,44 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+)
+
+// AWSUnencryptedAtRestElasticacheReplicationGroup See https://github.com/liamg/tfsec#included-checks for check info
+const AWSUnencryptedAtRestElasticacheReplicationGroup scanner.RuleID = "AWS035"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           AWSUnencryptedAtRestElasticacheReplicationGroup,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_elasticache_replication_group"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, context *scanner.Context) []scanner.Result {
+
+			encryptionAttr := block.GetAttribute("at_rest_encryption_enabled")
+			if encryptionAttr == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an unencrypted Elasticache Replication Group (missing at_rest_encryption_enabled attribute).", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			} else if !isBooleanOrStringTrue(encryptionAttr) {
+				return []scanner.Result{
+					check.NewResultWithValueAnnotation(
+						fmt.Sprintf("Resource '%s' defines an unencrypted Elasticache Replication Group (at_rest_encryption_enabled set to false).", block.Name()),
+						encryptionAttr.Range(),
+						encryptionAttr,
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/aws_unencrypted_in_transit_elasticache_replication_group.go b/internal/app/tfsec/checks/aws_unencrypted_in_transit_elasticache_replication_group.go
new file mode 100644
index 000000000..38669819d
--- /dev/null
+++ b/internal/app/tfsec/checks/aws_unencrypted_in_transit_elasticache_replication_group.go
@@ -0,0 +1,45 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+)
+
+// AWSUnencryptedInTransitElasticacheReplicationGroup See https://github.com/liamg/tfsec#included-checks for check info
+const AWSUnencryptedInTransitElasticacheReplicationGroup scanner.RuleID = "AWS036"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           AWSUnencryptedInTransitElasticacheReplicationGroup,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_elasticache_replication_group"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, context *scanner.Context) []scanner.Result {
+
+			encryptionAttr := block.GetAttribute("transit_encryption_enabled")
+			if encryptionAttr == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an unencrypted Elasticache Replication Group (missing transit_encryption_enabled attribute).", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			} else if !isBooleanOrStringTrue(encryptionAttr) {
+				return []scanner.Result{
+					check.NewResultWithValueAnnotation(
+						fmt.Sprintf("Resource '%s' defines an unencrypted Elasticache Replication Group (transit_encryption_enabled set to false).", block.Name()),
+						encryptionAttr.Range(),
+						encryptionAttr,
+						scanner.SeverityError,
+					),
+				}
+
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/utils.go b/internal/app/tfsec/checks/utils.go
new file mode 100644
index 000000000..a60199f55
--- /dev/null
+++ b/internal/app/tfsec/checks/utils.go
@@ -0,0 +1,19 @@
+package checks
+
+import (
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+	"github.com/zclconf/go-cty/cty"
+)
+
+// isBooleanOrStringTrue returns true if the attribute is a boolean and is
+// `true` or if the attribute is a string and is `"true"`.
+func isBooleanOrStringTrue(val *parser.Attribute) bool {
+	switch val.Type() {
+	case cty.Bool:
+		return val.Value().True()
+	case cty.String:
+		return val.Value().Equals(cty.StringVal("true")).True()
+	default:
+		return false
+	}
+}


Test patch:
diff --git a/internal/app/tfsec/aws_unencrypted_at_rest_elasticache_replication_group_test.go b/internal/app/tfsec/aws_unencrypted_at_rest_elasticache_replication_group_test.go
new file mode 100644
index 000000000..3b57cbba2
--- /dev/null
+++ b/internal/app/tfsec/aws_unencrypted_at_rest_elasticache_replication_group_test.go
@@ -0,0 +1,47 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_AWSUnencryptedAtRestElasticacheReplicationGroup(t *testing.T) {
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check aws_elasticache_replication_group missing at_rest_encryption_enabled",
+			source: `
+resource "aws_elasticache_replication_group" "my-resource" {
+        replication_group_id = "foo"
+        replication_group_description = "my foo cluster"
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedAtRestElasticacheReplicationGroup,
+		},
+		{
+			name: "check aws_elasticache_replication_group with at_rest_encryption_enabled",
+			source: `
+resource "aws_elasticache_replication_group" "my-resource" {
+        replication_group_id = "foo"
+        replication_group_description = "my foo cluster"
+
+        at_rest_encryption_enabled = true
+}`,
+			mustExcludeResultCode: checks.AWSUnencryptedAtRestElasticacheReplicationGroup,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}
diff --git a/internal/app/tfsec/aws_unencrypted_in_transit_elasticache_replication_group_test.go b/internal/app/tfsec/aws_unencrypted_in_transit_elasticache_replication_group_test.go
new file mode 100644
index 000000000..a1a72aaeb
--- /dev/null
+++ b/internal/app/tfsec/aws_unencrypted_in_transit_elasticache_replication_group_test.go
@@ -0,0 +1,47 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_AWSUnencryptedInTransitElasticacheReplicationGroup(t *testing.T) {
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check aws_elasticache_replication_group missing transit_encryption_enabled",
+			source: `
+resource "aws_elasticache_replication_group" "my-resource" {
+        replication_group_id = "foo"
+        replication_group_description = "my foo cluster"
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedInTransitElasticacheReplicationGroup,
+		},
+		{
+			name: "check aws_elasticache_replication_group with transit_encryption_enabled",
+			source: `
+resource "aws_elasticache_replication_group" "my-resource" {
+        replication_group_id = "foo"
+        replication_group_description = "my foo cluster"
+
+        transit_encryption_enabled = true
+}`,
+			mustExcludeResultCode: checks.AWSUnencryptedInTransitElasticacheReplicationGroup,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}
diff --git a/internal/app/tfsec/checks/utils_test.go b/internal/app/tfsec/checks/utils_test.go
new file mode 100644
index 000000000..be622b113
--- /dev/null
+++ b/internal/app/tfsec/checks/utils_test.go
@@ -0,0 +1,55 @@
+package checks
+
+import (
+	"testing"
+
+	hcl "github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+)
+
+func Test_isBooleanOrStringTrue(t *testing.T) {
+	var tests = []struct {
+		val     *parser.Attribute
+		rawExpr string
+		result  bool
+	}{
+		{
+			rawExpr: "false",
+			result:  false,
+		},
+		{
+			rawExpr: "true",
+			result:  true,
+		},
+		{
+			rawExpr: `"false"`,
+			result:  false,
+		},
+		{
+			rawExpr: `"true"`,
+			result:  true,
+		},
+		{
+			rawExpr: `"foo"`,
+			result:  false,
+		},
+		{
+			rawExpr: "5",
+			result:  false,
+		},
+	}
+
+	for _, test := range tests {
+		expr, _ := hclsyntax.ParseExpression([]byte(test.rawExpr), "", hcl.Pos{0, 0, 0})
+		attr := parser.NewAttribute(
+			&hclsyntax.Attribute{
+				Expr: expr,
+			},
+			nil,
+		)
+		if result := isBooleanOrStringTrue(attr); result != test.result {
+			t.Errorf("expected %v got %v\n", test.val, result)
+		}
+	}
+}