mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
Merge branch 'master' into prwlr-7751-github-app-authentication-incorrectly-handles-key-parameter-and-environment-variables
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
|
||||
+34
@@ -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."
|
||||
}
|
||||
+96
@@ -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}-<region>",
|
||||
"SageMaker": f"sagemaker-<region>-{s3_client.provider.identity.account}",
|
||||
# "CloudFormation": "cf-templates-.*-<region>",
|
||||
"EMR": f"aws-emr-studio-{s3_client.provider.identity.account}-<region>",
|
||||
"CodeStar": f"aws-codestar-<region>-{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("<region>", 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>", 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
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
+210
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AWSProviderBadge,
|
||||
AzureProviderBadge,
|
||||
GCPProviderBadge,
|
||||
GitHubProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
} from "../icons/providers-badge";
|
||||
@@ -52,3 +53,12 @@ export const CustomProviderInputKubernetes = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomProviderInputGitHub = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<GitHubProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">GitHub</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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: <CustomProviderInputAWS />,
|
||||
component: <CustomProviderInputAWS />,
|
||||
},
|
||||
{
|
||||
key: "gcp",
|
||||
gcp: {
|
||||
label: "Google Cloud Platform",
|
||||
value: <CustomProviderInputGCP />,
|
||||
component: <CustomProviderInputGCP />,
|
||||
},
|
||||
{
|
||||
key: "azure",
|
||||
azure: {
|
||||
label: "Microsoft Azure",
|
||||
value: <CustomProviderInputAzure />,
|
||||
component: <CustomProviderInputAzure />,
|
||||
},
|
||||
{
|
||||
key: "m365",
|
||||
m365: {
|
||||
label: "Microsoft 365",
|
||||
value: <CustomProviderInputM365 />,
|
||||
component: <CustomProviderInputM365 />,
|
||||
},
|
||||
{
|
||||
key: "kubernetes",
|
||||
kubernetes: {
|
||||
label: "Kubernetes",
|
||||
value: <CustomProviderInputKubernetes />,
|
||||
component: <CustomProviderInputKubernetes />,
|
||||
},
|
||||
];
|
||||
github: {
|
||||
label: "GitHub",
|
||||
component: <CustomProviderInputGitHub />,
|
||||
},
|
||||
};
|
||||
|
||||
const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({
|
||||
key: providerType,
|
||||
label: providerDisplayData[providerType].label,
|
||||
value: providerDisplayData[providerType].component,
|
||||
}));
|
||||
|
||||
export const CustomSelectProvider: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 <AWSProviderBadge width={30} height={30} />;
|
||||
@@ -33,18 +35,26 @@ export const ProvidersOverview = ({
|
||||
return <GCPProviderBadge width={30} height={30} />;
|
||||
case "kubernetes":
|
||||
return <KS8ProviderBadge width={30} height={30} />;
|
||||
case "github":
|
||||
return <GitHubProviderBadge width={30} height={30} />;
|
||||
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<ProviderType, string> = {
|
||||
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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user