Repository: liamg/tfsec
Pull request #77 (2020-01-13 00:18:22)

add check for unencrypted cloudfront comms
**Background:** 
CloudFront distributions can serve content over HTTP or HTTPS. The protocol to use is defined in the `viewer_protocol_policy` argument  found in `default_cache_behavior` and in the optional `ordered_cache_behavior` arguments.

This alerts on:
1. `default_cache_behavior` not being defined
2. `viewer_protocol_policy` within `default_cache_behavior` not being defined
3. `viewer_protocol_policy` set to `allow-all` which allows HTTP and HTTPS communications

If `ordered_cache_behavior` arguments are found then it alerts on:
1. `viewer_protocol_policy` not being defined in any `ordered_cache_behavior`'s
2. `viewer_protocol_policy` set to `allow-all` in any `ordered_cache_behavior`'s

**Note**: 
[`viewer_protocol_policy`](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#viewer_protocol_policy) can have `allow-all`, `https-only`, or `redirect-to-https`. We're only alerting on `allow-all`

Implements issue #45 


Patch:
diff --git a/README.md b/README.md
index 9d06065fe..94dc4b6f3 100644
--- a/README.md
+++ b/README.md
@@ -128,6 +128,7 @@ there are also checks which are provider agnostic.
 | AWS016  | aws      | Unencrypted SNS topic.
 | AWS017  | aws      | Unencrypted S3 bucket.
 | AWS018  | aws      | Missing description for security group/security group rule.
+| AWS020  | aws      | CloudFront distribution allows unencrypted (HTTP) communications.
 | 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_cloudfront_communications.go b/internal/app/tfsec/checks/aws_unencrypted_cloudfront_communications.go
new file mode 100644
index 000000000..ce525a120
--- /dev/null
+++ b/internal/app/tfsec/checks/aws_unencrypted_cloudfront_communications.go
@@ -0,0 +1,83 @@
+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"
+)
+
+// AWSUnencryptedCloudFrontCommunications See https://github.com/liamg/tfsec#included-checks for check info
+const AWSUnencryptedCloudFrontCommunications scanner.RuleID = "AWS020"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           AWSUnencryptedCloudFrontCommunications,
+		RequiredTypes:  []string{"resource"},
+		RequiredLabels: []string{"aws_cloudfront_distribution"},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, context *scanner.Context) []scanner.Result {
+
+			var results []scanner.Result
+
+			defaultBehaviorBlock := block.GetBlock("default_cache_behavior")
+			if defaultBehaviorBlock == nil {
+				results = append(results,
+					check.NewResult(
+						fmt.Sprintf("Resource '%s' defines a CloudFront distribution that allows unencrypted communications (missing default_cache_behavior block).", block.Name()),
+						block.Range(),
+						scanner.SeverityError,
+					),
+				)
+			} else if defaultBehaviorBlock != nil {
+				protocolPolicy := defaultBehaviorBlock.GetAttribute("viewer_protocol_policy")
+				if protocolPolicy == nil {
+					results = append(results,
+						check.NewResult(
+							fmt.Sprintf("Resource '%s' defines a CloudFront distribution that allows unencrypted communications (missing viewer_protocol_policy block).", block.Name()),
+							block.Range(),
+							scanner.SeverityError,
+						),
+					)
+				} else if protocolPolicy.Type() == cty.String && protocolPolicy.Value().AsString() == "allow-all" {
+					results = append(results,
+						check.NewResultWithValueAnnotation(
+							fmt.Sprintf("Resource '%s' defines a CloudFront distribution that allows unencrypted communications.", block.Name()),
+							protocolPolicy.Range(),
+							protocolPolicy,
+							scanner.SeverityError,
+						),
+					)
+				}
+			}
+
+			orderedBehaviorBlocks := block.GetBlocks("ordered_cache_behavior")
+			for _, orderedBehaviorBlock := range orderedBehaviorBlocks {
+				orderedProtocolPolicy := orderedBehaviorBlock.GetAttribute("viewer_protocol_policy")
+				if orderedProtocolPolicy == nil {
+					results = append(results,
+						check.NewResult(
+							fmt.Sprintf("Resource '%s' defines a CloudFront distribution that allows unencrypted communications (missing viewer_protocol_policy block).", block.Name()),
+							block.Range(),
+							scanner.SeverityError,
+						),
+					)
+				} else if orderedProtocolPolicy != nil && orderedProtocolPolicy.Type() == cty.String && orderedProtocolPolicy.Value().AsString() == "allow-all" {
+					results = append(results,
+						check.NewResultWithValueAnnotation(
+							fmt.Sprintf("Resource '%s' defines a CloudFront distribution that allows unencrypted communications.", block.Name()),
+							orderedProtocolPolicy.Range(),
+							orderedProtocolPolicy,
+							scanner.SeverityError,
+						),
+					)
+				}
+			}
+
+			return results
+
+		},
+	})
+}


Test patch:
diff --git a/internal/app/tfsec/aws_unencrypted_cloudfront_communications_test.go b/internal/app/tfsec/aws_unencrypted_cloudfront_communications_test.go
new file mode 100644
index 000000000..9c31e87c9
--- /dev/null
+++ b/internal/app/tfsec/aws_unencrypted_cloudfront_communications_test.go
@@ -0,0 +1,84 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_AWSUnencryptedCloudFrontCommunications(t *testing.T) {
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check no default_cache_behavior in aws_cloudfront_distribution",
+			source: `
+resource "aws_cloudfront_distribution" "s3_distribution" {
+
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedCloudFrontCommunications,
+		},
+		{
+			name: "check no default viewer_protocol_policy in default_cache_behavior",
+			source: `
+resource "aws_cloudfront_distribution" "s3_distribution" {
+	default_cache_behavior {
+
+	  }
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedCloudFrontCommunications,
+		},
+		{
+			name: "check viewer_protocol_policy include allows-all",
+			source: `
+resource "aws_cloudfront_distribution" "s3_distribution" {
+	default_cache_behavior {
+	    viewer_protocol_policy = "allow-all"
+	  }
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedCloudFrontCommunications,
+		},
+		{
+			name: "check no viewer_protocol_policy in ordered_cache_behavior",
+			source: `
+resource "aws_cloudfront_distribution" "s3_distribution" {
+	default_cache_behavior {
+		viewer_protocol_policy = "https-only"
+	}
+	
+	# Cache behavior with precedence 0
+	ordered_cache_behavior {
+
+	}
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedCloudFrontCommunications,
+		},
+		{
+			name: "check for allow-all in viewer_protocol_policy in orderd_cache_behavior ",
+			source: `
+resource "aws_cloudfront_distribution" "s3_distribution" {
+	default_cache_behavior {
+		viewer_protocol_policy = "https-only"
+	}
+	
+	# Cache behavior with precedence 0
+	ordered_cache_behavior {
+		viewer_protocol_policy = "allow-all"
+	}
+}`,
+			mustIncludeResultCode: checks.AWSUnencryptedCloudFrontCommunications,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+}