Repository: tfsec/tfsec
Pull request #835 (2021-07-08 13:47:57)

Add the initial checks for digital ocean
- add support for docs for Oracle and Digital Ocean



Patch:
diff --git a/cmd/tfsec-skeleton/generate.go b/cmd/tfsec-skeleton/generate.go
index d91357bb1..dc7cb70a3 100644
--- a/cmd/tfsec-skeleton/generate.go
+++ b/cmd/tfsec-skeleton/generate.go
@@ -11,7 +11,7 @@ import (
 	"github.com/liamg/tml"
 )
 
-var providers = map[string]string{"AWS": "aws", "Azure": "azu", "GCP": "gcp", "Oracle": "oci", "General": "gen"}
+var providers = map[string]string{"AWS": "aws", "Azure": "azu", "GCP": "gcp", "Oracle": "oci", "General": "gen", "DigitalOcean": "dig"}
 
 type checkSkeleton struct {
 	Provider         string
diff --git a/cmd/tfsec-skeleton/templates.go b/cmd/tfsec-skeleton/templates.go
index 9741f007c..9a13c7fe0 100644
--- a/cmd/tfsec-skeleton/templates.go
+++ b/cmd/tfsec-skeleton/templates.go
@@ -9,6 +9,7 @@ import (
 	"github.com/tfsec/tfsec/pkg/provider"
 	"github.com/tfsec/tfsec/pkg/result"
 	"github.com/tfsec/tfsec/pkg/rule"
+	"github.com/tfsec/tfsec/pkg/severity"
 )
 
 
@@ -44,7 +45,7 @@ func init() {
 		RequiredTypes:  []string{{.RequiredTypes}},
 		RequiredLabels: []string{{.RequiredLabels}},
 		DefaultSeverity: severity.Warning, //TODO set the default severity
-		CheckFunc: func(set result.Set, block block.Block, _ *hclcontext.Context){
+		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context){
 				
 			// function contents here
 
@@ -88,7 +89,7 @@ func Test_{{.CheckName}}(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			results := scanSource(test.source)
+			results := scanHCL(test.source, t)
 			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
 		})
 	}
diff --git a/internal/app/tfsec/rules/dig001.go b/internal/app/tfsec/rules/dig001.go
new file mode 100644
index 000000000..9a82a123b
--- /dev/null
+++ b/internal/app/tfsec/rules/dig001.go
@@ -0,0 +1,89 @@
+package rules
+
+import (
+	"fmt"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/block"
+	"github.com/tfsec/tfsec/internal/app/tfsec/hclcontext"
+	"github.com/tfsec/tfsec/internal/app/tfsec/scanner"
+	"github.com/tfsec/tfsec/pkg/provider"
+	"github.com/tfsec/tfsec/pkg/result"
+	"github.com/tfsec/tfsec/pkg/rule"
+	"github.com/tfsec/tfsec/pkg/severity"
+)
+
+const DIGFirewallHasOpenInboundAccess = "DIG001"
+const DIGFirewallHasOpenInboundAccessDescription = "The firewall has an inbound rule with open access"
+const DIGFirewallHasOpenInboundAccessImpact = "Your port is exposed to the internet"
+const DIGFirewallHasOpenInboundAccessResolution = "Set a more restrictive CIRDR range"
+const DIGFirewallHasOpenInboundAccessExplanation = `
+Opening up ports to connect out to the public internet is generally to be avoided. You should restrict access to IP addresses or ranges that are explicitly required where possible.
+`
+const DIGFirewallHasOpenInboundAccessBadExample = `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	inbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  source_addresses = ["0.0.0.0/0", "::/0"]
+	}
+}
+`
+const DIGFirewallHasOpenInboundAccessGoodExample = `
+resource "digitalocean_firewall" "good_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	inbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  source_addresses = ["192.168.1.0/24", "2002:1:2::/48"]
+	}
+}
+`
+
+func init() {
+	scanner.RegisterCheckRule(rule.Rule{
+		ID: DIGFirewallHasOpenInboundAccess,
+		Documentation: rule.RuleDocumentation{
+			Summary:     DIGFirewallHasOpenInboundAccessDescription,
+			Explanation: DIGFirewallHasOpenInboundAccessExplanation,
+			Impact:      DIGFirewallHasOpenInboundAccessImpact,
+			Resolution:  DIGFirewallHasOpenInboundAccessResolution,
+			BadExample:  DIGFirewallHasOpenInboundAccessBadExample,
+			GoodExample: DIGFirewallHasOpenInboundAccessGoodExample,
+			Links: []string{
+				"https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/firewall",
+				"https://docs.digitalocean.com/products/networking/firewalls/how-to/configure-rules/",
+			},
+		},
+		Provider:        provider.DigitalOceanProvider,
+		RequiredTypes:   []string{"resource"},
+		RequiredLabels:  []string{"digitalocean_firewall"},
+		DefaultSeverity: severity.Warning,
+		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
+
+			inboundBlocks := resourceBlock.GetBlocks("inbound_rule")
+
+			for _, inboundRuleBlock := range inboundBlocks {
+				if inboundRuleBlock.MissingChild("source_addresses") {
+					continue
+				}
+				sourceAddressesAttr := inboundRuleBlock.GetAttribute("source_addresses")
+				if isOpenCidr(sourceAddressesAttr) {
+					set.Add(
+						result.New(resourceBlock).
+							WithDescription(fmt.Sprintf("Resource '%s' defines a fully open inbound_rule.", resourceBlock.FullName())).
+							WithRange(sourceAddressesAttr.Range()).
+							WithAttributeAnnotation(sourceAddressesAttr).
+							WithSeverity(severity.Warning),
+					)
+				}
+			}
+		},
+	})
+}
diff --git a/internal/app/tfsec/rules/dig002.go b/internal/app/tfsec/rules/dig002.go
new file mode 100644
index 000000000..b063e829f
--- /dev/null
+++ b/internal/app/tfsec/rules/dig002.go
@@ -0,0 +1,88 @@
+package rules
+
+import (
+	"fmt"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/block"
+	"github.com/tfsec/tfsec/internal/app/tfsec/hclcontext"
+	"github.com/tfsec/tfsec/internal/app/tfsec/scanner"
+	"github.com/tfsec/tfsec/pkg/provider"
+	"github.com/tfsec/tfsec/pkg/result"
+	"github.com/tfsec/tfsec/pkg/rule"
+	"github.com/tfsec/tfsec/pkg/severity"
+)
+
+const DIGFirewallHasOpenOutboundAccess = "DIG002"
+const DIGFirewallHasOpenOutboundAccessDescription = "The firewall has an outbound rule with open access"
+const DIGFirewallHasOpenOutboundAccessImpact = "The port is exposed for ingress from the internet"
+const DIGFirewallHasOpenOutboundAccessResolution = "Set a more restrictive cidr range"
+const DIGFirewallHasOpenOutboundAccessExplanation = `
+Opening up ports to the public internet is generally to be avoided. You should restrict access to IP addresses or ranges that explicitly require it where possible.
+`
+const DIGFirewallHasOpenOutboundAccessBadExample = `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	outbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  destination_addresses = ["0.0.0.0/0", "::/0"]
+	}
+}
+`
+const DIGFirewallHasOpenOutboundAccessGoodExample = `
+resource "digitalocean_firewall" "good_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	outbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  destination_addresses = ["192.168.1.0/24", "2002:1:2::/48"]
+	}
+}
+`
+
+func init() {
+	scanner.RegisterCheckRule(rule.Rule{
+		ID: DIGFirewallHasOpenOutboundAccess,
+		Documentation: rule.RuleDocumentation{
+			Summary:     DIGFirewallHasOpenOutboundAccessDescription,
+			Explanation: DIGFirewallHasOpenOutboundAccessExplanation,
+			Impact:      DIGFirewallHasOpenOutboundAccessImpact,
+			Resolution:  DIGFirewallHasOpenOutboundAccessResolution,
+			BadExample:  DIGFirewallHasOpenOutboundAccessBadExample,
+			GoodExample: DIGFirewallHasOpenOutboundAccessGoodExample,
+			Links: []string{
+				"https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/firewall",
+				"https://docs.digitalocean.com/products/networking/firewalls/how-to/configure-rules/",
+			},
+		},
+		Provider:        provider.DigitalOceanProvider,
+		RequiredTypes:   []string{"resource"},
+		RequiredLabels:  []string{"digitalocean_firewall"},
+		DefaultSeverity: severity.Warning, //TODO set the default severity
+		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
+			inboundBlocks := resourceBlock.GetBlocks("outbound_rule")
+
+			for _, inboundRuleBlock := range inboundBlocks {
+				if inboundRuleBlock.MissingChild("destination_addresses") {
+					continue
+				}
+				sourceAddressesAttr := inboundRuleBlock.GetAttribute("destination_addresses")
+				if isOpenCidr(sourceAddressesAttr) {
+					set.Add(
+						result.New(resourceBlock).
+							WithDescription(fmt.Sprintf("Resource '%s' defines a fully open outbound_rule.", resourceBlock.FullName())).
+							WithRange(sourceAddressesAttr.Range()).
+							WithAttributeAnnotation(sourceAddressesAttr).
+							WithSeverity(severity.Warning),
+					)
+				}
+			}
+		},
+	})
+}
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 633b1fa74..afc8b674f 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -6,12 +6,13 @@ import "strings"
 type Provider string
 
 const (
-	AWSProvider     Provider = "aws"
-	AzureProvider   Provider = "azure"
-	GCPProvider     Provider = "google"
-	GeneralProvider Provider = "general"
-	OracleProvider  Provider = "oracle"
-	CustomProvider  Provider = "custom"
+	AWSProvider          Provider = "aws"
+	AzureProvider        Provider = "azure"
+	GCPProvider          Provider = "google"
+	GeneralProvider      Provider = "general"
+	OracleProvider       Provider = "oracle"
+	DigitalOceanProvider Provider = "digitalocean"
+	CustomProvider       Provider = "custom"
 )
 
 func RuleProviderToString(provider Provider) string {
diff --git a/scripts/publish-docs.sh b/scripts/publish-docs.sh
index 220a4d75f..f68784517 100755
--- a/scripts/publish-docs.sh
+++ b/scripts/publish-docs.sh
@@ -28,6 +28,8 @@ cp -r checkdocs/docs/aws/* ./_site/_docs/aws/
 cp -r checkdocs/docs/azure/* ./_site/_docs/azure/
 cp -r checkdocs/docs/google/* ./_site/_docs/google/
 cp -r checkdocs/docs/general/* ./_site/_docs/general/
+cp -r checkdocs/docs/oracle/* ./_site/_docs/oracle/
+cp -r checkdocs/docs/digitalocean/* ./_site/_docs/digitalocean/
 deploy
 
 rm -rf checkdocs


Test patch:
diff --git a/internal/app/tfsec/test/dig001_test.go b/internal/app/tfsec/test/dig001_test.go
new file mode 100644
index 000000000..ff22af854
--- /dev/null
+++ b/internal/app/tfsec/test/dig001_test.go
@@ -0,0 +1,95 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/rules"
+)
+
+func Test_DIGFirewallHasOpenInboundAccess(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode string
+		mustExcludeResultCode string
+	}{
+		{
+			name: "digital ocean firewall with open source addresses fails check",
+			source: `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	inbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  source_addresses = ["0.0.0.0/0", "::/0"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.DIGFirewallHasOpenInboundAccess,
+		},
+		{
+			name: "digital ocean firewall with open ipv6 source addresses fails check",
+			source: `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	inbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  source_addresses = ["::/0"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.DIGFirewallHasOpenInboundAccess,
+		},
+		{
+			name: "digital ocean firewall with open ipv4 source addresses fails check",
+			source: `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	inbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  source_addresses = ["0.0.0.0/0"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.DIGFirewallHasOpenInboundAccess,
+		},
+		{
+			name: "digital ocean firewall with good source addresses passes check",
+			source: `
+resource "digitalocean_firewall" "good_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	inbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  source_addresses = ["192.168.1.0/24", "2002:1:2::/48"]
+	}
+}
+
+`,
+			mustExcludeResultCode: rules.DIGFirewallHasOpenInboundAccess,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanHCL(test.source, t)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}
diff --git a/internal/app/tfsec/test/dig002_test.go b/internal/app/tfsec/test/dig002_test.go
new file mode 100644
index 000000000..60625f7a9
--- /dev/null
+++ b/internal/app/tfsec/test/dig002_test.go
@@ -0,0 +1,95 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/rules"
+)
+
+func Test_DIGFirewallHasOpenOutboundAccess(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode string
+		mustExcludeResultCode string
+	}{
+		{
+			name: "digital ocean firewall with open destination addresses fails check",
+			source: `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	outbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  destination_addresses = ["0.0.0.0/0", "::/0"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.DIGFirewallHasOpenOutboundAccess,
+		},
+		{
+			name: "digital ocean firewall with open ipv6 destination addresses fails check",
+			source: `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	outbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  destination_addresses = ["::/0"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.DIGFirewallHasOpenOutboundAccess,
+		},
+		{
+			name: "digital ocean firewall with open ipv4 destination addresses fails check",
+			source: `
+resource "digitalocean_firewall" "bad_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	outbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  destination_addresses = ["0.0.0.0/0"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.DIGFirewallHasOpenOutboundAccess,
+		},
+		{
+			name: "digital ocean firewall with good destination addresses passes check",
+			source: `
+resource "digitalocean_firewall" "good_example" {
+	name = "only-22-80-and-443"
+  
+	droplet_ids = [digitalocean_droplet.web.id]
+  
+	outbound_rule {
+	  protocol         = "tcp"
+	  port_range       = "22"
+	  destination_addresses = ["192.168.1.0/24", "2002:1:2::/48"]
+	}
+}
+
+`,
+			mustExcludeResultCode: rules.DIGFirewallHasOpenOutboundAccess,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanHCL(test.source, t)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}