Repository: aquasecurity/tfsec
Pull request #983 (2021-07-30 15:44:05)

Initial Kubernetes checks


Patch:
diff --git a/cmd/tfsec-skeleton/generation.go b/cmd/tfsec-skeleton/generation.go
index 0d3b52d5d..439335686 100644
--- a/cmd/tfsec-skeleton/generation.go
+++ b/cmd/tfsec-skeleton/generation.go
@@ -19,6 +19,7 @@ var providers = []string{
 	"General",
 	"GitHub",
 	"Google",
+	"Kubernetes",
 	"OpenStack",
 	"Oracle",
 }
diff --git a/cmd/tfsec-skeleton/interactive.go b/cmd/tfsec-skeleton/interactive.go
index d9d0059f2..9aa372bff 100644
--- a/cmd/tfsec-skeleton/interactive.go
+++ b/cmd/tfsec-skeleton/interactive.go
@@ -129,10 +129,10 @@ func (input *Input) gatherInputsInteractively() error {
 	if requirementIndex == len(requirementStrs)-1 {
 		input.Requirement = requirements.Custom(input.RequiredTypes[0], input.RequiredLabels[0], exampleCode)
 	} else {
-		dotPath := prompt.EnterInput("Enter the path to the attribute to check (e.g. settings.encryption.enabled): ")
+		input.AttributeName = prompt.EnterInput("Enter the path to the attribute to check (e.g. settings.encryption.enabled): ")
 		expectedValue := enterInterfaceValue("Enter the value the attribute must have to pass the check (e.g. true): ")
 		failForDefault := inputYesNo("Should the check fail if a default value is used?")
-		input.Requirement = requirements.NewAttributeRequirement(input.RequiredTypes[0], input.RequiredLabels[0], dotPath, expectedValue, failForDefault, exampleCode, requirementTypes[requirementIndex])
+		input.Requirement = requirements.NewAttributeRequirement(input.RequiredTypes[0], input.RequiredLabels[0], input.AttributeName, expectedValue, failForDefault, exampleCode, requirementTypes[requirementIndex])
 	}
 
 	_ = deleteSwap()
diff --git a/cmd/tfsec-skeleton/templates.go b/cmd/tfsec-skeleton/templates.go
index 2866d53db..fb1092897 100644
--- a/cmd/tfsec-skeleton/templates.go
+++ b/cmd/tfsec-skeleton/templates.go
@@ -65,7 +65,7 @@ func Test_{{.TestName}}_FailureExamples(t *testing.T) {
 
 	rule, err := scanner.GetRuleById(expectedCode)
 	if err != nil {
-		t.FailNow()
+		t.Fatalf("Rule not found: %s", expectedCode)
 	}
 	for i, badExample := range rule.Documentation.BadExample {
 		t.Logf("Running bad example for '%s' #%d", expectedCode, i+1)
@@ -87,7 +87,7 @@ func Test_{{.TestName}}_SuccessExamples(t *testing.T) {
 
 	rule, err := scanner.GetRuleById(expectedCode)
 	if err != nil {
-		t.FailNow()
+		t.Fatalf("Rule not found: %s", expectedCode)
 	}
 	for i, example := range rule.Documentation.GoodExample {
 		t.Logf("Running good example for '%s' #%d", expectedCode, i+1)
diff --git a/internal/app/tfsec/rules/init.go b/internal/app/tfsec/rules/init.go
index a5c46c1e1..9e80b19d2 100644
--- a/internal/app/tfsec/rules/init.go
+++ b/internal/app/tfsec/rules/init.go
@@ -63,6 +63,7 @@ import (
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/google/iam"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/google/sql"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/google/storage"
+	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/kubernetes/network"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/openstack/compute"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/openstack/fw"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/oracle/compute"
diff --git a/internal/app/tfsec/rules/kubernetes/network/no_public_egress_rule.go b/internal/app/tfsec/rules/kubernetes/network/no_public_egress_rule.go
new file mode 100644
index 000000000..45e87f70e
--- /dev/null
+++ b/internal/app/tfsec/rules/kubernetes/network/no_public_egress_rule.go
@@ -0,0 +1,183 @@
+package network
+
+// ATTENTION!
+// This rule was autogenerated!
+// Before making changes, consider updating the generator.
+
+import (
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/cidr"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/hclcontext"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
+	"github.com/aquasecurity/tfsec/pkg/provider"
+	"github.com/aquasecurity/tfsec/pkg/result"
+	"github.com/aquasecurity/tfsec/pkg/rule"
+	"github.com/aquasecurity/tfsec/pkg/severity"
+)
+
+func init() {
+	scanner.RegisterCheckRule(rule.Rule{
+		Provider:  provider.KubernetesProvider,
+		Service:   "network",
+		ShortCode: "no-public-egress",
+		Documentation: rule.RuleDocumentation{
+			Summary:     "Public egress should not be allowed via network policies",
+			Explanation: `You should not expose infrastructure to the public internet except where explicitly required`,
+			Impact:      "Exfiltration of data to the public internet",
+			Resolution:  "Remove public access except where explicitly required",
+			BadExample: []string{`
+resource "kubernetes_network_policy" "bad_example" {
+  metadata {
+    name      = "terraform-example-network-policy"
+    namespace = "default"
+  }
+
+  spec {
+    pod_selector {
+      match_expressions {
+        key      = "name"
+        operator = "In"
+        values   = ["webfront", "api"]
+      }
+    }
+
+    egress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      to {
+        ip_block {
+          cidr = "0.0.0.0/0"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    ingress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      from {
+        ip_block {
+          cidr = "10.0.0.0/16"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    policy_types = ["Ingress", "Egress"]
+  }
+}
+`},
+			GoodExample: []string{`
+resource "kubernetes_network_policy" "good_example" {
+  metadata {
+    name      = "terraform-example-network-policy"
+    namespace = "default"
+  }
+
+  spec {
+    pod_selector {
+      match_expressions {
+        key      = "name"
+        operator = "In"
+        values   = ["webfront", "api"]
+      }
+    }
+
+    egress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      to {
+        ip_block {
+          cidr = "10.0.0.0/16"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    ingress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      from {
+        ip_block {
+          cidr = "10.0.0.0/16"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    policy_types = ["Ingress", "Egress"]
+  }
+}
+`},
+			Links: []string{
+				"https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/network_policy#spec.ingress.from.ip_block.cidr",
+			},
+		},
+		RequiredTypes: []string{
+			"resource",
+		},
+		RequiredLabels: []string{
+			"kubernetes_network_policy",
+		},
+		DefaultSeverity: severity.High,
+		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
+
+			egressBlock := resourceBlock.GetBlock("spec").GetBlock("egress")
+			if egressBlock.IsNil() || len(egressBlock.GetBlocks("to")) == 0 {
+				set.AddResult().
+					WithDescription("Resource '%s' allows all egress traffic by default", resourceBlock.FullName())
+				return
+			}
+
+			for _, to := range egressBlock.GetBlocks("to") {
+				if cidrAttr := to.GetBlock("ip_block").GetAttribute("cidr"); cidrAttr.IsString() {
+					if cidr.IsOpen(cidrAttr) {
+						set.AddResult().
+							WithDescription("Resource '%s' allows egress traffic to the internet", resourceBlock.FullName()).
+							WithAttribute(cidrAttr)
+					}
+				}
+			}
+		},
+	})
+}
diff --git a/internal/app/tfsec/rules/kubernetes/network/no_public_ingress_rule.go b/internal/app/tfsec/rules/kubernetes/network/no_public_ingress_rule.go
new file mode 100644
index 000000000..5c5e82a78
--- /dev/null
+++ b/internal/app/tfsec/rules/kubernetes/network/no_public_ingress_rule.go
@@ -0,0 +1,182 @@
+package network
+
+// ATTENTION!
+// This rule was autogenerated!
+// Before making changes, consider updating the generator.
+
+import (
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/cidr"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/hclcontext"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
+	"github.com/aquasecurity/tfsec/pkg/provider"
+	"github.com/aquasecurity/tfsec/pkg/result"
+	"github.com/aquasecurity/tfsec/pkg/rule"
+	"github.com/aquasecurity/tfsec/pkg/severity"
+)
+
+func init() {
+	scanner.RegisterCheckRule(rule.Rule{
+		Provider:  provider.KubernetesProvider,
+		Service:   "network",
+		ShortCode: "no-public-ingress",
+		Documentation: rule.RuleDocumentation{
+			Summary:     "Public ingress should not be allowed via network policies",
+			Explanation: `You should not expose infrastructure to the public internet except where explicitly required`,
+			Impact:      "Exposure of infrastructure to the public internet",
+			Resolution:  "Remove public access except where explicitly required",
+			BadExample: []string{`
+resource "kubernetes_network_policy" "bad_example" {
+  metadata {
+    name      = "terraform-example-network-policy"
+    namespace = "default"
+  }
+
+  spec {
+    pod_selector {
+      match_expressions {
+        key      = "name"
+        operator = "In"
+        values   = ["webfront", "api"]
+      }
+    }
+
+    ingress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      from {
+        ip_block {
+          cidr = "0.0.0.0/0"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    egress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      to {
+        ip_block {
+          cidr = "0.0.0.0/0"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    policy_types = ["Ingress", "Egress"]
+  }
+}
+`},
+			GoodExample: []string{`
+resource "kubernetes_network_policy" "good_example" {
+  metadata {
+    name      = "terraform-example-network-policy"
+    namespace = "default"
+  }
+
+  spec {
+    pod_selector {
+      match_expressions {
+        key      = "name"
+        operator = "In"
+        values   = ["webfront", "api"]
+      }
+    }
+
+    ingress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      from {
+        ip_block {
+          cidr = "10.0.0.0/16"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    egress {
+      ports {
+        port     = "http"
+        protocol = "TCP"
+      }
+      ports {
+        port     = "8125"
+        protocol = "UDP"
+      }
+
+      to {
+        ip_block {
+          cidr = "0.0.0.0/0"
+          except = [
+            "10.0.0.0/24",
+            "10.0.1.0/24",
+          ]
+        }
+      }
+    }
+
+    policy_types = ["Ingress", "Egress"]
+  }
+}
+`},
+			Links: []string{
+				"https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/network_policy#spec.ingress.from.ip_block.cidr",
+			},
+		},
+		RequiredTypes: []string{
+			"resource",
+		},
+		RequiredLabels: []string{
+			"kubernetes_network_policy",
+		},
+		DefaultSeverity: severity.High,
+		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
+			ingressBlock := resourceBlock.GetBlock("spec").GetBlock("ingress")
+			if ingressBlock.IsNil() || len(ingressBlock.GetBlocks("from")) == 0 {
+				set.AddResult().
+					WithDescription("Resource '%s' allows all ingress traffic by default", resourceBlock.FullName())
+				return
+			}
+
+			for _, from := range ingressBlock.GetBlocks("from") {
+				if cidrAttr := from.GetBlock("ip_block").GetAttribute("cidr"); cidrAttr.IsString() {
+					if cidr.IsOpen(cidrAttr) {
+						set.AddResult().
+							WithDescription("Resource '%s' allows ingress traffic from the internet", resourceBlock.FullName()).
+							WithAttribute(cidrAttr)
+					}
+				}
+			}
+		},
+	})
+}
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 3dc9fbff3..20e35dc71 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -14,6 +14,7 @@ const (
 	GeneralProvider      Provider = "general"
 	GitHubProvider       Provider = "github"
 	GoogleProvider       Provider = "google"
+	KubernetesProvider   Provider = "kubernetes"
 	OracleProvider       Provider = "oracle"
 	OpenStackProvider    Provider = "openstack"
 )
diff --git a/pkg/rule/rule.go b/pkg/rule/rule.go
index 7e2e4b376..f89398b45 100644
--- a/pkg/rule/rule.go
+++ b/pkg/rule/rule.go
@@ -2,6 +2,7 @@ package rule
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/aquasecurity/tfsec/pkg/provider"
 	"github.com/aquasecurity/tfsec/pkg/result"
@@ -28,7 +29,7 @@ type Rule struct {
 }
 
 func (r Rule) ID() string {
-	return fmt.Sprintf("%s-%s-%s", r.Provider, r.Service, r.ShortCode)
+	return strings.ToLower(fmt.Sprintf("%s-%s-%s", r.Provider, r.Service, r.ShortCode))
 }
 
 func (r Rule) MatchesID(id string) bool {


Test patch:
diff --git a/internal/app/tfsec/rules/kubernetes/network/no_public_egress_rule_test.go b/internal/app/tfsec/rules/kubernetes/network/no_public_egress_rule_test.go
new file mode 100644
index 000000000..7d4dac68d
--- /dev/null
+++ b/internal/app/tfsec/rules/kubernetes/network/no_public_egress_rule_test.go
@@ -0,0 +1,53 @@
+package network
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
+)
+
+func Test_KubernetesNoPublicEgress_FailureExamples(t *testing.T) {
+	expectedCode := "kubernetes-network-no-public-egress"
+
+	rule, err := scanner.GetRuleById(expectedCode)
+	if err != nil {
+		t.Fatalf("Rule not found: %s", expectedCode)
+	}
+	for i, badExample := range rule.Documentation.BadExample {
+		t.Logf("Running bad example for '%s' #%d", expectedCode, i+1)
+		if strings.TrimSpace(badExample) == "" {
+			t.Fatalf("bad example code not provided for %s", rule.ID())
+		}
+		defer func() {
+			if err := recover(); err != nil {
+				t.Fatalf("Scan (bad) failed: %s", err)
+			}
+		}()
+		results := testutil.ScanHCL(badExample, t)
+		testutil.AssertCheckCode(t, rule.ID(), "", results)
+	}
+}
+
+func Test_KubernetesNoPublicEgress_SuccessExamples(t *testing.T) {
+	expectedCode := "kubernetes-network-no-public-egress"
+
+	rule, err := scanner.GetRuleById(expectedCode)
+	if err != nil {
+		t.Fatalf("Rule not found: %s", expectedCode)
+	}
+	for i, example := range rule.Documentation.GoodExample {
+		t.Logf("Running good example for '%s' #%d", expectedCode, i+1)
+		if strings.TrimSpace(example) == "" {
+			t.Fatalf("good example code not provided for %s", rule.ID())
+		}
+		defer func() {
+			if err := recover(); err != nil {
+				t.Fatalf("Scan (good) failed: %s", err)
+			}
+		}()
+		results := testutil.ScanHCL(example, t)
+		testutil.AssertCheckCode(t, "", rule.ID(), results)
+	}
+}
diff --git a/internal/app/tfsec/rules/kubernetes/network/no_public_ingress_rule_test.go b/internal/app/tfsec/rules/kubernetes/network/no_public_ingress_rule_test.go
new file mode 100644
index 000000000..01a19c58a
--- /dev/null
+++ b/internal/app/tfsec/rules/kubernetes/network/no_public_ingress_rule_test.go
@@ -0,0 +1,53 @@
+package network
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
+)
+
+func Test_KubernetesNoPublicIngress_FailureExamples(t *testing.T) {
+	expectedCode := "kubernetes-network-no-public-ingress"
+
+	rule, err := scanner.GetRuleById(expectedCode)
+	if err != nil {
+		t.Fatalf("Rule not found: %s", expectedCode)
+	}
+	for i, badExample := range rule.Documentation.BadExample {
+		t.Logf("Running bad example for '%s' #%d", expectedCode, i+1)
+		if strings.TrimSpace(badExample) == "" {
+			t.Fatalf("bad example code not provided for %s", rule.ID())
+		}
+		defer func() {
+			if err := recover(); err != nil {
+				t.Fatalf("Scan (bad) failed: %s", err)
+			}
+		}()
+		results := testutil.ScanHCL(badExample, t)
+		testutil.AssertCheckCode(t, rule.ID(), "", results)
+	}
+}
+
+func Test_KubernetesNoPublicIngress_SuccessExamples(t *testing.T) {
+	expectedCode := "kubernetes-network-no-public-ingress"
+
+	rule, err := scanner.GetRuleById(expectedCode)
+	if err != nil {
+		t.Fatalf("Rule not found: %s", expectedCode)
+	}
+	for i, example := range rule.Documentation.GoodExample {
+		t.Logf("Running good example for '%s' #%d", expectedCode, i+1)
+		if strings.TrimSpace(example) == "" {
+			t.Fatalf("good example code not provided for %s", rule.ID())
+		}
+		defer func() {
+			if err := recover(); err != nil {
+				t.Fatalf("Scan (good) failed: %s", err)
+			}
+		}()
+		results := testutil.ScanHCL(example, t)
+		testutil.AssertCheckCode(t, "", rule.ID(), results)
+	}
+}
diff --git a/internal/app/tfsec/test/wildcard_test.go b/internal/app/tfsec/test/wildcard_test.go
index 5366c42a7..22efb22c4 100644
--- a/internal/app/tfsec/test/wildcard_test.go
+++ b/internal/app/tfsec/test/wildcard_test.go
@@ -54,7 +54,7 @@ func Test_WildcardMatchingOnRequiredLabels(t *testing.T) {
 
 	for i, test := range tests {
 
-		code := fmt.Sprintf("WILD%d", i)
+		code := fmt.Sprintf("wild%d", i)
 
 		scanner.RegisterCheckRule(rule.Rule{
 			Service:   "service",