chore(aws): skip unattached IAM policies unless --scan-unused-services (#11150)

This commit is contained in:
Hugo Pereira Brito
2026-05-14 08:10:20 +01:00
committed by GitHub
parent 0abbb7fc59
commit 739be07077
12 changed files with 417 additions and 0 deletions
@@ -56,6 +56,18 @@ Prowler scans only attached security groups to report vulnerabilities in activel
- `ec2_networkacl_allow_ingress_X_port`
#### AWS Identity and Access Management (IAM)
Customer-managed IAM policies that are not attached to any user, group, or role grant no effective permissions until a principal is bound to them. Prowler treats such policies as dormant by default and skips the content-evaluation checks below when `--scan-unused-services` is not set. Enable the flag to surface findings on unattached policies as well.
- `iam_policy_allows_privilege_escalation`
- `iam_policy_no_full_access_to_cloudtrail`
- `iam_policy_no_full_access_to_kms`
- `iam_policy_no_wildcard_marketplace_subscribe`
- `iam_no_custom_policy_permissive_role_assumption`
The dedicated `iam_customer_unattached_policy_no_administrative_privileges` check still inspects unattached policies regardless of the flag, since its purpose is to highlight dormant administrator privileges.
#### AWS Glue
AWS Glue best practices recommend encrypting metadata and connection passwords in Data Catalogs.
+1
View File
@@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🔄 Changed
- `entra_emergency_access_exclusion` check for M365 provider now scopes the exclusion requirement to enabled Conditional Access policies with a `Block` grant control instead of every enabled policy, focusing on the lockout-relevant policy set [(#10849)](https://github.com/prowler-cloud/prowler/pull/10849)
- AWS IAM customer-managed policy checks no longer emit `FAIL` on unattached policies unless `--scan-unused-services` is enabled [(#11150)](https://github.com/prowler-cloud/prowler/pull/11150)
---
@@ -16,6 +16,8 @@ class iam_no_custom_policy_permissive_role_assumption(Check):
for policy in iam_client.policies.values():
# Check only custom policies
if policy.type == "Custom":
if not policy.attached and not iam_client.provider.scan_unused_services:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
report.status = "PASS"
@@ -11,6 +11,8 @@ class iam_policy_allows_privilege_escalation(Check):
for policy in iam_client.policies.values():
if policy.type == "Custom":
if not policy.attached and not iam_client.provider.scan_unused_services:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
report.status = "PASS"
@@ -11,6 +11,8 @@ class iam_policy_no_full_access_to_cloudtrail(Check):
for policy in iam_client.policies.values():
# Check only custom policies
if policy.type == "Custom":
if not policy.attached and not iam_client.provider.scan_unused_services:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
report.status = "PASS"
@@ -11,6 +11,8 @@ class iam_policy_no_full_access_to_kms(Check):
for policy in iam_client.policies.values():
# Check only custom policies
if policy.type == "Custom":
if not policy.attached and not iam_client.provider.scan_unused_services:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
report.status = "PASS"
@@ -10,6 +10,8 @@ class iam_policy_no_wildcard_marketplace_subscribe(Check):
findings = []
for policy in iam_client.policies.values():
if policy.type == "Custom":
if not policy.attached and not iam_client.provider.scan_unused_services:
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=policy)
report.region = iam_client.region
report.status = "PASS"
@@ -408,3 +408,83 @@ class Test_iam_no_custom_policy_permissive_role_assumption:
assert search(
"allows permissive STS Role assumption", result[0].status_extended
)
@mock_aws
def test_unattached_policy_skipped_when_scan_unused_services_disabled(self):
iam_client = client("iam")
policy_name = "unattached_permissive_assume_role"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*"},
],
}
iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document)
)
from prowler.providers.aws.services.iam.iam_service import IAM
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption import (
iam_no_custom_policy_permissive_role_assumption,
)
check = iam_no_custom_policy_permissive_role_assumption()
result = check.execute()
assert result == []
@mock_aws
def test_attached_policy_fails_when_scan_unused_services_disabled(self):
iam_client = client("iam")
user_name = "test_user_assume_role"
policy_name = "attached_permissive_assume_role"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*"},
],
}
arn = iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document)
)["Policy"]["Arn"]
iam_client.create_user(UserName=user_name)
iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn)
from prowler.providers.aws.services.iam.iam_service import IAM
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_no_custom_policy_permissive_role_assumption.iam_no_custom_policy_permissive_role_assumption import (
iam_no_custom_policy_permissive_role_assumption,
)
check = iam_no_custom_policy_permissive_role_assumption()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_arn == arn
assert search(
"allows permissive STS Role assumption", result[0].status_extended
)
@@ -1261,3 +1261,86 @@ class Test_iam_policy_allows_privilege_escalation:
permissions
]:
assert search(permission, finding.status_extended)
@mock_aws
def test_unattached_policy_skipped_when_scan_unused_services_disabled(self):
iam_client = client("iam", region_name=AWS_REGION_US_EAST_1)
policy_name = "unattached_privilege_escalation"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "iam:CreateAccessKey", "Resource": "*"},
],
}
iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document)
)
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
from prowler.providers.aws.services.iam.iam_service import IAM
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client",
new=IAM(aws_provider),
),
):
from prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import (
iam_policy_allows_privilege_escalation,
)
check = iam_policy_allows_privilege_escalation()
result = check.execute()
assert result == []
@mock_aws
def test_attached_policy_fails_when_scan_unused_services_disabled(self):
iam_client = client("iam", region_name=AWS_REGION_US_EAST_1)
user_name = "test_user_privesc"
policy_name = "attached_privilege_escalation"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "iam:CreateAccessKey", "Resource": "*"},
],
}
policy_arn = iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document)
)["Policy"]["Arn"]
iam_client.create_user(UserName=user_name)
iam_client.attach_user_policy(UserName=user_name, PolicyArn=policy_arn)
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
from prowler.providers.aws.services.iam.iam_service import IAM
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client",
new=IAM(aws_provider),
),
):
from prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import (
iam_policy_allows_privilege_escalation,
)
check = iam_policy_allows_privilege_escalation()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_arn == policy_arn
assert search(
f"Custom Policy {policy_arn} allows privilege escalation",
result[0].status_extended,
)
@@ -207,3 +207,78 @@ class Test_iam_policy_no_full_access_to_cloudtrail:
assert result[0].resource_id == "policy_no_cloudtrail_full_no_actions"
assert result[0].resource_arn == arn
assert result[0].region == "us-east-1"
@mock_aws
def test_unattached_policy_skipped_when_scan_unused_services_disabled(self):
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
iam_client = client("iam", region_name=AWS_REGION_US_EAST_1)
policy_name = "unattached_cloudtrail_full"
policy_document_full_access = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "cloudtrail:*", "Resource": "*"},
],
}
iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access)
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail import (
iam_policy_no_full_access_to_cloudtrail,
)
check = iam_policy_no_full_access_to_cloudtrail()
result = check.execute()
assert result == []
@mock_aws
def test_attached_policy_fails_when_scan_unused_services_disabled(self):
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
iam_client = client("iam", region_name=AWS_REGION_US_EAST_1)
user_name = "test_user_cloudtrail"
policy_name = "attached_cloudtrail_full"
policy_document_full_access = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "cloudtrail:*", "Resource": "*"},
],
}
arn = iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access)
)["Policy"]["Arn"]
iam_client.create_user(UserName=user_name)
iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_cloudtrail.iam_policy_no_full_access_to_cloudtrail import (
iam_policy_no_full_access_to_cloudtrail,
)
check = iam_policy_no_full_access_to_cloudtrail()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Custom Policy {policy_name} allows 'cloudtrail:*' privileges."
)
assert result[0].resource_arn == arn
@@ -329,6 +329,81 @@ class Test_iam_policy_no_full_access_to_kms_with_unicode:
assert result[0].resource_arn == arn
assert result[0].region == "us-east-1"
@mock_aws
def test_unattached_policy_skipped_when_scan_unused_services_disabled(self):
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
iam_client = client("iam")
policy_name = "unattached_kms_full"
policy_document_full_access = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "kms:*", "Resource": "*"},
],
}
iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access)
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms import (
iam_policy_no_full_access_to_kms,
)
check = iam_policy_no_full_access_to_kms()
result = check.execute()
assert result == []
@mock_aws
def test_attached_policy_fails_when_scan_unused_services_disabled(self):
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
iam_client = client("iam")
user_name = "test_user_kms"
policy_name = "attached_kms_full"
policy_document_full_access = {
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "kms:*", "Resource": "*"},
],
}
arn = iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document_full_access)
)["Policy"]["Arn"]
iam_client.create_user(UserName=user_name)
iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_policy_no_full_access_to_kms.iam_policy_no_full_access_to_kms import (
iam_policy_no_full_access_to_kms,
)
check = iam_policy_no_full_access_to_kms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Custom Policy {policy_name} allows 'kms:*' privileges."
)
assert result[0].resource_arn == arn
@mock_aws
def test_policy_full_access_and_full_deny_to_kms(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
@@ -507,3 +507,84 @@ class Test_iam_policy_no_wildcard_marketplace_subscribe:
check = iam_policy_no_wildcard_marketplace_subscribe()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_unattached_policy_skipped_when_scan_unused_services_disabled(self):
"""No FAIL for an unattached risky policy when --scan-unused-services is off."""
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
iam_client = client("iam")
policy_name = "unattached_marketplace_subscribe"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "aws-marketplace:Subscribe",
"Resource": "*",
},
],
}
iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document)
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
f"{CHECK_MODULE_PATH}.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import (
iam_policy_no_wildcard_marketplace_subscribe,
)
check = iam_policy_no_wildcard_marketplace_subscribe()
result = check.execute()
assert result == []
@mock_aws
def test_attached_policy_fails_when_scan_unused_services_disabled(self):
"""Attached risky policy still FAILs when --scan-unused-services is off."""
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1], scan_unused_services=False
)
iam_client = client("iam")
user_name = "test_user_marketplace"
policy_name = "attached_marketplace_subscribe"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "aws-marketplace:Subscribe",
"Resource": "*",
},
],
}
arn = iam_client.create_policy(
PolicyName=policy_name, PolicyDocument=dumps(policy_document)
)["Policy"]["Arn"]
iam_client.create_user(UserName=user_name)
iam_client.attach_user_policy(UserName=user_name, PolicyArn=arn)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
f"{CHECK_MODULE_PATH}.iam_client",
new=IAM(aws_provider),
):
from prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe import (
iam_policy_no_wildcard_marketplace_subscribe,
)
check = iam_policy_no_wildcard_marketplace_subscribe()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_arn == arn