Repository: aquasecurity/tfsec
Pull request #985 (2021-07-30 16:01:23)

Add first cloudstack check for user data
The user_data must not have sensitive data



Patch:
diff --git a/cmd/tfsec-docs/webpage.go b/cmd/tfsec-docs/webpage.go
index 33857908c..79e4e3c53 100644
--- a/cmd/tfsec-docs/webpage.go
+++ b/cmd/tfsec-docs/webpage.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 	"text/template"
 
+	"github.com/aquasecurity/tfsec/pkg/provider"
 	"github.com/aquasecurity/tfsec/pkg/rule"
 )
 
@@ -90,14 +91,7 @@ func formatProviderName(providerName string) string {
 	if providerName == "digitalocean" {
 		providerName = "digital ocean"
 	}
-	switch providerName {
-	case "aws":
-		return strings.ToUpper(providerName)
-	case "openstack":
-		return "OpenStack"
-	default:
-		return strings.Title(strings.ToLower(providerName))
-	}
+	return provider.Provider(providerName).DisplayName()
 }
 
 func generateWebPage(webProviderPath string, r rule.Rule) error {
diff --git a/cmd/tfsec-skeleton/generation.go b/cmd/tfsec-skeleton/generation.go
index 439335686..de35bc38d 100644
--- a/cmd/tfsec-skeleton/generation.go
+++ b/cmd/tfsec-skeleton/generation.go
@@ -21,6 +21,7 @@ var providers = []string{
 	"Google",
 	"Kubernetes",
 	"OpenStack",
+	"CloudStack",
 	"Oracle",
 }
 
diff --git a/internal/app/tfsec/rules/aws/launch/no_sensitive_info_rule.go b/internal/app/tfsec/rules/aws/launch/no_sensitive_info_rule.go
index 4f11f82a1..b18c45e9b 100644
--- a/internal/app/tfsec/rules/aws/launch/no_sensitive_info_rule.go
+++ b/internal/app/tfsec/rules/aws/launch/no_sensitive_info_rule.go
@@ -27,7 +27,7 @@ func init() {
 		ShortCode: "no-sensitive-info",
 		Documentation: rule.RuleDocumentation{
 			Summary:     "Ensure all data stored in the Launch configuration EBS is securely encrypted",
-			Explanation: `When creating Launch Configurations, user data can be used for the initial configuration of the check. User data must not contain any sensitive data.`,
+			Explanation: `When creating Launch Configurations, user data can be used for the initial configuration of the instance. User data must not contain any sensitive data.`,
 			Impact:      "Sensitive credentials in user data can be leaked",
 			Resolution:  "Don't use sensitive data in user data",
 			BadExample: []string{`
diff --git a/internal/app/tfsec/rules/cloudstack/compute/no_sensitive_info_rule.go b/internal/app/tfsec/rules/cloudstack/compute/no_sensitive_info_rule.go
new file mode 100644
index 000000000..7a68d9dc1
--- /dev/null
+++ b/internal/app/tfsec/rules/cloudstack/compute/no_sensitive_info_rule.go
@@ -0,0 +1,108 @@
+package compute
+
+// ATTENTION!
+// This rule was autogenerated!
+// Before making changes, consider updating the generator.
+
+import (
+	"encoding/base64"
+
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/debug"
+	"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"
+	"github.com/owenrumney/squealer/pkg/squealer"
+)
+
+func init() {
+	scanner.RegisterCheckRule(rule.Rule{
+		Provider:  provider.CloudStackProvider,
+		Service:   "compute",
+		ShortCode: "no-sensitive-info",
+		Documentation: rule.RuleDocumentation{
+			Summary:     "No sensitive data stored in user_data",
+			Explanation: `When creating instances, user data can be used during the initial confiugtation. User data must not contain sensitive information`,
+			Impact:      "Sensitive credentials in the user data can be leaked",
+			Resolution:  "Don't use sensitive data in the user data section",
+			BadExample: []string{`
+resource "cloudstack_instance" "web" {
+  name             = "server-1"
+  service_offering = "small"
+  network_id       = "6eb22f91-7454-4107-89f4-36afcdf33021"
+  template         = "CentOS 6.5"
+  zone             = "zone-1"
+  user_data        = <<EOF
+export DATABASE_PASSWORD=\"SomeSortOfPassword\"
+EOF
+}
+`, `
+resource "cloudstack_instance" "web" {
+  name             = "server-1"
+  service_offering = "small"
+  network_id       = "6eb22f91-7454-4107-89f4-36afcdf33021"
+  template         = "CentOS 6.5"
+  zone             = "zone-1"
+  user_data        = "ZXhwb3J0IERBVEFCQVNFX1BBU1NXT1JEPSJTb21lU29ydE9mUGFzc3dvcmQi"
+}
+`},
+			GoodExample: []string{`
+resource "cloudstack_instance" "web" {
+  name             = "server-1"
+  service_offering = "small"
+  network_id       = "6eb22f91-7454-4107-89f4-36afcdf33021"
+  template         = "CentOS 6.5"
+  zone             = "zone-1"
+  user_data        = <<EOF
+export GREETING="Hello there"
+EOF
+}
+`, `
+resource "cloudstack_instance" "web" {
+  name             = "server-1"
+  service_offering = "small"
+  network_id       = "6eb22f91-7454-4107-89f4-36afcdf33021"
+  template         = "CentOS 6.5"
+  zone             = "zone-1"
+  user_data        = "ZXhwb3J0IEVESVRPUj12aW1hY3M="
+}
+`},
+			Links: []string{
+				"https://registry.terraform.io/providers/hashicorp/cloudstack/latest/docs/resources/instance#",
+			},
+		},
+		RequiredTypes: []string{
+			"resource",
+		},
+		RequiredLabels: []string{
+			"cloudstack_instance",
+		},
+		DefaultSeverity: severity.High,
+		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
+
+			customDataAttr := resourceBlock.GetAttribute("user_data")
+
+			if customDataAttr.IsNotNil() && customDataAttr.IsString() {
+				encoded, err := base64.StdEncoding.DecodeString(customDataAttr.Value().AsString())
+				if err != nil {
+					debug.Log("could not decode the base64 string in the terraform, trying with the string verbatim")
+					encoded = []byte(customDataAttr.Value().AsString())
+				}
+				if checkStringForSensitive(string(encoded)) {
+					set.AddResult().
+						WithDescription("Resource '%s' has user_data_base64 with sensitive data.", resourceBlock.FullName()).
+						WithAttribute(customDataAttr)
+				}
+			}
+
+		},
+	})
+}
+
+func checkStringForSensitive(stringToCheck string) bool {
+	scanResult := squealer.NewStringScanner().Scan(stringToCheck)
+	return scanResult.TransgressionFound
+}
diff --git a/internal/app/tfsec/rules/init.go b/internal/app/tfsec/rules/init.go
index 9e80b19d2..c05281a8e 100644
--- a/internal/app/tfsec/rules/init.go
+++ b/internal/app/tfsec/rules/init.go
@@ -52,6 +52,7 @@ import (
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/azure/securitycenter"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/azure/storage"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/azure/synapse"
+	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/cloudstack/compute"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/digitalocean/compute"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/digitalocean/droplet"
 	_ "github.com/aquasecurity/tfsec/internal/app/tfsec/rules/digitalocean/loadbalancing"
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 20e35dc71..3df3f8274 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -17,6 +17,7 @@ const (
 	KubernetesProvider   Provider = "kubernetes"
 	OracleProvider       Provider = "oracle"
 	OpenStackProvider    Provider = "openstack"
+	CloudStackProvider   Provider = "cloudstack"
 )
 
 func RuleProviderToString(provider Provider) string {
@@ -31,6 +32,8 @@ func (p Provider) DisplayName() string {
 		return "Digital Ocean"
 	case "openstack":
 		return "OpenStack"
+	case "cloudstack":
+		return "Cloudstack"
 	default:
 		return strings.Title(strings.ToLower(string(p)))
 	}


Test patch:
diff --git a/internal/app/tfsec/rules/cloudstack/compute/no_sensitive_info_rule_test.go b/internal/app/tfsec/rules/cloudstack/compute/no_sensitive_info_rule_test.go
new file mode 100644
index 000000000..b05fb2323
--- /dev/null
+++ b/internal/app/tfsec/rules/cloudstack/compute/no_sensitive_info_rule_test.go
@@ -0,0 +1,53 @@
+package compute
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
+)
+
+func Test_CloudstackNoSensitiveInfo_FailureExamples(t *testing.T) {
+	expectedCode := "cloudstack-compute-no-sensitive-info"
+
+	rule, err := scanner.GetRuleById(expectedCode)
+	if err != nil {
+		t.FailNow()
+	}
+	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_CloudstackNoSensitiveInfo_SuccessExamples(t *testing.T) {
+	expectedCode := "cloudstack-compute-no-sensitive-info"
+
+	rule, err := scanner.GetRuleById(expectedCode)
+	if err != nil {
+		t.FailNow()
+	}
+	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)
+	}
+}