diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 170c276727..e5c4aa3eff 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `vm_desired_sku_size` check for Azure provider [(#8191)](https://github.com/prowler-cloud/prowler/pull/8191) - `vm_scaleset_not_empty` check for Azure provider [(#8192)](https://github.com/prowler-cloud/prowler/pull/8192) - GitHub repository and organization scoping support with `--repository/respositories` and `--organization/organizations` flags [(#8329)](https://github.com/prowler-cloud/prowler/pull/8329) +- `s3_bucket_shadow_resource_vulnerability` check for AWS provider [(#8398)](https://github.com/prowler-cloud/prowler/pull/8398) ### Changed - Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347) @@ -24,6 +25,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add missing audit evidence for controls 1.1.4 and 2.5.5 for ISMS-P compliance. [(#8386)](https://github.com/prowler-cloud/prowler/pull/8386) - Use the correct @staticmethod decorator for `set_identity` and `set_session_config` methods in AwsProvider [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056) - Use the correct default value for `role_session_name` and `session_duration` in AwsSetUpSession [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056) +- Use the correct default value for `role_session_name` and `session_duration` in S3 [(#8417)](https://github.com/prowler-cloud/prowler/pull/8417) - GitHub App authentication inconsistency where `--github-app-key` and `--github-app-key-path` were incorrectly aliased to the same parameter, causing confusion between file paths and key content [(#8422)](https://github.com/prowler-cloud/prowler/pull/8422) --- @@ -38,6 +40,8 @@ All notable changes to the **Prowler SDK** are documented in this file. - Use default tenant domain instead of first domain in list for Azure and M365 providers [(#8402)](https://github.com/prowler-cloud/prowler/pull/8402) - Avoid multiple module error calls in M365 provider [(#8353)](https://github.com/prowler-cloud/prowler/pull/8353) - Tweaks from Prowler ThreatScore in order to handle the correct reqs [(#8401)](https://github.com/prowler-cloud/prowler/pull/8401) +- Make `setup_assumed_session` static for the AWS provider [(#8419)](https://github.com/prowler-cloud/prowler/pull/8419) + --- ## [v5.9.2] (Prowler v5.9.2) diff --git a/prowler/providers/aws/aws_provider.py b/prowler/providers/aws/aws_provider.py index 14dfa6ec4e..9893f249bc 100644 --- a/prowler/providers/aws/aws_provider.py +++ b/prowler/providers/aws/aws_provider.py @@ -254,7 +254,7 @@ class AwsProvider(Provider): ) # Assume the IAM Role logger.info(f"Assuming role: {assumed_role_information.role_arn.arn}") - assumed_role_credentials = self.assume_role( + assumed_role_credentials = AwsProvider.assume_role( self._session.current_session, assumed_role_information, ) @@ -267,8 +267,10 @@ class AwsProvider(Provider): self._assumed_role_configuration = assumed_role_configuration # Store a new current session using the assumed IAM Role - self._session.current_session = self.setup_assumed_session( - assumed_role_configuration.credentials + self._session.current_session = AwsProvider.setup_assumed_session( + self._identity, + assumed_role_configuration, + self._session, ) logger.info("Audit session is the new session created assuming an IAM Role") @@ -316,8 +318,10 @@ class AwsProvider(Provider): credentials=organizations_assumed_role_credentials, ) # Get a new session using the AWS Organizations IAM Role assumed - aws_organizations_session = self.setup_assumed_session( - organizations_assumed_role_configuration.credentials + aws_organizations_session = AwsProvider.setup_assumed_session( + self._identity, + organizations_assumed_role_configuration, + self._session, ) logger.info( "Generated new session for to get the AWS Organizations metadata" @@ -576,9 +580,11 @@ class AwsProvider(Provider): file=pathlib.Path(__file__).name, ) + @staticmethod def setup_assumed_session( - self, - assumed_role_credentials: AWSCredentials, + identity: AWSIdentityInfo, + assumed_role_configuration: AWSAssumeRoleConfiguration, + session: AWSSession, ) -> Session: """ Sets up an assumed session using the provided assumed role credentials. @@ -588,7 +594,9 @@ class AwsProvider(Provider): refreshing of the assumed role credentials. Args: + identity (AWSIdentityInfo): The identity information. assumed_role_credentials (AWSCredentials): The assumed role credentials. + session (AWSSession): The AWS provider session. Returns: Session: The assumed session. @@ -607,20 +615,22 @@ class AwsProvider(Provider): # that needs to be a method without arguments that retrieves a new set of fresh credentials # assuming the role again. assumed_refreshable_credentials = RefreshableCredentials( - access_key=assumed_role_credentials.aws_access_key_id, - secret_key=assumed_role_credentials.aws_secret_access_key, - token=assumed_role_credentials.aws_session_token, - expiry_time=assumed_role_credentials.expiration, - refresh_using=self.refresh_credentials, + access_key=assumed_role_configuration.credentials.aws_access_key_id, + secret_key=assumed_role_configuration.credentials.aws_secret_access_key, + token=assumed_role_configuration.credentials.aws_session_token, + expiry_time=assumed_role_configuration.credentials.expiration, + refresh_using=lambda: AwsProvider.refresh_credentials( + assumed_role_configuration, session + ), method="sts-assume-role", ) # Here we need the botocore session since it needs to use refreshable credentials assumed_session = BotocoreSession() assumed_session._credentials = assumed_refreshable_credentials - assumed_session.set_config_variable("region", self._identity.profile_region) + assumed_session.set_config_variable("region", identity.profile_region) return Session( - profile_name=self._identity.profile, + profile_name=identity.profile, botocore_session=assumed_session, ) except Exception as error: @@ -630,7 +640,10 @@ class AwsProvider(Provider): raise error # TODO: maybe this can be improved with botocore.credentials.DeferredRefreshableCredentials https://stackoverflow.com/a/75576540 - def refresh_credentials(self) -> dict: + @staticmethod + def refresh_credentials( + assumed_role_configuration: AWSAssumeRoleConfiguration, session: AWSSession + ) -> dict: """ Refresh credentials method using AWS STS Assume Role. @@ -640,7 +653,7 @@ class AwsProvider(Provider): logger.info("Refreshing assumed credentials...") # Since this method does not accept arguments, we need to get the original_session and the assumed role credentials - current_credentials = self._assumed_role_configuration.credentials + current_credentials = assumed_role_configuration.credentials refreshed_credentials = { "access_key": current_credentials.aws_access_key_id, "secret_key": current_credentials.aws_secret_access_key, @@ -655,8 +668,8 @@ class AwsProvider(Provider): if datetime.fromisoformat(refreshed_credentials["expiry_time"]) <= datetime.now( get_localzone() ): - assume_role_response = self.assume_role( - self._session.original_session, self._assumed_role_configuration.info + assume_role_response = AwsProvider.assume_role( + session.original_session, assumed_role_configuration.info ) refreshed_credentials = dict( # Keys of the dict has to be the same as those that are being searched in the parent class diff --git a/prowler/providers/aws/lib/s3/s3.py b/prowler/providers/aws/lib/s3/s3.py index f90a6844b7..f195a98663 100644 --- a/prowler/providers/aws/lib/s3/s3.py +++ b/prowler/providers/aws/lib/s3/s3.py @@ -76,9 +76,9 @@ class S3: output_directory: str, session: AWSSession = None, role_arn: str = None, - session_duration: int = None, + session_duration: int = 3600, external_id: str = None, - role_session_name: str = None, + role_session_name: str = ROLE_SESSION_NAME, mfa: bool = None, profile: str = None, aws_access_key_id: str = None, diff --git a/prowler/providers/aws/lib/session/aws_set_up_session.py b/prowler/providers/aws/lib/session/aws_set_up_session.py index d62822c57c..9246c0a9eb 100644 --- a/prowler/providers/aws/lib/session/aws_set_up_session.py +++ b/prowler/providers/aws/lib/session/aws_set_up_session.py @@ -132,7 +132,7 @@ class AwsSetUpSession: ) # Assume the IAM Role logger.info(f"Assuming role: {assumed_role_information.role_arn.arn}") - assumed_role_credentials = self.assume_role( + assumed_role_credentials = AwsProvider.assume_role( self._session.current_session, assumed_role_information, ) @@ -145,8 +145,10 @@ class AwsSetUpSession: self._assumed_role_configuration = assumed_role_configuration # Store a new current session using the assumed IAM Role - self._session.current_session = self.setup_assumed_session( - assumed_role_configuration.credentials + self._session.current_session = AwsProvider.setup_assumed_session( + self._identity, + self._assumed_role_configuration, + self._session, ) logger.info("Audit session is the new session created assuming an IAM Role") @@ -192,10 +194,8 @@ def validate_arguments( "If a role ARN is provided, a session duration, an external ID, and a role session name are required." ) else: - if session_duration or external_id or role_session_name: - raise ValueError( - "If a session duration, an external ID, or a role session name is provided, a role ARN is required." - ) + if external_id: + raise ValueError("If an external ID is provided, a role ARN is required.") if not profile and not aws_access_key_id and not aws_secret_access_key: raise ValueError( "If no role ARN is provided, a profile, an AWS access key ID, or an AWS secret access key is required." diff --git a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/__init__.py b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json new file mode 100644 index 0000000000..4c9717f59e --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "s3_bucket_shadow_resource_vulnerability", + "CheckTitle": "Check for S3 buckets vulnerable to Shadow Resource Hijacking (Bucket Monopoly)", + "CheckType": [ + "" + ], + "ServiceName": "s3", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "Severity": "high", + "ResourceType": "AwsS3Bucket", + "Description": "Checks for S3 buckets with predictable names that could be hijacked by an attacker before legitimate use, leading to data leakage or other security breaches.", + "Risk": "An attacker can pre-create S3 buckets with predictable names used by various AWS services. When a legitimate user's service attempts to use that bucket, it may inadvertently write sensitive data to the attacker-controlled bucket, leading to information disclosure, denial of service, or even remote code execution.", + "RelatedUrl": "https://www.aquasec.com/blog/bucket-monopoly-breaching-aws-accounts-through-shadow-resources/", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "Manually verify the ownership of any flagged S3 buckets. If a bucket is not owned by your account, investigate its origin and purpose. If it is not a legitimate resource, you should avoid using services that may interact with it.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that all S3 buckets associated with your AWS account are owned by your account. Be cautious of services that create buckets with predictable names. Whenever possible, pre-create these buckets in all regions to prevent hijacking.", + "Url": "https://www.aquasec.com/blog/bucket-monopoly-breaching-aws-accounts-through-shadow-resources/" + } + }, + "Categories": [ + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check is based on the 'Bucket Monopoly' vulnerability disclosed by Aqua Security." +} diff --git a/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py new file mode 100644 index 0000000000..f18855da30 --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability.py @@ -0,0 +1,96 @@ +import re + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.lib.logger import logger +from prowler.providers.aws.services.s3.s3_client import s3_client + + +class s3_bucket_shadow_resource_vulnerability(Check): + def execute(self): + findings = [] + # Predictable bucket name patterns from the research article + # These patterns are used by AWS services and can be claimed by attackers + predictable_patterns = { + "Glue": f"aws-glue-assets-{s3_client.provider.identity.account}-", + "SageMaker": f"sagemaker--{s3_client.provider.identity.account}", + # "CloudFormation": "cf-templates-.*-", + "EMR": f"aws-emr-studio-{s3_client.provider.identity.account}-", + "CodeStar": f"aws-codestar--{s3_client.provider.identity.account}", + # Add other patterns here as they are discovered + } + + # Track buckets we've already reported to avoid duplicates + reported_buckets = set() + + # First, check buckets in the current account + for bucket in s3_client.buckets.values(): + report = Check_Report_AWS(self.metadata(), resource=bucket) + report.region = bucket.region + report.resource_id = bucket.name + report.resource_arn = bucket.arn + report.resource_tags = bucket.tags + report.status = "PASS" + report.status_extended = ( + f"S3 bucket {bucket.name} is not a known shadow resource." + ) + + # Check if this bucket matches any predictable pattern + for service, pattern_format in predictable_patterns.items(): + pattern = pattern_format.replace("", bucket.region) + + if re.match(pattern, bucket.name): + if bucket.owner_id != s3_client.audited_canonical_id: + report.status = "FAIL" + report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource and it is owned by another account ({bucket.owner_id})." + else: + report.status = "PASS" + report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource but it is correctly owned by the audited account." + break + findings.append(report) + reported_buckets.add(bucket.name) + + # Now check for shadow resources in other accounts by testing predictable patterns + # We'll test different regions to see if shadow resources exist + regions_to_test = ( + s3_client.provider.identity.audited_regions + or s3_client.regional_clients.keys() + ) + + for region in regions_to_test: + for service, pattern_format in predictable_patterns.items(): + # Generate bucket name for this region + bucket_name = pattern_format.replace("", region) + + # Skip if we've already reported this bucket + if bucket_name in reported_buckets: + continue + + logger.info( + f"Checking if shadow resource bucket {bucket_name} exists in other accounts" + ) + # Check if this bucket exists in another account + if s3_client._head_bucket(bucket_name): + # Create a virtual bucket object for reporting + virtual_bucket = type( + "obj", + (object,), + { + "name": bucket_name, + "region": region, + "arn": f"arn:{s3_client.audited_partition}:s3:::{bucket_name}", + "tags": [], + }, + )() + + report = Check_Report_AWS(self.metadata(), resource=virtual_bucket) + report.region = region + report.resource_id = bucket_name + report.resource_arn = virtual_bucket.arn + report.resource_tags = [] + report.status = "FAIL" + report.status_extended = f"S3 bucket {bucket_name} for service {service} is a known shadow resource that exists and is owned by another account." + + findings.append(report) + reported_buckets.add(bucket_name) + + return findings diff --git a/prowler/providers/aws/services/s3/s3_service.py b/prowler/providers/aws/services/s3/s3_service.py index 3dec3fc441..00605fbe6e 100644 --- a/prowler/providers/aws/services/s3/s3_service.py +++ b/prowler/providers/aws/services/s3/s3_service.py @@ -16,6 +16,7 @@ class S3(AWSService): self.account_arn_template = f"arn:{self.audited_partition}:s3:{self.region}:{self.audited_account}:account" self.regions_with_buckets = [] self.buckets = {} + self.audited_canonical_id = "" self._list_buckets(provider) self.__threading_call__(self._get_bucket_versioning, self.buckets.values()) self.__threading_call__(self._get_bucket_logging, self.buckets.values()) @@ -40,6 +41,7 @@ class S3(AWSService): logger.info("S3 - Listing buckets...") try: list_buckets = self.client.list_buckets() + self.audited_canonical_id = list_buckets["Owner"]["ID"] for bucket in list_buckets["Buckets"]: try: bucket_region = self.client.get_bucket_location( @@ -237,9 +239,10 @@ class S3(AWSService): logger.info("S3 - Get buckets acl...") try: regional_client = self.regional_clients[bucket.region] + acl = regional_client.get_bucket_acl(Bucket=bucket.name) + bucket.owner_id = acl["Owner"]["ID"] grantees = [] - acl_grants = regional_client.get_bucket_acl(Bucket=bucket.name)["Grants"] - for grant in acl_grants: + for grant in acl["Grants"]: grantee = ACL_Grantee(type=grant["Grantee"]["Type"]) if "DisplayName" in grant["Grantee"]: grantee.display_name = grant["Grantee"]["DisplayName"] @@ -683,6 +686,8 @@ class ReplicationRule(BaseModel): class Bucket(BaseModel): arn: str name: str + owner_id: Optional[str] + owner: Optional[str] versioning: bool = False logging: bool = False public_access_block: Optional[PublicAccessBlock] diff --git a/tests/providers/aws/aws_provider_test.py b/tests/providers/aws/aws_provider_test.py index 5153729551..c14218f987 100644 --- a/tests/providers/aws/aws_provider_test.py +++ b/tests/providers/aws/aws_provider_test.py @@ -2008,7 +2008,12 @@ aws: ).isoformat(), } - assert aws_provider.refresh_credentials() == refreshed_credentials + assert ( + AwsProvider.refresh_credentials( + aws_provider._assumed_role_configuration, aws_provider._session + ) + == refreshed_credentials + ) @mock_aws def test_refresh_credentials_after_expiration(self): @@ -2025,7 +2030,9 @@ aws: current_credentials = aws_provider._assumed_role_configuration.credentials # Refresh credentials - refreshed_credentials = aws_provider.refresh_credentials() + refreshed_credentials = AwsProvider.refresh_credentials( + aws_provider._assumed_role_configuration, aws_provider._session + ) # Assert that the refreshed credentials are different access_key = refreshed_credentials.get("access_key") diff --git a/tests/providers/aws/lib/s3/s3_test.py b/tests/providers/aws/lib/s3/s3_test.py index d6797bb1b9..c32b81460d 100644 --- a/tests/providers/aws/lib/s3/s3_test.py +++ b/tests/providers/aws/lib/s3/s3_test.py @@ -413,5 +413,16 @@ class TestS3: ) assert ( str(e.value) - == "If a session duration, an external ID, or a role session name is provided, a role ARN is required." + == "If no role ARN is provided, a profile, an AWS access key ID, or an AWS secret access key is required." ) + + @mock_aws + def test_init_without_session_and_role_arn_but_profile(self): + with pytest.raises(ValueError) as e: + S3( + session=None, + bucket_name=S3_BUCKET_NAME, + output_directory=CURRENT_DIRECTORY, + external_id="1234567890", + ) + assert str(e.value) == "If an external ID is provided, a role ARN is required." diff --git a/tests/providers/aws/lib/security_hub/security_hub_test.py b/tests/providers/aws/lib/security_hub/security_hub_test.py index 1095667090..3cb8119316 100644 --- a/tests/providers/aws/lib/security_hub/security_hub_test.py +++ b/tests/providers/aws/lib/security_hub/security_hub_test.py @@ -564,5 +564,5 @@ class TestSecurityHub: assert ( str(e.value) - == "If a session duration, an external ID, or a role session name is provided, a role ARN is required." + == "If no role ARN is provided, a profile, an AWS access key ID, or an AWS secret access key is required." ) diff --git a/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py b/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py new file mode 100644 index 0000000000..5013ff1cdd --- /dev/null +++ b/tests/providers/aws/services/s3/s3_bucket_shadow_resource_vulnerability/s3_bucket_shadow_resource_vulnerability_test.py @@ -0,0 +1,210 @@ +from unittest import mock + +from moto import mock_aws + +from prowler.providers.aws.services.s3.s3_service import Bucket +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_s3_bucket_shadow_resource_vulnerability: + @mock_aws + def test_no_buckets(self): + s3_client = mock.MagicMock + s3_client.buckets = {} + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + s3_client = mock.MagicMock + s3_client.provider = aws_provider + s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability.s3_client", + new=s3_client, + ), + ): + from prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability import ( + s3_bucket_shadow_resource_vulnerability, + ) + + check = s3_bucket_shadow_resource_vulnerability() + result = check.execute() + assert len(result) == 0 + + @mock_aws + def test_bucket_owned_by_account(self): + s3_client = mock.MagicMock + bucket_name = f"sagemaker-{AWS_REGION_US_EAST_1}-{AWS_ACCOUNT_NUMBER}" + s3_client.audited_account_id = AWS_ACCOUNT_NUMBER + s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER + s3_client.buckets = { + bucket_name: Bucket( + name=bucket_name, + arn=f"arn:aws:s3:::{bucket_name}", + region=AWS_REGION_US_EAST_1, + owner_id=AWS_ACCOUNT_NUMBER, + ) + } + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + s3_client = mock.MagicMock + s3_client.provider = aws_provider + s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability.s3_client", + new=s3_client, + ), + ): + from prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability import ( + s3_bucket_shadow_resource_vulnerability, + ) + + check = s3_bucket_shadow_resource_vulnerability() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + "is correctly owned by the audited account" in result[0].status_extended + ) + + @mock_aws + def test_bucket_not_predictable(self): + s3_client = mock.MagicMock + bucket_name = "my-non-predictable-bucket" + s3_client.audited_account_id = AWS_ACCOUNT_NUMBER + s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER + s3_client.buckets = { + bucket_name: Bucket( + name=bucket_name, + arn=f"arn:aws:s3:::{bucket_name}", + region=AWS_REGION_US_EAST_1, + owner_id=AWS_ACCOUNT_NUMBER, + ) + } + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + s3_client = mock.MagicMock + s3_client.provider = aws_provider + s3_client._head_bucket = mock.MagicMock(return_value=False) + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability.s3_client", + new=s3_client, + ), + ): + from prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability import ( + s3_bucket_shadow_resource_vulnerability, + ) + + check = s3_bucket_shadow_resource_vulnerability() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert "is not a known shadow resource" in result[0].status_extended + + @mock_aws + def test_shadow_resource_in_other_account(self): + # Mock S3 client with no buckets in current account + s3_client = mock.MagicMock() + s3_client.buckets = {} + s3_client.audited_account_id = AWS_ACCOUNT_NUMBER + s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER + s3_client.audited_partition = "aws" + + # Mock regional clients - this is what the check uses to determine regions to test + s3_client.regional_clients = { + "us-east-1": mock.MagicMock(), + "us-west-2": mock.MagicMock(), + "eu-west-1": mock.MagicMock(), + } + + # Define the shadow resources we want to simulate + shadow_resources = [ + f"aws-glue-assets-{AWS_ACCOUNT_NUMBER}-us-west-2", + f"sagemaker-us-east-1-{AWS_ACCOUNT_NUMBER}", + f"aws-emr-studio-{AWS_ACCOUNT_NUMBER}-eu-west-1", + ] + + # Mock the _head_bucket method to simulate finding shadow resources + def mock_head_bucket(bucket_name): + return bucket_name in shadow_resources + + s3_client._head_bucket = mock_head_bucket + + # Mock provider with multiple regions to test + aws_provider = set_mocked_aws_provider(["us-east-1", "us-west-2", "eu-west-1"]) + aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + aws_provider.identity.account = AWS_ACCOUNT_NUMBER + s3_client.provider = aws_provider + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability.s3_client", + new=s3_client, + ), + ): + from prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability import ( + s3_bucket_shadow_resource_vulnerability, + ) + + check = s3_bucket_shadow_resource_vulnerability() + result = check.execute() + + # Should find shadow resources + assert len(result) >= 3 + + # Check if we found all expected shadow resources + found_services = set() + for finding in result: + if ( + finding.status == "FAIL" + and "shadow resource" in finding.status_extended + ): + if ( + "aws-glue-assets" in finding.status_extended + and "Glue" in finding.status_extended + ): + found_services.add("Glue") + assert "us-west-2" in finding.status_extended + elif ( + "sagemaker" in finding.status_extended + and "SageMaker" in finding.status_extended + ): + found_services.add("SageMaker") + assert "us-east-1" in finding.status_extended + elif ( + "aws-emr-studio" in finding.status_extended + and "EMR" in finding.status_extended + ): + found_services.add("EMR") + assert "eu-west-1" in finding.status_extended + + # Verify common attributes + assert "owned by another account" in finding.status_extended + + # Verify we found all expected services + expected_services = {"Glue", "SageMaker", "EMR"} + assert found_services == expected_services diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index b99eabf12b..b419cec6c5 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Rename `Memberships` to `Organization` in the sidebar [(#8415)](https://github.com/prowler-cloud/prowler/pull/8415) - Removed `Browse all resources` from the sidebar, sidebar now shows a single `Resources` entry [(#8418)](https://github.com/prowler-cloud/prowler/pull/8418) +- Removed `Misconfigurations` from the `Top Failed Findings` section in the sidebar [(#8426)](https://github.com/prowler-cloud/prowler/pull/8426) ___ ## [v1.9.3] (Prowler v5.9.3) diff --git a/ui/components/filters/custom-provider-inputs.tsx b/ui/components/filters/custom-provider-inputs.tsx index d16d0d5afd..77f56c2946 100644 --- a/ui/components/filters/custom-provider-inputs.tsx +++ b/ui/components/filters/custom-provider-inputs.tsx @@ -4,6 +4,7 @@ import { AWSProviderBadge, AzureProviderBadge, GCPProviderBadge, + GitHubProviderBadge, KS8ProviderBadge, M365ProviderBadge, } from "../icons/providers-badge"; @@ -52,3 +53,12 @@ export const CustomProviderInputKubernetes = () => { ); }; + +export const CustomProviderInputGitHub = () => { + return ( +
+ +

GitHub

+
+ ); +}; diff --git a/ui/components/filters/custom-select-provider.tsx b/ui/components/filters/custom-select-provider.tsx index db614ea494..0d56c3b27d 100644 --- a/ui/components/filters/custom-select-provider.tsx +++ b/ui/components/filters/custom-select-provider.tsx @@ -4,41 +4,52 @@ import { Select, SelectItem } from "@nextui-org/react"; import { useRouter, useSearchParams } from "next/navigation"; import React, { useCallback, useMemo } from "react"; +import { PROVIDER_TYPES, ProviderType } from "@/types/providers"; + import { CustomProviderInputAWS, CustomProviderInputAzure, CustomProviderInputGCP, + CustomProviderInputGitHub, CustomProviderInputKubernetes, CustomProviderInputM365, } from "./custom-provider-inputs"; -const dataInputsProvider = [ - { - key: "aws", +const providerDisplayData: Record< + ProviderType, + { label: string; component: React.ReactElement } +> = { + aws: { label: "Amazon Web Services", - value: , + component: , }, - { - key: "gcp", + gcp: { label: "Google Cloud Platform", - value: , + component: , }, - { - key: "azure", + azure: { label: "Microsoft Azure", - value: , + component: , }, - { - key: "m365", + m365: { label: "Microsoft 365", - value: , + component: , }, - { - key: "kubernetes", + kubernetes: { label: "Kubernetes", - value: , + component: , }, -]; + github: { + label: "GitHub", + component: , + }, +}; + +const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({ + key: providerType, + label: providerDisplayData[providerType].label, + value: providerDisplayData[providerType].component, +})); export const CustomSelectProvider: React.FC = () => { const router = useRouter(); diff --git a/ui/components/filters/data-filters.ts b/ui/components/filters/data-filters.ts index 92fa8d7523..6a57abc96f 100644 --- a/ui/components/filters/data-filters.ts +++ b/ui/components/filters/data-filters.ts @@ -1,4 +1,5 @@ import { FilterType } from "@/types/filters"; +import { PROVIDER_TYPES } from "@/types/providers"; export const filterProviders = [ { @@ -13,7 +14,7 @@ export const filterScans = [ { key: "provider_type__in", labelCheckboxGroup: "Cloud Provider", - values: ["aws", "azure", "m365", "gcp", "kubernetes"], + values: [...PROVIDER_TYPES], index: 0, }, { @@ -55,7 +56,7 @@ export const filterFindings = [ { key: FilterType.PROVIDER_TYPE, labelCheckboxGroup: "Cloud Provider", - values: ["aws", "azure", "m365", "gcp", "kubernetes"], + values: [...PROVIDER_TYPES], index: 5, }, { diff --git a/ui/components/overview/provider-overview/provider-overview.tsx b/ui/components/overview/provider-overview/provider-overview.tsx index 41d518eb8a..5fe8a07070 100644 --- a/ui/components/overview/provider-overview/provider-overview.tsx +++ b/ui/components/overview/provider-overview/provider-overview.tsx @@ -7,11 +7,13 @@ import { AWSProviderBadge, AzureProviderBadge, GCPProviderBadge, + GitHubProviderBadge, KS8ProviderBadge, M365ProviderBadge, } from "@/components/icons/providers-badge"; import { CustomButton } from "@/components/ui/custom/custom-button"; import { ProviderOverviewProps } from "@/types"; +import { PROVIDER_TYPES, ProviderType } from "@/types/providers"; export const ProvidersOverview = ({ providersOverview, @@ -21,7 +23,7 @@ export const ProvidersOverview = ({ const calculatePassingPercentage = (pass: number, total: number) => total > 0 ? ((pass / total) * 100).toFixed(2) : "0.00"; - const renderProviderBadge = (providerId: string) => { + const renderProviderBadge = (providerId: ProviderType) => { switch (providerId) { case "aws": return ; @@ -33,18 +35,26 @@ export const ProvidersOverview = ({ return ; case "kubernetes": return ; + case "github": + return ; default: return null; } }; - const providers = [ - { id: "aws", name: "AWS" }, - { id: "azure", name: "Azure" }, - { id: "m365", name: "M365" }, - { id: "gcp", name: "GCP" }, - { id: "kubernetes", name: "Kubernetes" }, - ]; + const providerDisplayNames: Record = { + aws: "AWS", + azure: "Azure", + m365: "M365", + gcp: "GCP", + kubernetes: "Kubernetes", + github: "GitHub", + }; + + const providers = PROVIDER_TYPES.map((providerType) => ({ + id: providerType, + name: providerDisplayNames[providerType], + })); if (!providersOverview || !Array.isArray(providersOverview.data)) { return ( diff --git a/ui/lib/menu-list.ts b/ui/lib/menu-list.ts index 3b0b54ef4c..237540ff2d 100644 --- a/ui/lib/menu-list.ts +++ b/ui/lib/menu-list.ts @@ -1,7 +1,6 @@ "use client"; import { - AlertCircle, Bookmark, CloudCog, Cog, @@ -79,11 +78,6 @@ export const getMenuList = (pathname: string): GroupProps[] => { label: "Top failed findings", icon: Bookmark, submenus: [ - { - href: "/findings?filter[status__in]=FAIL&sort=severity,-inserted_at", - label: "Misconfigurations", - icon: AlertCircle, - }, { href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=aws%2Cazure%2Cgcp%2Ckubernetes&filter[service__in]=iam%2Crbac&sort=-inserted_at", label: "IAM Issues",