Repository: liamg/tfsec
Pull request #157 (2020-09-05 13:58:33)

feat: add check for encryption at rest for Elasticsearch domains
This PR adds a few new AWS checks for a few best practices on Elasticsearch domains. I'd love for these to be included, let me know your thoughts @liamg! Specifically, it checks for the these blocks:

```hcl
encrypt_at_rest {
    enabled = true
}
``` 

```hcl
node_to_node_encryption {
    enabled = true
}
```

```hcl
domain_endpoint_options {
    enforce_https = true
}
```

reedloden: Perhaps also check the TLS protocol version? Ensuring that at least TLS 1.2 is used. Not sure if there's a good way outside of checking to see if TLS 1.0 is selected (and warning on that)?

Patch:
diff --git a/README.md b/README.md
index ce76f0329..0d7499ae6 100644
--- a/README.md
+++ b/README.md
@@ -157,6 +157,10 @@ there are also checks which are provider agnostic.
 | AWS023  | aws      | ECR repository has image scans disabled
 | AWS024  | aws      | Kinesis stream is unencrypted
 | AWS025  | aws      | API Gateway domain name uses outdated SSL/TLS protocols.
+| AWS031  | aws      | Elasticsearch domain isn't encrypted at rest.
+| 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.
 | 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_oudated_tls_policy_elasticsearch_domain_endpoint.go b/internal/app/tfsec/checks/aws_oudated_tls_policy_elasticsearch_domain_endpoint.go
new file mode 100644
index 000000000..0ec97ff93
--- /dev/null
+++ b/internal/app/tfsec/checks/aws_oudated_tls_policy_elasticsearch_domain_endpoint.go
@@ -0,0 +1,56 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+	"github.com/zclconf/go-cty/cty"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+)
+
+// AWSOutdatedTLSPolicyElasticsearchDomainEndpoint See
+// https://github.com/liamg/tfsec#included-checks for check info
+const AWSOutdatedTLSPolicyElasticsearchDomainEndpoint scanner.RuleID = "AWS034"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           AWSOutdatedTLSPolicyElasticsearchDomainEndpoint,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_elasticsearch_domain"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, context *scanner.Context) []scanner.Result {
+
+			endpointBlock := block.GetBlock("domain_endpoint_options")
+			if endpointBlock == nil {
+				// Check AWS033 covers this case.
+				return nil
+			}
+
+			tlsPolicyAttr := endpointBlock.GetAttribute("tls_security_policy")
+			if tlsPolicyAttr == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with an outdated TLS policy (defaults to Policy-Min-TLS-1-0-2019-07).", block.Name()),
+						endpointBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+
+				return nil
+			}
+
+			if tlsPolicyAttr.Value().Equals(cty.StringVal("Policy-Min-TLS-1-0-2019-07")).True() {
+				return []scanner.Result{
+					check.NewResultWithValueAnnotation(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with an outdated TLS policy (set to Policy-Min-TLS-1-0-2019-07).", block.Name()),
+						tlsPolicyAttr.Range(),
+						tlsPolicyAttr,
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/aws_plaintext_node_to_node_elasticsearch_traffic.go b/internal/app/tfsec/checks/aws_plaintext_node_to_node_elasticsearch_traffic.go
new file mode 100644
index 000000000..a7154667a
--- /dev/null
+++ b/internal/app/tfsec/checks/aws_plaintext_node_to_node_elasticsearch_traffic.go
@@ -0,0 +1,61 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+	"github.com/zclconf/go-cty/cty"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+)
+
+// AWSPlaintextNodeToNodeElasticsearchTraffic See https://github.com/liamg/tfsec#included-checks for check info
+const AWSPlaintextNodeToNodeElasticsearchTraffic scanner.RuleID = "AWS032"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           AWSPlaintextNodeToNodeElasticsearchTraffic,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_elasticsearch_domain"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, context *scanner.Context) []scanner.Result {
+
+			encryptionBlock := block.GetBlock("node_to_node_encryption")
+			if encryptionBlock == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with plaintext traffic (missing node_to_node_encryption block).", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			enabledAttr := encryptionBlock.GetAttribute("enabled")
+			if enabledAttr == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with plaintext traffic (missing enabled attribute).", block.Name()),
+						encryptionBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			isTrueBool := enabledAttr.Type() == cty.Bool && enabledAttr.Value().True()
+			isTrueString := enabledAttr.Type() == cty.String &&
+				enabledAttr.Value().Equals(cty.StringVal("true")).True()
+			nodeToNodeEncryptionEnabled := isTrueBool || isTrueString
+			if !nodeToNodeEncryptionEnabled {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with plaintext traffic (enabled attribute set to false).", block.Name()),
+						encryptionBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/aws_unencrypted_elasticsearch_domain.go b/internal/app/tfsec/checks/aws_unencrypted_elasticsearch_domain.go
new file mode 100644
index 000000000..d134382e2
--- /dev/null
+++ b/internal/app/tfsec/checks/aws_unencrypted_elasticsearch_domain.go
@@ -0,0 +1,61 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+	"github.com/zclconf/go-cty/cty"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+)
+
+// AWSUnencryptedElasticsearchDomain See https://github.com/liamg/tfsec#included-checks for check info
+const AWSUnencryptedElasticsearchDomain scanner.RuleID = "AWS031"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           AWSUnencryptedElasticsearchDomain,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_elasticsearch_domain"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, context *scanner.Context) []scanner.Result {
+
+			encryptionBlock := block.GetBlock("encrypt_at_rest")
+			if encryptionBlock == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an unencrypted Elasticsearch domain (missing encrypt_at_rest block).", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			enabledAttr := encryptionBlock.GetAttribute("enabled")
+			if enabledAttr == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an unencrypted Elasticsearch domain (missing enabled attribute).", block.Name()),
+						encryptionBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			isTrueBool := enabledAttr.Type() == cty.Bool && enabledAttr.Value().True()
+			isTrueString := enabledAttr.Type() == cty.String &&
+				enabledAttr.Value().Equals(cty.StringVal("true")).True()
+			encryptionEnabled := isTrueBool || isTrueString
+			if !encryptionEnabled {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an unencrypted Elasticsearch domain (enabled attribute set to false).", block.Name()),
+						encryptionBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}
diff --git a/internal/app/tfsec/checks/aws_unenforced_https_elasticsearch_domain_endpoint.go b/internal/app/tfsec/checks/aws_unenforced_https_elasticsearch_domain_endpoint.go
new file mode 100644
index 000000000..1944fc812
--- /dev/null
+++ b/internal/app/tfsec/checks/aws_unenforced_https_elasticsearch_domain_endpoint.go
@@ -0,0 +1,62 @@
+package checks
+
+import (
+	"fmt"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+	"github.com/zclconf/go-cty/cty"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+)
+
+// AWSUnenforcedHTTPSElasticsearchDomainEndpoint See
+// https://github.com/liamg/tfsec#included-checks for check info
+const AWSUnenforcedHTTPSElasticsearchDomainEndpoint scanner.RuleID = "AWS033"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           AWSUnenforcedHTTPSElasticsearchDomainEndpoint,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_elasticsearch_domain"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, context *scanner.Context) []scanner.Result {
+
+			endpointBlock := block.GetBlock("domain_endpoint_options")
+			if endpointBlock == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with plaintext traffic (missing domain_endpoint_options block).", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			enforceHTTPSAttr := endpointBlock.GetAttribute("enforce_https")
+			if enforceHTTPSAttr == nil {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with plaintext traffic (missing enforce_https attribute).", block.Name()),
+						endpointBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			isTrueBool := enforceHTTPSAttr.Type() == cty.Bool && enforceHTTPSAttr.Value().True()
+			isTrueString := enforceHTTPSAttr.Type() == cty.String &&
+				enforceHTTPSAttr.Value().Equals(cty.StringVal("true")).True()
+			enforcedHTTPS := isTrueBool || isTrueString
+			if !enforcedHTTPS {
+				return []scanner.Result{
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines an Elasticsearch domain with plaintext traffic (enabled attribute set to false).", block.Name()),
+						endpointBlock.Range(),
+						scanner.SeverityError,
+					),
+				}
+			}
+
+			return nil
+		},
+	})
+}


Test patch:
diff --git a/internal/app/tfsec/aws_outdated_tls_policy_elasticsearch_domain_endpoint_test.go b/internal/app/tfsec/aws_outdated_tls_policy_elasticsearch_domain_endpoint_test.go
new file mode 100644
index 000000000..acdfdedfd
--- /dev/null
+++ b/internal/app/tfsec/aws_outdated_tls_policy_elasticsearch_domain_endpoint_test.go
@@ -0,0 +1,74 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_AWSOutdatedTLSPolicyElasticsearchDomainEndpoint(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check no domain_endpoint_options aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+	
+}`,
+			mustExcludeResultCode: checks.AWSOutdatedTLSPolicyElasticsearchDomainEndpoint,
+		},
+		{
+			name: "check tls_security_policy for aws_elasticsearch_domain isn't the default",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  domain_endpoint_options {
+    enforce_https = true
+  }
+}`,
+			mustIncludeResultCode: checks.AWSOutdatedTLSPolicyElasticsearchDomainEndpoint,
+		},
+		{
+			name: "check tls_security_policy isn't set to TLsv1.0 for aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  domain_endpoint_options {
+    enforce_https = true
+    tls_security_policy = "Policy-Min-TLS-1-0-2019-07"
+  }
+}`,
+			mustIncludeResultCode: checks.AWSOutdatedTLSPolicyElasticsearchDomainEndpoint,
+		},
+		{
+			name: "check tls_security_policy is set to TLSv1.2 for aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  domain_endpoint_options {
+    enforce_https = true
+    tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
+  }
+}`,
+			mustExcludeResultCode: checks.AWSOutdatedTLSPolicyElasticsearchDomainEndpoint,
+		},
+	}
+
+	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_plaintext_node_to_node_elasticsearch_traffic_test.go b/internal/app/tfsec/aws_plaintext_node_to_node_elasticsearch_traffic_test.go
new file mode 100644
index 000000000..d4a1ff4e9
--- /dev/null
+++ b/internal/app/tfsec/aws_plaintext_node_to_node_elasticsearch_traffic_test.go
@@ -0,0 +1,60 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_AWSPlaintextNodeToNodeElasticsearchTraffic(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check no node_to_node_encryption block aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+	
+}`,
+			mustIncludeResultCode: checks.AWSPlaintextNodeToNodeElasticsearchTraffic,
+		},
+		{
+			name: "check false enabled attr aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  node_to_node_encryption {
+    enabled = false
+  }
+}`,
+			mustIncludeResultCode: checks.AWSPlaintextNodeToNodeElasticsearchTraffic,
+		},
+		{
+			name: "check true enabled attr aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  node_to_node_encryption {
+    enabled = true
+  }
+}`,
+			mustExcludeResultCode: checks.AWSPlaintextNodeToNodeElasticsearchTraffic,
+		},
+	}
+
+	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_elasticearch_domain_test.go b/internal/app/tfsec/aws_unencrypted_elasticearch_domain_test.go
new file mode 100644
index 000000000..d5ba94aca
--- /dev/null
+++ b/internal/app/tfsec/aws_unencrypted_elasticearch_domain_test.go
@@ -0,0 +1,58 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func TestAWSUnencryptedElasticsearchDomain(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check no encrypt_at_rest block aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+	
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedElasticsearchDomain,
+		},
+		{
+			name: "check false enabled attr aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  encrypt_at_rest { }
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedElasticsearchDomain,
+		},
+		{
+			name: "check true enabled attr aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  encrypt_at_rest {
+    enabled = true
+  }
+}`,
+			mustExcludeResultCode: checks.AWSUnencryptedElasticsearchDomain,
+		},
+	}
+
+	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_unenforced_https_elasticsearch_domain_endpoint_test.go b/internal/app/tfsec/aws_unenforced_https_elasticsearch_domain_endpoint_test.go
new file mode 100644
index 000000000..f88fd05ed
--- /dev/null
+++ b/internal/app/tfsec/aws_unenforced_https_elasticsearch_domain_endpoint_test.go
@@ -0,0 +1,60 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_AWSUnenforcedHTTPSElasticsearchDomainEndpoint(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check no  domain_endpoint_options aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+	
+}`,
+			mustIncludeResultCode: checks.AWSUnenforcedHTTPSElasticsearchDomainEndpoint,
+		},
+		{
+			name: "check false enforce_https attr aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  domain_endpoint_options {
+    enforce_https = false
+  }
+}`,
+			mustIncludeResultCode: checks.AWSUnenforcedHTTPSElasticsearchDomainEndpoint,
+		},
+		{
+			name: "check true enforce_https aws_elasticsearch_domain",
+			source: `
+resource "aws_elasticsearch_domain" "my_elasticsearch_domain" {
+  domain_name = "domain-foo"
+
+  domain_endpoint_options {
+    enforce_https = true
+  }
+}`,
+			mustExcludeResultCode: checks.AWSUnenforcedHTTPSElasticsearchDomainEndpoint,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}