Repository: tfsec/tfsec (go)

Issue:
AWS IAM policies checks
For AWS, IAM policies can be checked for common insecure patterns and encourage the user to create the least privilege policies.

Some examples:

Principal identifiers should be restricted as possible:
```
principals {
  type        = "AWS"
  identifiers = ["*"]
}
```

Actions should be restrcited as possible:
`actions   = ["*"]`
`actions   = ["ec2:*"]`

Ressources should be restricted as possible:
`resources = ["*"]`

All the previous not apply if `effect = "Deny"`.

This may also apply to resources policies (Bucket policies, SNS topic policies, ...).

Pull request:
Add check for AWS IAM policy best practice
Resolves #141 

Patch:
diff --git a/internal/app/tfsec/block/attribute.go b/internal/app/tfsec/block/attribute.go
index 8d6a91919..6eb66fbf7 100644
--- a/internal/app/tfsec/block/attribute.go
+++ b/internal/app/tfsec/block/attribute.go
@@ -33,4 +33,5 @@ type Attribute interface {
 	IsString() bool
 	IsNumber() bool
 	IsBool() bool
+	ValueAsStrings() []string
 }
diff --git a/internal/app/tfsec/block/hclattribute.go b/internal/app/tfsec/block/hclattribute.go
index 901617a93..b941f0d8c 100644
--- a/internal/app/tfsec/block/hclattribute.go
+++ b/internal/app/tfsec/block/hclattribute.go
@@ -76,6 +76,36 @@ func (attr *HCLAttribute) Name() string {
 	return attr.hclAttribute.Name
 }
 
+func (attr *HCLAttribute) ValueAsStrings() []string {
+	return getStrings(attr.hclAttribute.Expr, attr.ctx)
+}
+
+func getStrings(expr hcl.Expression, ctx *hcl.EvalContext) []string {
+	var results []string
+	switch t := expr.(type) {
+	case *hclsyntax.TupleConsExpr:
+		for _, expr := range t.Exprs {
+			results = append(results, getStrings(expr, ctx)...)
+		}
+	case *hclsyntax.FunctionCallExpr, *hclsyntax.ScopeTraversalExpr,
+		*hclsyntax.ConditionalExpr:
+		subVal, err := t.Value(ctx)
+		if err == nil && subVal.Type() == cty.String {
+			results = append(results, subVal.AsString())
+		}
+	case *hclsyntax.LiteralValueExpr:
+		if t.Val.Type() == cty.String {
+			results = append(results, t.Val.AsString())
+		}
+	case *hclsyntax.TemplateExpr:
+		// walk the parts of the expression to ensure that it has a literal value
+		for _, p := range t.Parts {
+			results = append(results, getStrings(p, ctx)...)
+		}
+	}
+	return results
+}
+
 func (attr *HCLAttribute) listContains(val cty.Value, stringToLookFor string, ignoreCase bool) bool {
 	valueSlice := val.AsValueSlice()
 	for _, value := range valueSlice {
@@ -84,6 +114,9 @@ func (attr *HCLAttribute) listContains(val cty.Value, stringToLookFor string, ig
 			valueMap := value.AsValueMap()
 			stringToTest = valueMap["key"]
 		}
+		if !value.IsKnown() {
+			continue
+		}
 		if ignoreCase && containsIgnoreCase(stringToTest.AsString(), stringToLookFor) {
 			return true
 		}
diff --git a/internal/app/tfsec/rules/aws097.go b/internal/app/tfsec/rules/aws097.go
index 791d15d7e..f94911dbe 100644
--- a/internal/app/tfsec/rules/aws097.go
+++ b/internal/app/tfsec/rules/aws097.go
@@ -105,9 +105,14 @@ type awsIAMPolicyDocument struct {
 }
 
 type awsIAMPolicyDocumentStatement struct {
-	Effect   string                    `json:"Effect"`
-	Action   awsIAMPolicyDocumentValue `json:"Action"`
-	Resource awsIAMPolicyDocumentValue `json:"Resource,omitempty"`
+	Effect    string                    `json:"Effect"`
+	Action    awsIAMPolicyDocumentValue `json:"Action"`
+	Resource  awsIAMPolicyDocumentValue `json:"Resource,omitempty"`
+	Principal awsIAMPolicyPrincipal     `json:"Principal,omitempty"`
+}
+
+type awsIAMPolicyPrincipal struct {
+	AWS awsIAMPolicyDocumentValue `json:"AWS"`
 }
 
 // AWS allows string or []string as value, we convert everything to []string to avoid casting
@@ -191,8 +196,8 @@ func init() {
 						if resources := statementBlock.GetAttribute("resources"); resources != nil {
 							if resources.Contains("*") {
 								set.Add(
-									result.New(resourceBlock).
-										WithDescription(fmt.Sprintf("Resource '%s' a policy with KMS actions for all KMS keys.", resourceBlock.FullName())).
+									result.New(policyDocumentBlock).
+										WithDescription(fmt.Sprintf("Resource '%s' a policy with KMS actions for all KMS keys.", policyDocumentBlock.FullName())).
 										WithRange(resources.Range()).
 										WithAttributeAnnotation(resources).
 										WithSeverity(severity.Error),
diff --git a/internal/app/tfsec/rules/aws099.go b/internal/app/tfsec/rules/aws099.go
new file mode 100644
index 000000000..6f4e37ff8
--- /dev/null
+++ b/internal/app/tfsec/rules/aws099.go
@@ -0,0 +1,233 @@
+package rules
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"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 AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege = "AWS099"
+const AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeDescription = "IAM policy should avoid use of wildcards and instead apply the principle of least privilege"
+const AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeImpact = "Overly permissive policies may grant access to sensitive resources"
+const AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeResolution = "Specify the exact permissions required, and to which resources they should apply instead of using wildcards."
+const AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeExplanation = `
+You should use the principle of least privilege when defining your IAM policies. This means you should specify each exact permission required without using wildcards, as this could cause the granting of access to certain undesired actions, resources and principals.
+`
+const AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeBadExample = `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = data.aws_iam_policy_document.s3_policy.json
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+			Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+data "aws_iam_policy_document" "s3_policy" {
+  statement {
+    principals {
+      type        = "AWS"
+      identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
+    }
+    actions   = ["s3:*"]
+    resources = ["*"]
+  }
+}
+`
+const AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeGoodExample = `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = data.aws_iam_policy_document.s3_policy.json
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+			Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+data "aws_iam_policy_document" "s3_policy" {
+  statement {
+    principals {
+      type        = "AWS"
+      identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
+    }
+    actions   = ["s3:GetObject"]
+    resources = [aws_s3_bucket.example.arn]
+  }
+}
+`
+
+func init() {
+	scanner.RegisterCheckRule(rule.Rule{
+		ID: AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		Documentation: rule.RuleDocumentation{
+			Summary:     AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeDescription,
+			Explanation: AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeExplanation,
+			Impact:      AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeImpact,
+			Resolution:  AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeResolution,
+			BadExample:  AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeBadExample,
+			GoodExample: AWSIAMPolicyShouldUsePrincipleOfLeastPrivilegeGoodExample,
+			Links:       []string{"https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html"},
+		},
+		Provider:        provider.AWSProvider,
+		RequiredTypes:   []string{"resource"},
+		RequiredLabels:  []string{"aws_iam_policy", "aws_iam_user_policy", "aws_iam_group_policy", "aws_iam_role_policy"},
+		DefaultSeverity: severity.Info,
+		CheckFunc: func(set result.Set, resourceBlock block.Block, ctx *hclcontext.Context) {
+			policyAttr := resourceBlock.GetAttribute("policy")
+			if policyAttr == nil {
+				return
+			}
+
+			if policyAttr.IsString() {
+				checkAWS099PolicyJSON(set, resourceBlock, policyAttr)
+				return
+			}
+
+			policyDocumentBlock, err := ctx.GetReferencedBlock(policyAttr)
+			if err != nil {
+				return
+			}
+
+			if policyDocumentBlock.Type() != "data" || policyDocumentBlock.TypeLabel() != "aws_iam_policy_document" {
+				return
+			}
+
+			checkAWS099PolicyDocumentBlock(set, policyDocumentBlock)
+
+		},
+	})
+}
+
+func checkAWS099PolicyDocumentBlock(set result.Set, policyDocumentBlock block.Block) {
+
+	if statementBlocks := policyDocumentBlock.GetBlocks("statement"); statementBlocks != nil {
+		for _, statementBlock := range statementBlocks {
+
+			if statementBlock.HasChild("effect") && statementBlock.GetAttribute("effect").Equals("deny", block.IgnoreCase) {
+				continue
+			}
+
+			actionsAttr := statementBlock.GetAttribute("actions")
+			if actionsAttr != nil && actionsAttr.Contains("*") {
+				set.Add(
+					result.New(policyDocumentBlock).
+						WithDescription(fmt.Sprintf("Resource '%s' defines a policy with wildcarded actions.", policyDocumentBlock.FullName())).
+						WithRange(actionsAttr.Range()).
+						WithAttributeAnnotation(actionsAttr),
+				)
+			}
+
+			resourcesAttr := statementBlock.GetAttribute("resources")
+			if resourcesAttr != nil && resourcesAttr.Contains("*") {
+				set.Add(
+					result.New(policyDocumentBlock).
+						WithDescription(fmt.Sprintf("Resource '%s' defines a policy with wildcarded resources.", policyDocumentBlock.FullName())).
+						WithRange(resourcesAttr.Range()).
+						WithAttributeAnnotation(resourcesAttr),
+				)
+			}
+
+			principalsBlock := statementBlock.GetBlock("principals")
+			if principalsBlock != nil {
+				principalTypeAttr := principalsBlock.GetAttribute("type")
+				if principalTypeAttr != nil && principalTypeAttr.Equals("AWS") {
+					identifiersAttr := principalsBlock.GetAttribute("identifiers")
+					if identifiersAttr != nil {
+						for _, ident := range identifiersAttr.ValueAsStrings() {
+							if strings.Contains(ident, "*") {
+								set.Add(
+									result.New(policyDocumentBlock).
+										WithDescription(fmt.Sprintf("Resource '%s' defines a policy with wildcarded principal identifiers.", policyDocumentBlock.FullName())).
+										WithRange(resourcesAttr.Range()).
+										WithAttributeAnnotation(resourcesAttr),
+								)
+								break
+							}
+						}
+					}
+				}
+			}
+
+		}
+	}
+}
+
+func checkAWS099PolicyJSON(set result.Set, resourceBlock block.Block, policyAttr block.Attribute) {
+	var document awsIAMPolicyDocument
+	if err := json.Unmarshal([]byte(policyAttr.Value().AsString()), &document); err != nil {
+		return
+	}
+	for _, statement := range document.Statements {
+		if strings.ToLower(statement.Effect) == "deny" {
+			continue
+		}
+		for _, action := range statement.Action {
+			if strings.Contains(action, "*") {
+				set.Add(
+					result.New(resourceBlock).
+						WithDescription(fmt.Sprintf("Resource '%s' defines a policy with wildcarded actions.", resourceBlock.FullName())).
+						WithRange(policyAttr.Range()).
+						WithAttributeAnnotation(policyAttr),
+				)
+			}
+		}
+		for _, resource := range statement.Resource {
+			if strings.Contains(resource, "*") {
+				set.Add(
+					result.New(resourceBlock).
+						WithDescription(fmt.Sprintf("Resource '%s' defines a policy with wildcarded resources.", resourceBlock.FullName())).
+						WithRange(policyAttr.Range()).
+						WithAttributeAnnotation(policyAttr),
+				)
+			}
+		}
+		for _, identifier := range statement.Principal.AWS {
+			if strings.Contains(identifier, "*") {
+				set.Add(
+					result.New(resourceBlock).
+						WithDescription(fmt.Sprintf("Resource '%s' defines a policy with wildcarded principal identifiers.", resourceBlock.FullName())).
+						WithRange(policyAttr.Range()).
+						WithAttributeAnnotation(policyAttr),
+				)
+			}
+		}
+	}
+}
diff --git a/internal/app/tfsec/scanner/scanner.go b/internal/app/tfsec/scanner/scanner.go
index 7a01224dc..0f94bc56a 100644
--- a/internal/app/tfsec/scanner/scanner.go
+++ b/internal/app/tfsec/scanner/scanner.go
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/tfsec/tfsec/pkg/result"
+	"github.com/tfsec/tfsec/pkg/severity"
 
 	"github.com/tfsec/tfsec/internal/app/tfsec/block"
 	"github.com/tfsec/tfsec/internal/app/tfsec/hclcontext"
@@ -74,6 +75,9 @@ func (scanner *Scanner) Scan(blocks []block.Block) []result.Result {
 						results = append(results, *res)
 					} else if ruleResults != nil {
 						for _, ruleResult := range ruleResults.All() {
+							if ruleResult.Severity == severity.None {
+								ruleResult.Severity = r.DefaultSeverity
+							}
 							if scanner.includeIgnored || (!scanner.checkRangeIgnored(ruleResult.RuleID, ruleResult.Range, checkBlock) && !checkInList(ruleResult.RuleID, scanner.excludedRuleIDs)) {
 								results = append(results, ruleResult)
 							} else {
diff --git a/pkg/severity/severity.go b/pkg/severity/severity.go
index e260ca3ea..759345909 100644
--- a/pkg/severity/severity.go
+++ b/pkg/severity/severity.go
@@ -3,7 +3,7 @@ package severity
 type Severity string
 
 const (
-	None     Severity = "NONE"
+	None     Severity = ""
 	Critical Severity = "CRITICAL"
 	Error    Severity = "ERROR"
 	Warning  Severity = "WARNING"
@@ -11,7 +11,7 @@ const (
 )
 
 var ValidSeverity = []Severity{
-	Error, Warning, Info,
+	Critical, Error, Warning, Info,
 }
 
 func (s *Severity) IsValid() bool {


Test patch:
diff --git a/internal/app/tfsec/test/aws099_test.go b/internal/app/tfsec/test/aws099_test.go
new file mode 100644
index 000000000..e727c9723
--- /dev/null
+++ b/internal/app/tfsec/test/aws099_test.go
@@ -0,0 +1,449 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/tfsec/tfsec/internal/app/tfsec/rules"
+)
+
+func Test_AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege(t *testing.T) {
+
+	var tests = []struct {
+		name                  string
+		source                string
+		mustIncludeResultCode string
+		mustExcludeResultCode string
+	}{
+		{
+			name: "Fails on wildcarded resource (inline)",
+			source: `
+		resource "aws_iam_role_policy" "test_policy" {
+			name = "test_policy"
+			role = aws_iam_role.test_role.id
+
+			policy = data.aws_iam_policy_document.s3_policy.json
+		}
+
+		resource "aws_iam_role" "test_role" {
+			name = "test_role"
+			assume_role_policy = jsonencode({
+				Version = "2012-10-17"
+				Statement = [
+				{
+					Action = "sts:AssumeRole"
+					Effect = "Allow"
+					Sid    = ""
+					Principal = {
+					Service = "ec2.amazonaws.com"
+					}
+				},
+				]
+			})
+		}
+
+		data "aws_iam_policy_document" "s3_policy" {
+			statement {
+				principals {
+					type        = "AWS"
+					identifiers = ["arn:aws:iam::1234567890:root"]
+				}
+				actions   = ["s3:GetObject"]
+				resources = ["*"]
+			}
+		}
+				`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded templated identifier (inline)",
+			source: `
+		resource "aws_iam_role_policy" "test_policy" {
+			name = "test_policy"
+			role = aws_iam_role.test_role.id
+
+			policy = data.aws_iam_policy_document.s3_policy.json
+		}
+
+		resource "aws_iam_role" "test_role" {
+			name = "test_role"
+			assume_role_policy = jsonencode({
+				Version = "2012-10-17"
+				Statement = [
+				{
+					Action = "sts:AssumeRole"
+					Effect = "Allow"
+					Sid    = ""
+					Principal = {
+					Service = "ec2.amazonaws.com"
+					}
+				},
+				]
+			})
+		}
+
+		data "aws_iam_policy_document" "s3_policy" {
+			statement {
+				principals {
+					type        = "AWS"
+					identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:*"]
+				}
+				actions   = ["s3:GetObject"]
+				resources = ["asdasdasd"]
+			}
+		}
+		`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded templated identifier with local (inline)",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = data.aws_iam_policy_document.s3_policy.json
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+			Service = "ec2.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+locals {
+	wildcard = "*"
+}
+
+data "aws_iam_policy_document" "s3_policy" {
+	statement {
+		principals {
+			type        = "AWS"
+			identifiers = ["arn:aws:iam::${local.wildcard}:root"]
+		}
+		actions   = ["s3:GetObject"]
+		resources = ["asdasdasd"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded templated identifier with variable (inline)",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = data.aws_iam_policy_document.s3_policy.json
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+			Service = "ec2.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+variable "wildcard" {
+	default = "*"
+}
+
+data "aws_iam_policy_document" "s3_policy" {
+	statement {
+		principals {
+			type        = "AWS"
+			identifiers = [var.wildcard]
+		}
+		actions   = ["s3:GetObject"]
+		resources = ["asdasdasd"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded action (inline)",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = data.aws_iam_policy_document.s3_policy.json
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+			Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+data "aws_iam_policy_document" "s3_policy" {
+	statement {
+	principals {
+		type        = "AWS"
+		identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
+	}
+	actions   = ["s3:*"]
+	resources = ["something:blah"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded principal (inline)",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = data.aws_iam_policy_document.s3_policy.json
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+				Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+data "aws_iam_policy_document" "s3_policy" {
+	statement {
+	principals {
+		type        = "AWS"
+		identifiers = ["*"]
+	}
+	actions   = ["s3:GetObject"]
+	resources = ["something:exact"]
+	}
+}
+`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded resource (json)",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = <<EOF
+{
+	"Version": "2012-10-17",
+	"Statement": [
+		{
+			"Sid": "ListYourObjects",
+			"Effect": "Allow",
+			"Action": "s3:ListBucket",
+			"Resource": ["arn:aws:s3:::*"],
+			"Principal": {
+				"AWS": "arn:aws:iam::1234567890:root"
+			}
+		}
+	]
+}
+EOF
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+				Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded action (json)",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = <<EOF
+{
+	"Version": "2012-10-17",
+	"Statement": [
+		{
+			"Sid": "ListYourObjects",
+			"Effect": "Allow",
+			"Action": "s3:*",
+			"Resource": ["arn:aws:s3:::bucket-name"],
+			"Principal": {
+				"AWS": "arn:aws:iam::1234567890:root"
+			}
+		}
+	]
+}
+EOF
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+				Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Fails on wildcarded principal (json)",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = <<EOF
+{
+	"Version": "2012-10-17",
+	"Statement": [
+		{
+			"Sid": "ListYourObjects",
+			"Effect": "Allow",
+			"Action": "s3:ListBucket",
+			"Resource": ["arn:aws:s3:::bucket-name"],
+			"Principal": {
+				"AWS": "arn:aws:iam::1234567890:*"
+			}
+		}
+	]
+}
+EOF
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+				Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+`,
+			mustIncludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+		{
+			name: "Passes without wildcard usage",
+			source: `
+resource "aws_iam_role_policy" "test_policy" {
+	name = "test_policy"
+	role = aws_iam_role.test_role.id
+
+	policy = data.aws_iam_policy_document.s3_policy.json
+}
+
+resource "aws_iam_role" "test_role" {
+	name = "test_role"
+	assume_role_policy = jsonencode({
+		Version = "2012-10-17"
+		Statement = [
+		{
+			Action = "sts:AssumeRole"
+			Effect = "Allow"
+			Sid    = ""
+			Principal = {
+				Service = "s3.amazonaws.com"
+			}
+		},
+		]
+	})
+}
+
+data "aws_iam_policy_document" "s3_policy" {
+	statement {
+	principals {
+		type        = "AWS"
+		identifiers = ["aws:arn:21345/blah"]
+	}
+	actions   = ["s3:GetObject"]
+	resources = ["something:exact"]
+	}
+}
+`,
+			mustExcludeResultCode: rules.AWSIAMPolicyShouldUsePrincipleOfLeastPrivilege,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			results := scanHCL(test.source, t)
+			assertCheckCode(t, test.mustIncludeResultCode, test.mustExcludeResultCode, results)
+		})
+	}
+
+}