Repository: tfsec/tfsec
Pull request #836 (2021-07-08 15:06:28)

Add check for droplet ssh_keys


Patch:
diff --git a/cmd/tfsec-docs/webpage.go b/cmd/tfsec-docs/webpage.go
index 2a599c800..25266a0d0 100644
--- a/cmd/tfsec-docs/webpage.go
+++ b/cmd/tfsec-docs/webpage.go
@@ -21,6 +21,13 @@ permalink: /docs/{{$.Provider}}/{{$.ID}}/
 
 {{$.Documentation.Explanation}}
 
+### Impact
+{{$.Documentation.Impact}}
+
+### Resolution
+{{$.Documentation.Resolution}}
+
+
 {{if $.Documentation.BadExample }}
 ### Insecure Example
 
diff --git a/internal/app/tfsec/rules/dig002.go b/internal/app/tfsec/rules/dig002.go
index b063e829f..9669dee36 100644
--- a/internal/app/tfsec/rules/dig002.go
+++ b/internal/app/tfsec/rules/dig002.go
@@ -72,13 +72,13 @@ func init() {
 				if inboundRuleBlock.MissingChild("destination_addresses") {
 					continue
 				}
-				sourceAddressesAttr := inboundRuleBlock.GetAttribute("destination_addresses")
-				if isOpenCidr(sourceAddressesAttr) {
+				destinationAddressesAttr := inboundRuleBlock.GetAttribute("destination_addresses")
+				if isOpenCidr(destinationAddressesAttr) {
 					set.Add(
 						result.New(resourceBlock).
 							WithDescription(fmt.Sprintf("Resource '%s' defines a fully open outbound_rule.", resourceBlock.FullName())).
-							WithRange(sourceAddressesAttr.Range()).
-							WithAttributeAnnotation(sourceAddressesAttr).
+							WithRange(destinationAddressesAttr.Range()).
+							WithAttributeAnnotation(destinationAddressesAttr).
 							WithSeverity(severity.Warning),
 					)
 				}
diff --git a/internal/app/tfsec/rules/dig003.go b/internal/app/tfsec/rules/dig003.go
new file mode 100644
index 000000000..259af5b69
--- /dev/null
+++ b/internal/app/tfsec/rules/dig003.go
@@ -0,0 +1,86 @@
+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 DIGDropletHasNoSSHKeysAssigned = "DIG003"
+const DIGDropletHasNoSSHKeysAssignedDescription = "SSH Keys are the preferred way to connect to your droplet, no keys are supplied"
+const DIGDropletHasNoSSHKeysAssignedImpact = "Logging in with username and password is easier to compromise"
+const DIGDropletHasNoSSHKeysAssignedResolution = "Use ssh keys for login"
+const DIGDropletHasNoSSHKeysAssignedExplanation = `
+When working with a server, you’ll likely spend most of your time in a terminal session connected to your server through SSH. A more secure alternative to password-based logins, SSH keys use encryption to provide a secure way of logging into your server and are recommended for all users.
+`
+const DIGDropletHasNoSSHKeysAssignedBadExample = `
+resource "digitalocean_droplet" "good_example" {
+	image    = "ubuntu-18-04-x64"
+	name     = "web-1"
+	region   = "nyc2"
+	size     = "s-1vcpu-1gb"
+ }
+`
+const DIGDropletHasNoSSHKeysAssignedGoodExample = `
+data "digitalocean_ssh_key" "terraform" {
+	name = "myKey"
+  }
+  
+resource "digitalocean_droplet" "good_example" {
+	image    = "ubuntu-18-04-x64"
+	name     = "web-1"
+	region   = "nyc2"
+	size     = "s-1vcpu-1gb"
+	ssh_keys = [ data.digitalocean_ssh_key.myKey.id ]
+}
+`
+
+func init() {
+	scanner.RegisterCheckRule(rule.Rule{
+		ID: DIGDropletHasNoSSHKeysAssigned,
+		Documentation: rule.RuleDocumentation{
+			Summary:     DIGDropletHasNoSSHKeysAssignedDescription,
+			Explanation: DIGDropletHasNoSSHKeysAssignedExplanation,
+			Impact:      DIGDropletHasNoSSHKeysAssignedImpact,
+			Resolution:  DIGDropletHasNoSSHKeysAssignedResolution,
+			BadExample:  DIGDropletHasNoSSHKeysAssignedBadExample,
+			GoodExample: DIGDropletHasNoSSHKeysAssignedGoodExample,
+			Links: []string{
+				"https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/droplet#ssh_keys",
+				"https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process",
+			},
+		},
+		Provider:        provider.DigitalOceanProvider,
+		RequiredTypes:   []string{"resource"},
+		RequiredLabels:  []string{"digitalocean_droplet"},
+		DefaultSeverity: severity.Warning,
+		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context) {
+
+			if resourceBlock.MissingChild("ssh_keys") {
+				set.Add(
+					result.New(resourceBlock).
+						WithDescription(fmt.Sprintf("Resource '%s' does not define ssh_keys", resourceBlock.FullName())).
+						WithRange(resourceBlock.Range()).
+						WithSeverity(severity.Warning),
+				)
+				return
+			}
+			sshKeysAttr := resourceBlock.GetAttribute("ssh_keys")
+			if sshKeysAttr.IsEmpty() {
+				set.Add(
+					result.New(resourceBlock).
+						WithDescription(fmt.Sprintf("Resource '%s' has ssh_key specified but is empty.", resourceBlock.FullName())).
+						WithRange(sshKeysAttr.Range()).
+						WithAttributeAnnotation(sshKeysAttr).
+						WithSeverity(severity.Warning),
+				)
+			}
+		},
+	})
+}


Test patch:
diff --git a/internal/app/tfsec/test/dig003_test.go b/internal/app/tfsec/test/dig003_test.go
new file mode 100644
index 000000000..73be125f3
--- /dev/null
+++ b/internal/app/tfsec/test/dig003_test.go
@@ -0,0 +1,68 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/rules"
+)
+
+func Test_DIGDropletHasNoSSHKeysAssigned(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode string
+		mustExcludeResultCode string
+	}{
+		{
+			name: "droplet with no ssh_keys fails check",
+			source: `
+resource "digitalocean_droplet" "bad_example" {
+  image  = "ubuntu-18-04-x64"
+  name   = "web-1"
+  region = "nyc2"
+  size   = "s-1vcpu-1gb"
+}
+`,
+			mustIncludeResultCode: rules.DIGDropletHasNoSSHKeysAssigned,
+		},
+		{
+			name: "droplet with ssh keys defined but empty fails check",
+			source: `
+resource "digitalocean_droplet" "bad_example" {
+  image    = "ubuntu-18-04-x64"
+  name     = "web-1"
+  region   = "nyc2"
+  size     = "s-1vcpu-1gb"
+  ssh_heys = []
+}
+`,
+			mustIncludeResultCode: rules.DIGDropletHasNoSSHKeysAssigned,
+		},
+		{
+			name: "TODO: add test name",
+			source: `
+data "digitalocean_ssh_key" "terraform" {
+  name = "myKey"
+}
+
+resource "digitalocean_droplet" "good_example" {
+  image    = "ubuntu-18-04-x64"
+  name     = "web-1"
+  region   = "nyc2"
+  size     = "s-1vcpu-1gb"
+  ssh_keys = [ data.digitalocean_ssh_key.myKey.id ]
+}
+`,
+			mustExcludeResultCode: rules.DIGDropletHasNoSSHKeysAssigned,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanHCL(test.source, t)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}