diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 84643d604b..e71b9562da 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `bedrock_full_access_policy_attached` check for AWS provider [(#10577)](https://github.com/prowler-cloud/prowler/pull/10577) - `iam_role_access_not_stale_to_bedrock` and `iam_user_access_not_stale_to_bedrock` checks for AWS provider [(#10536)](https://github.com/prowler-cloud/prowler/pull/10536) +- `iam_policy_no_wildcard_marketplace_subscribe` and `iam_inline_policy_no_wildcard_marketplace_subscribe` checks for AWS provider [(#10525)](https://github.com/prowler-cloud/prowler/pull/10525) --- diff --git a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json index caa6c92ad4..a147a5ea1f 100644 --- a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json +++ b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json @@ -344,6 +344,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "bedrock_full_access_policy_attached", "ec2_instance_profile_attached", "iam_aws_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/c5_aws.json b/prowler/compliance/aws/c5_aws.json index 249bd1fb8c..70580e5d22 100644 --- a/prowler/compliance/aws/c5_aws.json +++ b/prowler/compliance/aws/c5_aws.json @@ -5130,6 +5130,8 @@ "iam_support_role_created", "iam_user_with_temporary_credentials", "bedrock_api_key_no_administrative_privileges", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "fms_policy_compliant", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", @@ -5200,6 +5202,8 @@ "iam_support_role_created", "iam_user_with_temporary_credentials", "bedrock_api_key_no_administrative_privileges", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "fms_policy_compliant", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/cisa_aws.json b/prowler/compliance/aws/cisa_aws.json index 6dc8edab68..fa8e27a061 100644 --- a/prowler/compliance/aws/cisa_aws.json +++ b/prowler/compliance/aws/cisa_aws.json @@ -183,6 +183,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "elbv2_ssl_listeners", "iam_no_custom_policy_permissive_role_assumption", "iam_aws_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/csa_ccm_4.0_aws.json b/prowler/compliance/aws/csa_ccm_4.0_aws.json index 6b6c7e5e7a..d69ed6fe1d 100644 --- a/prowler/compliance/aws/csa_ccm_4.0_aws.json +++ b/prowler/compliance/aws/csa_ccm_4.0_aws.json @@ -3319,6 +3319,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json index b0418f9d45..c97c407e61 100644 --- a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json +++ b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json @@ -86,6 +86,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_administrator_access_with_mfa", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json index b623f63ba2..76b6f9aa6c 100644 --- a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json @@ -308,6 +308,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_ebs_public_snapshot", "ec2_instance_public_ip", "ec2_instance_imdsv2_enabled", diff --git a/prowler/compliance/aws/ffiec_aws.json b/prowler/compliance/aws/ffiec_aws.json index d1bb22f4cc..697d9ee49f 100644 --- a/prowler/compliance/aws/ffiec_aws.json +++ b/prowler/compliance/aws/ffiec_aws.json @@ -418,6 +418,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_instance_profile_attached", "iam_policy_attached_only_to_group_or_roles", "iam_aws_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/hipaa_aws.json b/prowler/compliance/aws/hipaa_aws.json index 79d8b16f53..34de052b95 100644 --- a/prowler/compliance/aws/hipaa_aws.json +++ b/prowler/compliance/aws/hipaa_aws.json @@ -265,6 +265,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges" diff --git a/prowler/compliance/aws/iso27001_2022_aws.json b/prowler/compliance/aws/iso27001_2022_aws.json index b3cbaabf74..04f7f750d3 100644 --- a/prowler/compliance/aws/iso27001_2022_aws.json +++ b/prowler/compliance/aws/iso27001_2022_aws.json @@ -308,6 +308,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges" @@ -972,6 +974,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "bedrock_full_access_policy_attached", "ec2_instance_profile_attached", "iam_aws_attached_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json index 91271660c9..35b863ae46 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -1389,6 +1389,8 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "bedrock_full_access_policy_attached", "iam_administrator_access_with_mfa", "iam_avoid_root_usage", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json index 05e15f2fbc..3f99680675 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -1389,6 +1389,8 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "bedrock_full_access_policy_attached", "iam_administrator_access_with_mfa", "iam_avoid_root_usage", diff --git a/prowler/compliance/aws/nist_800_171_revision_2_aws.json b/prowler/compliance/aws/nist_800_171_revision_2_aws.json index 7767597546..bd0c84f327 100644 --- a/prowler/compliance/aws/nist_800_171_revision_2_aws.json +++ b/prowler/compliance/aws/nist_800_171_revision_2_aws.json @@ -156,6 +156,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/nist_800_53_revision_4_aws.json b/prowler/compliance/aws/nist_800_53_revision_4_aws.json index 0a38bcb217..24bf67fa31 100644 --- a/prowler/compliance/aws/nist_800_53_revision_4_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_4_aws.json @@ -228,6 +228,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_ebs_public_snapshot", "ec2_instance_public_ip", "ec2_instance_imdsv2_enabled", diff --git a/prowler/compliance/aws/nist_800_53_revision_5_aws.json b/prowler/compliance/aws/nist_800_53_revision_5_aws.json index 5f30bada81..12a55a9359 100644 --- a/prowler/compliance/aws/nist_800_53_revision_5_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_5_aws.json @@ -1186,6 +1186,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "ec2_ebs_public_snapshot", "ec2_instance_public_ip", "ec2_instance_imdsv2_enabled", diff --git a/prowler/compliance/aws/nist_csf_1.1_aws.json b/prowler/compliance/aws/nist_csf_1.1_aws.json index 34e55bde96..cb21e19757 100644 --- a/prowler/compliance/aws/nist_csf_1.1_aws.json +++ b/prowler/compliance/aws/nist_csf_1.1_aws.json @@ -630,6 +630,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_inline_policy_no_administrative_privileges", diff --git a/prowler/compliance/aws/nist_csf_2.0_aws.json b/prowler/compliance/aws/nist_csf_2.0_aws.json index fc8b1b58ca..c6750f1cae 100644 --- a/prowler/compliance/aws/nist_csf_2.0_aws.json +++ b/prowler/compliance/aws/nist_csf_2.0_aws.json @@ -776,6 +776,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "iam_administrator_access_with_mfa", "iam_group_administrator_access_policy", "iam_user_administrator_access_policy", diff --git a/prowler/compliance/aws/soc2_aws.json b/prowler/compliance/aws/soc2_aws.json index 6b6ec6f904..472d20ab75 100644 --- a/prowler/compliance/aws/soc2_aws.json +++ b/prowler/compliance/aws/soc2_aws.json @@ -205,6 +205,8 @@ } ], "Checks": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe", + "iam_policy_no_wildcard_marketplace_subscribe", "bedrock_full_access_policy_attached", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/__init__.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.metadata.json b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.metadata.json new file mode 100644 index 0000000000..692390afce --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "aws", + "CheckID": "iam_inline_policy_no_wildcard_marketplace_subscribe", + "CheckTitle": "Inline IAM policy does not allow 'aws-marketplace:Subscribe' on all resources", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsIamPolicy", + "ResourceGroup": "IAM", + "Description": "**IAM inline policies** are analyzed to identify statements that grant `aws-marketplace:Subscribe` on all resources (`*`). This action controls the ability to subscribe to AWS Marketplace products, including **Amazon Bedrock foundation models**, and should be scoped to specific product ARNs to enforce least privilege.", + "Risk": "Granting `aws-marketplace:Subscribe` on all resources via inline policies allows subscribing to any Marketplace product, including expensive Bedrock foundation models, leading to uncontrolled costs, shadow AI usage, and compliance violations.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "```yaml\nResources:\n ExampleRole:\n Type: AWS::IAM::Role\n Properties:\n Policies:\n - PolicyName: scoped-marketplace-subscribe\n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Action:\n - aws-marketplace:Subscribe # FIX: scope to specific product ARNs\n Resource:\n - arn:aws:aws-marketplace::123456789012:product/approved-product-id\n```", + "Other": "1. In the AWS Console, open IAM and go to Users, Roles, or Groups where the inline policy is attached\n2. Select the entity, go to the Permissions tab, and open the inline policy\n3. Click Edit policy and switch to the JSON editor\n4. Replace \"Resource\": \"*\" with specific, approved AWS Marketplace product ARNs\n5. Save changes and re-run the check to confirm it passes", + "Terraform": "```hcl\nresource \"aws_iam_role_policy\" \"scoped_marketplace_subscribe\" {\n name = \"scoped-marketplace-subscribe\"\n role = aws_iam_role.example.id\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Action = [\"aws-marketplace:Subscribe\"]\n Resource = [\"arn:aws:aws-marketplace::123456789012:product/approved-product-id\"] # FIX: scope to specific products\n }]\n })\n}\n```" + }, + "Recommendation": { + "Text": "Replace `Resource: \"*\"` with specific, approved AWS Marketplace product ARNs. Prefer managed policies over inline and apply the principle of least privilege to `aws-marketplace:Subscribe` permissions to prevent unauthorized subscriptions to costly Bedrock models and other Marketplace products.", + "Url": "https://hub.prowler.com/check/iam_inline_policy_no_wildcard_marketplace_subscribe" + } + }, + "Categories": [ + "gen-ai", + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "iam_policy_no_wildcard_marketplace_subscribe" + ], + "Notes": "This check only evaluates IAM inline policies. See iam_policy_no_wildcard_marketplace_subscribe for the customer-managed policy variant." +} diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.py new file mode 100644 index 0000000000..38abdf3984 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe.py @@ -0,0 +1,33 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import ( + policy_allows_marketplace_subscribe_on_all_resources, +) + + +class iam_inline_policy_no_wildcard_marketplace_subscribe(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + for policy in iam_client.policies.values(): + if policy.type == "Inline": + report = Check_Report_AWS(metadata=self.metadata(), resource=policy) + report.region = iam_client.region + report.resource_id = f"{policy.entity}/{policy.name}" + report.status = "PASS" + + resource_type_str = report.resource_arn.split(":")[-1].split("/")[0] + resource_attached = report.resource_arn.split("/")[-1] + + report.status_extended = f"Inline policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow 'aws-marketplace:Subscribe' on all resources." + + if ( + policy.document + and policy_allows_marketplace_subscribe_on_all_resources( + policy.document + ) + ): + report.status = "FAIL" + report.status_extended = f"Inline policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows 'aws-marketplace:Subscribe' on all resources." + + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/__init__.py b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.metadata.json b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.metadata.json new file mode 100644 index 0000000000..22c7a44cfe --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.metadata.json @@ -0,0 +1,42 @@ +{ + "Provider": "aws", + "CheckID": "iam_policy_no_wildcard_marketplace_subscribe", + "CheckTitle": "Custom IAM policy does not allow 'aws-marketplace:Subscribe' on all resources", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsIamPolicy", + "ResourceGroup": "IAM", + "Description": "**Customer-managed IAM policies** are examined for statements that grant `aws-marketplace:Subscribe` on all resources (`*`). This action controls the ability to subscribe to AWS Marketplace products, including **Amazon Bedrock foundation models**, and should be scoped to specific product ARNs to enforce least privilege.", + "Risk": "Granting `aws-marketplace:Subscribe` on all resources allows subscribing to any Marketplace product, including expensive Bedrock foundation models, leading to uncontrolled costs, shadow AI usage, and compliance violations from unapproved deployments.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html", + "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" + ], + "Remediation": { + "Code": { + "CLI": "aws iam create-policy-version --policy-arn --policy-document '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"aws-marketplace:Subscribe\"],\"Resource\":\"arn:aws:aws-marketplace:::product/\"}]}' --set-as-default", + "NativeIaC": "```yaml\nResources:\n ScopedMarketplacePolicy:\n Type: AWS::IAM::ManagedPolicy\n Properties:\n PolicyDocument:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Action:\n - aws-marketplace:Subscribe # FIX: scope to specific product ARNs\n Resource:\n - arn:aws:aws-marketplace::123456789012:product/approved-product-id\n```", + "Other": "1. In the AWS Console, open IAM > Policies\n2. Find the custom policy that allows aws-marketplace:Subscribe on Resource: *\n3. Click Edit and switch to the JSON editor\n4. Replace \"Resource\": \"*\" with specific, approved AWS Marketplace product ARNs\n5. Save changes and re-run the check to confirm it passes", + "Terraform": "```hcl\nresource \"aws_iam_policy\" \"scoped_marketplace_subscribe\" {\n name = \"scoped-marketplace-subscribe\"\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Action = [\"aws-marketplace:Subscribe\"]\n Resource = [\"arn:aws:aws-marketplace::123456789012:product/approved-product-id\"] # FIX: scope to specific products\n }]\n })\n}\n```" + }, + "Recommendation": { + "Text": "Replace `Resource: \"*\"` with specific, approved AWS Marketplace product ARNs. Apply the principle of least privilege to `aws-marketplace:Subscribe` permissions to prevent unauthorized subscriptions to costly Bedrock models and other Marketplace products.", + "Url": "https://hub.prowler.com/check/iam_policy_no_wildcard_marketplace_subscribe" + } + }, + "Categories": [ + "gen-ai", + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [ + "iam_inline_policy_no_wildcard_marketplace_subscribe" + ], + "Notes": "This check only evaluates customer-managed IAM policies. AWS managed policies are maintained by AWS and cannot be modified. See iam_inline_policy_no_wildcard_marketplace_subscribe for the inline policy variant." +} diff --git a/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py new file mode 100644 index 0000000000..fe0e740a5e --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe.py @@ -0,0 +1,28 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import ( + policy_allows_marketplace_subscribe_on_all_resources, +) + + +class iam_policy_no_wildcard_marketplace_subscribe(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + for policy in iam_client.policies.values(): + if policy.type == "Custom": + report = Check_Report_AWS(metadata=self.metadata(), resource=policy) + report.region = iam_client.region + report.status = "PASS" + report.status_extended = f"Custom Policy {policy.name} does not allow 'aws-marketplace:Subscribe' on all resources." + + if ( + policy.document + and policy_allows_marketplace_subscribe_on_all_resources( + policy.document + ) + ): + report.status = "FAIL" + report.status_extended = f"Custom Policy {policy.name} allows 'aws-marketplace:Subscribe' on all resources." + + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/iam/lib/policy.py b/prowler/providers/aws/services/iam/lib/policy.py index fa3352bd6c..562e270df6 100644 --- a/prowler/providers/aws/services/iam/lib/policy.py +++ b/prowler/providers/aws/services/iam/lib/policy.py @@ -997,6 +997,93 @@ def is_codebuild_using_allowed_github_org( return False, None +def policy_allows_marketplace_subscribe_on_all_resources( + policy_document: dict, +) -> bool: + """Check if a policy document can allow aws-marketplace:Subscribe on Resource:*. + + Inspects statements with Resource ``*`` for Allow effects that grant + ``aws-marketplace:Subscribe`` via ``Action`` or ``NotAction`` (wildcard + patterns expanded through ``expand_actions``). Unconditional Deny + statements on Resource ``*`` (via either ``Action`` or ``NotAction``) + covering the same action take precedence. Conditional Deny statements + are not treated as global cancellation because the condition scope is + request-dependent and is not evaluated here. Conditional Allow + statements are still treated as potentially allowing access on + ``Resource:*``, since the wildcard scope remains risky even when + gated by a condition. + + Args: + policy_document: The IAM policy document to analyse. + + Returns: + True if the policy can allow aws-marketplace:Subscribe on all + resources, False otherwise. + """ + if not policy_document or "Statement" not in policy_document: + return False + + target_actions = set( + expand_actions( + "aws-marketplace:Subscribe", + InvalidActionHandling.REMOVE, + ) + ) + if not target_actions: + target_actions = {"aws-marketplace:Subscribe"} + + statements = policy_document.get("Statement", []) + if not isinstance(statements, list): + statements = [statements] + + allowed_on_all = set() + denied_on_all = set() + all_aws_actions = None + + for statement in statements: + effect = statement.get("Effect", "") + if not isinstance(effect, str): + continue + effect_lower = effect.strip().lower() + if effect_lower not in ("allow", "deny"): + continue + + resources = statement.get("Resource", []) + if isinstance(resources, str): + resources = [resources] + if "*" not in resources: + continue + + if effect_lower == "deny" and "Condition" in statement: + continue + + statement_actions = set() + action_patterns = _get_patterns_from_standard_value(statement.get("Action")) + for pattern in action_patterns: + statement_actions.update( + expand_actions(pattern, InvalidActionHandling.REMOVE) + ) + + not_action_patterns = _get_patterns_from_standard_value( + statement.get("NotAction") + ) + if not_action_patterns: + if all_aws_actions is None: + all_aws_actions = set(expand_actions("*", InvalidActionHandling.REMOVE)) + exclusions = set() + for pattern in not_action_patterns: + exclusions.update(expand_actions(pattern, InvalidActionHandling.REMOVE)) + statement_actions.update(all_aws_actions.difference(exclusions)) + + if effect_lower == "allow": + allowed_on_all.update(statement_actions) + else: + denied_on_all.update(statement_actions) + + effective = allowed_on_all.difference(denied_on_all) + return bool(target_actions & effective) + + def has_codebuild_trusted_principal(trust_policy: dict) -> bool: """ Returns True if the trust policy allows codebuild.amazonaws.com as a trusted principal, otherwise False. diff --git a/tests/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe_test.py b/tests/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe_test.py new file mode 100644 index 0000000000..8f9c57a1e1 --- /dev/null +++ b/tests/providers/aws/services/iam/iam_inline_policy_no_wildcard_marketplace_subscribe/iam_inline_policy_no_wildcard_marketplace_subscribe_test.py @@ -0,0 +1,461 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.iam.iam_service import IAM +from tests.providers.aws.utils import ( + ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +CHECK_MODULE_PATH = "prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe" + + +class Test_iam_inline_policy_no_wildcard_marketplace_subscribe: + @mock_aws + def test_inline_policy_allows_marketplace_subscribe_on_all_resources(self): + """FAIL: Inline policy allows aws-marketplace:Subscribe on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_subscribe_wildcard" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_allows_marketplace_wildcard_action(self): + """FAIL: Inline policy allows aws-marketplace:* on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_all_actions" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:*", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_scoped_resource(self): + """PASS: Inline policy allows aws-marketplace:Subscribe on a specific resource.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_subscribe_scoped" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/example-product-id", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_unrelated_action(self): + """PASS: Inline policy allows an unrelated action on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "ec2_full_access" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:*", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_deny_overrides_allow(self): + """PASS: Allow + Deny on * for same action — Deny wins.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_subscribe_denied" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_deny_specific_resource_does_not_override(self): + """FAIL: Deny on specific resource does not negate Allow on *.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "deny_specific_only" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/blocked", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Inline policy {policy_name} attached to role {role_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == f"{role_name}/{policy_name}" + assert result[0].resource_arn == role_arn + assert result[0].region == "eu-west-1" + + @mock_aws + def test_inline_policy_not_action_allows_marketplace_subscribe(self): + """FAIL: Allow + NotAction excludes only unrelated actions so Subscribe is granted.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_not_action" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "ec2:Describe*", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "FAIL" + assert result[0].resource_arn == role_arn + + @mock_aws + def test_inline_policy_not_action_excludes_marketplace_subscribe(self): + """PASS: Allow + NotAction that excludes aws-marketplace:Subscribe does not grant it.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "marketplace_not_action_excluded" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert result[0].status == "PASS" + assert result[0].resource_arn == role_arn + + @mock_aws + def test_no_inline_policies(self): + """No findings when there are no inline policies.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + f"{CHECK_MODULE_PATH}.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_no_wildcard_marketplace_subscribe.iam_inline_policy_no_wildcard_marketplace_subscribe import ( + iam_inline_policy_no_wildcard_marketplace_subscribe, + ) + + check = iam_inline_policy_no_wildcard_marketplace_subscribe() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py b/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py new file mode 100644 index 0000000000..b667e7dbfb --- /dev/null +++ b/tests/providers/aws/services/iam/iam_policy_no_wildcard_marketplace_subscribe/iam_policy_no_wildcard_marketplace_subscribe_test.py @@ -0,0 +1,509 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.iam.iam_service import IAM +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +CHECK_MODULE_PATH = "prowler.providers.aws.services.iam.iam_policy_no_wildcard_marketplace_subscribe.iam_policy_no_wildcard_marketplace_subscribe" + + +class Test_iam_policy_no_wildcard_marketplace_subscribe: + @mock_aws + def test_policy_allows_marketplace_subscribe_on_all_resources(self): + """FAIL: Policy explicitly allows aws-marketplace:Subscribe on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_subscribe_wildcard" + 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"] + + 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[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_allows_marketplace_wildcard_action_on_all_resources(self): + """FAIL: Policy allows aws-marketplace:* on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_all_actions" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_allows_full_wildcard_on_all_resources(self): + """FAIL: Policy allows * (all actions) on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "full_admin_access" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_allows_marketplace_subscribe_on_specific_resource(self): + """PASS: Policy allows aws-marketplace:Subscribe on a specific resource ARN.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_subscribe_scoped" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/example-product-id", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_unrelated_action_on_all_resources(self): + """PASS: Policy allows an unrelated action on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "ec2_full_access" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ec2:*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_marketplace_subscribe_denied_on_all_resources(self): + """PASS: Allow + Deny on * for same action — Deny wins.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_subscribe_denied" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} does not allow 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_deny_specific_resource_does_not_override_allow_all(self): + """FAIL: Deny on a specific resource does not negate Allow on Resource:*.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "deny_specific_resource" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "arn:aws:aws-marketplace::123456789012:product/blocked-product", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_deny_unrelated_action_does_not_override_allow(self): + """FAIL: Deny for a different action does not override the Allow for Subscribe.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "deny_unrelated_action" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "ec2:TerminateInstances", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_case_insensitive_action_matching(self): + """FAIL: Mixed-case action still matches aws-marketplace:Subscribe.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "mixed_case_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"] + + 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[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Custom Policy {policy_name} allows 'aws-marketplace:Subscribe' on all resources." + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == arn + assert result[0].region == "us-east-1" + + @mock_aws + def test_policy_not_action_allows_marketplace_subscribe(self): + """FAIL: Allow + NotAction excludes only unrelated actions so Subscribe is granted.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_not_action" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "ec2:Describe*", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "FAIL" + assert result[0].resource_arn == arn + + @mock_aws + def test_policy_not_action_excludes_marketplace_subscribe(self): + """PASS: Allow + NotAction that excludes aws-marketplace:Subscribe does not grant it.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam") + policy_name = "marketplace_not_action_excluded" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["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 result[0].status == "PASS" + assert result[0].resource_arn == arn + + @mock_aws + def test_no_custom_policies(self): + """No findings when there are no custom IAM policies.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + 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) == 0 diff --git a/tests/providers/aws/services/iam/lib/policy_test.py b/tests/providers/aws/services/iam/lib/policy_test.py index 50cca4cbad..bf64ff9211 100644 --- a/tests/providers/aws/services/iam/lib/policy_test.py +++ b/tests/providers/aws/services/iam/lib/policy_test.py @@ -15,6 +15,7 @@ from prowler.providers.aws.services.iam.lib.policy import ( is_condition_restricting_from_private_ip, is_condition_restricting_to_trusted_ips, is_policy_public, + policy_allows_marketplace_subscribe_on_all_resources, ) TRUSTED_AWS_ACCOUNT_NUMBER = "123456789012" @@ -137,6 +138,47 @@ class Test_Policy: assert result == {"s3:GetObject", "s3:ListBucket"} assert "s3:PutObject" not in result + def test_policy_allows_marketplace_subscribe_conditional_deny_does_not_cancel(self): + """Conditional deny should not globally cancel a wildcard marketplace allow.""" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + "Condition": {"StringEquals": {"aws:RequestedRegion": "us-east-1"}}, + }, + ], + } + + assert policy_allows_marketplace_subscribe_on_all_resources(policy) + + def test_policy_allows_marketplace_subscribe_unconditional_deny_cancels(self): + """Unconditional deny on Resource:* should cancel the wildcard marketplace allow.""" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": "aws-marketplace:Subscribe", + "Resource": "*", + }, + ], + } + + assert not policy_allows_marketplace_subscribe_on_all_resources(policy) + # Test lowercase context key name --> aws def test_condition_parser_string_equals_aws_SourceAccount_list(self): condition_statement = {