Repository: liamg/tfsec
Pull request #159 (2020-09-09 21:26:42)

[GCP Check] Add IAM user grant check
👋 Adding another GCP check that looks for IAM grants directly to user identities. It searches data types `google_iam_policy` and quite a few resource types related to IAM to accomplish this. All it searches for is a prefix of `user:` within the member/members attribute of each block.

Example TF blocks it would flag:
```
data "google_iam_policy" "test-policy" {
	binding {
                ...
		members = [
			"group:test@example.com",
			"user:test@example.com",
		]
	}
}

resource "google_storage_bucket_iam_binding" "bucket-binding" {
        ...
	members = [
		"user:test@example.com",
		]
}

resource "google_project_iam_member" "project-member" {
        ...
	member = "user:test@example.com"
}
```

If I've forgotten any resources from the check feel free to add them. Otherwise once this gets merged I'll put together a documentation page 👍 


Patch:
diff --git a/README.md b/README.md
index 61d0b086c..81b25188d 100644
--- a/README.md
+++ b/README.md
@@ -178,6 +178,7 @@ there are also checks which are provider agnostic.
 | GCP008  | google   | Legacy client authentication methods utilized.
 | GCP009  | google   | Pod security policy enforcement not defined.
 | GCP010  | google   | Shielded GKE nodes not enabled.
+| GCP011  | google   | IAM granted directly to user.
 
 ## Running in CI
 
diff --git a/internal/app/tfsec/checks/google_user_iam_grant.go b/internal/app/tfsec/checks/google_user_iam_grant.go
new file mode 100644
index 000000000..bbdee9c1d
--- /dev/null
+++ b/internal/app/tfsec/checks/google_user_iam_grant.go
@@ -0,0 +1,73 @@
+package checks
+
+import (
+	"fmt"
+	"strings"
+	"github.com/liamg/tfsec/internal/app/tfsec/parser"
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+	"github.com/zclconf/go-cty/cty"
+)
+
+// GoogleUserIAMGrant See https://github.com/liamg/tfsec#included-checks for check info
+const GoogleUserIAMGrant scanner.RuleID = "GCP011"
+
+func init() {
+	scanner.RegisterCheck(scanner.Check{
+		Code:           GoogleUserIAMGrant,
+		RequiredTypes:  []string{"resource", "data"},
+		RequiredLabels: []string{
+			"google_cloud_run_service_iam_binding",
+			"google_cloud_run_service_iam_member",
+			"google_compute_instance_iam_binding",
+			"google_compute_instance_iam_member",
+			"google_compute_subnetwork_iam_binding",
+			"google_compute_subnetwork_iam_member",
+			"google_data_catalog_entry_group_iam_binding",
+			"google_data_catalog_entry_group_iam_member",
+			"google_folder_iam_member",
+			"google_folder_iam_binding",
+			"google_project_iam_member",
+			"google_project_iam_binding",
+			"google_pubsub_subscription_iam_binding",
+			"google_pubsub_subscription_iam_member",
+			"google_pubsub_topic_iam_binding",
+			"google_pubsub_topic_iam_member",
+			"google_sourcerepo_repository_iam_binding",
+			"google_sourcerepo_repository_iam_member",
+			"google_spanner_database_iam_binding",
+			"google_spanner_database_iam_member",
+			"google_spanner_instance_iam_binding",
+			"google_spanner_instance_iam_member",
+			"google_storage_bucket_iam_binding",
+			"google_storage_bucket_iam_member",
+			"google_iam_policy",
+		},
+		CheckFunc: func(check *scanner.Check, block *parser.Block, _ *scanner.Context) []scanner.Result {
+
+			var members []cty.Value
+			var attributes *parser.Attribute
+
+			if attributes = block.GetAttribute("member"); attributes != nil {
+				members = append(members, attributes.Value())
+			} else if attributes = block.GetAttribute("members"); attributes != nil {
+				members = attributes.Value().AsValueSlice()
+			} else if attributes = block.GetBlock("binding").GetAttribute("members"); attributes != nil {
+				members = attributes.Value().AsValueSlice()
+			}
+
+			for _, identities := range members {
+				if identities.Type() == cty.String && strings.HasPrefix(identities.AsString(), "user:") {
+					return []scanner.Result{
+						check.NewResult(
+							fmt.Sprintf("'%s' grants IAM to a user object. It is recommended to manage user permissions with groups.", block.Name()),
+							attributes.Range(),
+							scanner.SeverityWarning,
+						),
+					}
+				}
+			}
+
+			return nil
+		},
+	})
+}


Test patch:
diff --git a/internal/app/tfsec/google_user_iam_grant_test.go b/internal/app/tfsec/google_user_iam_grant_test.go
new file mode 100644
index 000000000..b6d4b223d
--- /dev/null
+++ b/internal/app/tfsec/google_user_iam_grant_test.go
@@ -0,0 +1,95 @@
+package tfsec
+
+import (
+	"testing"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/scanner"
+
+	"github.com/liamg/tfsec/internal/app/tfsec/checks"
+)
+
+func Test_GoogleUserIAMGrant(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode scanner.RuleID
+		mustExcludeResultCode scanner.RuleID
+	}{
+		{
+			name: "check google_project_iam_binding with grant to user identity",
+			source: `
+resource "google_project_iam_binding" "project-binding" {
+	members = [
+		"user:test@example.com",
+		]
+}`,
+			mustIncludeResultCode: checks.GoogleUserIAMGrant,
+		},
+		{
+			name: "check google_project_iam_binding with grant to user identity",
+			source: `
+resource "google_project_iam_binding" "project-binding" {
+	members = [
+		"group:test@example.com",
+		]
+}`,
+			mustExcludeResultCode: checks.GoogleUserIAMGrant,
+		},
+		{
+			name: "check google_project_iam_member with grant to user identity",
+			source: `
+resource "google_project_iam_member" "project-member" {
+	member = "user:test@example.com"
+}`,
+			mustIncludeResultCode: checks.GoogleUserIAMGrant,
+		},
+		{
+			name: "check google_storage_bucket_iam_binding with grant to user identity",
+			source: `
+resource "google_storage_bucket_iam_binding" "bucket-binding" {
+	members = [
+		"user:test@example.com",
+		]
+}`,
+			mustIncludeResultCode: checks.GoogleUserIAMGrant,
+		},
+		{
+			name: "check google_storage_bucket_iam_member with grant to user identity",
+			source: `
+resource "google_storage_bucket_iam_member" "bucket-member" {
+	member = "user:test@example.com"
+}`,
+			mustIncludeResultCode: checks.GoogleUserIAMGrant,
+		},
+		{
+			name: "check google_storage_bucket_iam_member with grant to service account identity",
+			source: `
+resource "google_storage_bucket_iam_member" "bucket-member" {
+	member = "serviceAccount:test@example.com"
+}`,
+			mustExcludeResultCode: checks.GoogleUserIAMGrant,
+		},
+		{
+			name: "check data.google_iam_policy with grant to user",
+			source: `
+data "google_iam_policy" "test-policy" {
+	binding {
+		members = [
+			"group:test@example.com",
+			"user:test@example.com",
+		]
+	}
+}`,
+			mustIncludeResultCode: checks.GoogleUserIAMGrant,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanSource(test.source)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}