From cfd401928194b3cb323d8165a848a068ddfd9a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Tue, 3 Sep 2024 13:47:43 +0200 Subject: [PATCH] fix(aws): raise ArgumentTypeError for parser (#4921) --- prowler/providers/aws/aws_provider.py | 4 +- .../providers/aws/exceptions/exceptions.py | 100 +++++++----------- .../providers/aws/lib/arguments/arguments.py | 44 ++++++-- tests/lib/cli/parser_test.py | 7 +- tests/providers/aws/aws_provider_test.py | 11 +- 5 files changed, 89 insertions(+), 77 deletions(-) diff --git a/prowler/providers/aws/aws_provider.py b/prowler/providers/aws/aws_provider.py index c0d6b3e2fe..cfb300cc81 100644 --- a/prowler/providers/aws/aws_provider.py +++ b/prowler/providers/aws/aws_provider.py @@ -1245,7 +1245,7 @@ def validate_session_duration(duration: int) -> int: # Since the range(i,j) goes from i to j-1 we have to j+1 if duration not in range(900, 43201): raise AWSArgumentTypeValidationError( - original_exception="Session Duration must be between 900 and 43200 seconds.", + message="Session Duration must be between 900 and 43200 seconds.", file=os.path.basename(__file__), ) else: @@ -1274,5 +1274,5 @@ def validate_role_session_name(session_name) -> str: else: raise AWSArgumentTypeValidationError( file=os.path.basename(__file__), - original_exception="Role Session Name must be between 2 and 64 characters and may contain alphanumeric characters, periods, hyphens, and underscores.", + message="Role Session Name must be between 2 and 64 characters and may contain alphanumeric characters, periods, hyphens, and underscores.", ) diff --git a/prowler/providers/aws/exceptions/exceptions.py b/prowler/providers/aws/exceptions/exceptions.py index 4a54f4470e..dafd4709c2 100644 --- a/prowler/providers/aws/exceptions/exceptions.py +++ b/prowler/providers/aws/exceptions/exceptions.py @@ -8,188 +8,170 @@ class AWSBaseException(ProwlerException): (1902, "AWSClientError"): { "message": "AWS ClientError occurred", "remediation": "Check your AWS client configuration and permissions.", - "file": "{file}", - "provider": "AWS", }, (1903, "AWSProfileNotFoundError"): { "message": "AWS Profile not found", "remediation": "Ensure the AWS profile is correctly configured, please visit https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html", - "file": "{file}", - "provider": "AWS", }, (1904, "AWSNoCredentialsError"): { "message": "No AWS credentials found", "remediation": "Verify that AWS credentials are properly set up, please visit https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/aws/authentication/ and https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-configure.html", - "file": "{file}", - "provider": "AWS", }, (1905, "AWSArgumentTypeValidationError"): { "message": "AWS argument type validation error", "remediation": "Check the provided argument types specific to AWS and ensure they meet the required format. For session duration check: https://docs.aws.amazon.com/singlesignon/latest/userguide/howtosessionduration.html and for role session name check: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-session-name", - "file": "{file}", - "provider": "AWS", }, (1906, "AWSSetUpSessionError"): { "message": "AWS session setup error", "remediation": "Check the AWS session setup and ensure it is properly configured, please visit https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html and check if the provided profile has the necessary permissions.", - "file": "{file}", - "provider": "AWS", }, (1907, "AWSIAMRoleARNRegionNotEmtpy"): { "message": "AWS IAM Role ARN region is not empty", "remediation": "Check the AWS IAM Role ARN region and ensure it is empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.", - "file": "{file}", - "provider": "AWS", }, (1908, "AWSIAMRoleARNPartitionEmpty"): { "message": "AWS IAM Role ARN partition is empty", "remediation": "Check the AWS IAM Role ARN partition and ensure it is not empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.", - "file": "{file}", - "provider": "AWS", }, (1909, "AWSIAMRoleARNMissingFields"): { "message": "AWS IAM Role ARN missing fields", "remediation": "Check the AWS IAM Role ARN and ensure all required fields are present, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.", - "file": "{file}", - "provider": "AWS", }, (1910, "AWSIAMRoleARNServiceNotIAMnorSTS"): { "message": "AWS IAM Role ARN service is not IAM nor STS", "remediation": "Check the AWS IAM Role ARN service and ensure it is either IAM or STS, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.", - "file": "{file}", - "provider": "AWS", }, (1911, "AWSIAMRoleARNInvalidAccountID"): { "message": "AWS IAM Role ARN account ID is invalid", "remediation": "Check the AWS IAM Role ARN account ID and ensure it is a valid 12-digit number, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.", - "file": "{file}", - "provider": "AWS", }, (1912, "AWSIAMRoleARNInvalidResourceType"): { "message": "AWS IAM Role ARN resource type is invalid", "remediation": "Check the AWS IAM Role ARN resource type and ensure it is valid, resources types are: role, user, assumed-role, root, federated-user, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.", - "file": "{file}", - "provider": "AWS", }, (1913, "AWSIAMRoleARNEmptyResource"): { "message": "AWS IAM Role ARN resource is empty", "remediation": "Check the AWS IAM Role ARN resource and ensure it is not empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.", - "file": "{file}", - "provider": "AWS", }, (1914, "AWSAssumeRoleError"): { "message": "AWS assume role error", - "remediation": "Check the AWS assume role configuration and ensure it is properly set up, please visithttps://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/aws/role-assumption/ and https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-session-name", - "file": "{file}", - "provider": "AWS", + "remediation": "Check the AWS assume role configuration and ensure it is properly set up, please visit https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/aws/role-assumption/ and https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-session-name", }, } - def __init__(self, code, provider="AWS", file=None, original_exception=None): + def __init__(self, code, file=None, original_exception=None, message=None): error_info = self.AWS_ERROR_CODES.get((code, self.__class__.__name__)) - super().__init__(code, provider, file, original_exception, error_info) + if message: + error_info["message"] = message + super().__init__( + code, + provider="AWS", + file=file, + original_exception=original_exception, + error_info=error_info, + ) class AWSCredentialsError(AWSBaseException): """Base class for AWS credentials errors.""" - def __init__(self, code, provider="AWS", file=None, original_exception=None): - super().__init__(code, provider, file, original_exception) + def __init__(self, code, file=None, original_exception=None, message=None): + super().__init__(code, file, original_exception, message) class AWSClientError(AWSCredentialsError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1902, provider="AWS", file=file, original_exception=original_exception + 1902, file=file, original_exception=original_exception, message=message ) class AWSProfileNotFoundError(AWSCredentialsError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1903, provider="AWS", file=file, original_exception=original_exception + 1903, file=file, original_exception=original_exception, message=message ) class AWSNoCredentialsError(AWSCredentialsError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1904, provider="AWS", file=file, original_exception=original_exception + 1904, file=file, original_exception=original_exception, message=message ) class AWSArgumentTypeValidationError(AWSBaseException): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1905, provider="AWS", file=file, original_exception=original_exception + 1905, file=file, original_exception=original_exception, message=message ) class AWSSetUpSessionError(AWSBaseException): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1906, provider="AWS", file=file, original_exception=original_exception + 1906, file=file, original_exception=original_exception, message=message ) class AWSRoleArnError(AWSBaseException): """Base class for AWS role ARN errors.""" - def __init__(self, code, provider="AWS", file=None, original_exception=None): - super().__init__(code, provider, file, original_exception) + def __init__(self, code, file=None, original_exception=None, message=None): + super().__init__(code, file, original_exception, message) class AWSIAMRoleARNRegionNotEmtpy(AWSRoleArnError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1907, provider="AWS", file=file, original_exception=original_exception + 1907, file=file, original_exception=original_exception, message=message ) class AWSIAMRoleARNPartitionEmpty(AWSRoleArnError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1908, provider="AWS", file=file, original_exception=original_exception + 1908, file=file, original_exception=original_exception, message=message ) class AWSIAMRoleARNMissingFields(AWSRoleArnError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1909, provider="AWS", file=file, original_exception=original_exception + 1909, file=file, original_exception=original_exception, message=message ) class AWSIAMRoleARNServiceNotIAMnorSTS(AWSRoleArnError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1910, provider="AWS", file=file, original_exception=original_exception + 1910, file=file, original_exception=original_exception, message=message ) class AWSIAMRoleARNInvalidAccountID(AWSRoleArnError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1911, provider="AWS", file=file, original_exception=original_exception + 1911, file=file, original_exception=original_exception, message=message ) class AWSIAMRoleARNInvalidResourceType(AWSRoleArnError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1912, provider="AWS", file=file, original_exception=original_exception + 1912, file=file, original_exception=original_exception, message=message ) class AWSIAMRoleARNEmptyResource(AWSRoleArnError): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1913, provider="AWS", file=file, original_exception=original_exception + 1913, file=file, original_exception=original_exception, message=message ) class AWSAssumeRoleError(AWSBaseException): - def __init__(self, file=None, original_exception=None): + def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 1914, provider="AWS", file=file, original_exception=original_exception + 1914, file=file, original_exception=original_exception, message=message ) diff --git a/prowler/providers/aws/lib/arguments/arguments.py b/prowler/providers/aws/lib/arguments/arguments.py index d4a03e54bd..8cac4b6a1a 100644 --- a/prowler/providers/aws/lib/arguments/arguments.py +++ b/prowler/providers/aws/lib/arguments/arguments.py @@ -1,11 +1,7 @@ from argparse import ArgumentTypeError, Namespace -from re import search +from re import fullmatch, search -from prowler.providers.aws.aws_provider import ( - get_aws_available_regions, - validate_role_session_name, - validate_session_duration, -) +from prowler.providers.aws.aws_provider import get_aws_available_regions from prowler.providers.aws.config import ROLE_SESSION_NAME from prowler.providers.aws.lib.arn.arn import arn_type @@ -172,6 +168,42 @@ def init_parser(self): ) +def validate_session_duration(session_duration: int) -> int: + """validate_session_duration validates that the input session_duration is valid""" + duration = int(session_duration) + # Since the range(i,j) goes from i to j-1 we have to j+1 + if duration not in range(900, 43201): + raise ArgumentTypeError( + "Session duration must be between 900 and 43200 seconds" + ) + else: + return duration + + +def validate_role_session_name(session_name) -> str: + """ + Validates that the role session name is valid. + + Args: + session_name (str): The role session name to be validated. + + Returns: + str: The validated role session name. + + Raises: + ArgumentTypeError: If the role session name is invalid. + + Documentation: + - AWS STS AssumeRole API: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html + """ + if fullmatch(r"[\w+=,.@-]{2,64}", session_name): + return session_name + else: + raise ArgumentTypeError( + "Role session name must be between 2 and 64 characters long and may contain alphanumeric characters, hyphens, underscores, plus signs, equal signs, commas, periods, at signs, and tildes." + ) + + def validate_arguments(arguments: Namespace) -> tuple[bool, str]: """validate_arguments returns {True, "} if the provider arguments passed are valid and can be used together. It performs an extra validation, specific for the AWS provider, apart from the argparse lib.""" diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py index 1798c4f809..89781f2223 100644 --- a/tests/lib/cli/parser_test.py +++ b/tests/lib/cli/parser_test.py @@ -6,7 +6,6 @@ from mock import patch from prowler.lib.cli.parser import ProwlerArgumentParser from prowler.providers.aws.config import ROLE_SESSION_NAME -from prowler.providers.aws.exceptions.exceptions import AWSArgumentTypeValidationError from prowler.providers.aws.lib.arguments.arguments import ( validate_bucket, validate_role_session_name, @@ -1326,13 +1325,13 @@ class Test_Parser: "role-name?", ] for role_name in bad_role_names: - with pytest.raises(AWSArgumentTypeValidationError) as argument_error: + with pytest.raises(ArgumentTypeError) as argument_error: validate_role_session_name(role_name) - assert argument_error.type == AWSArgumentTypeValidationError + assert argument_error.type == ArgumentTypeError assert ( argument_error.value.args[0] - == "[1905] AWS argument type validation error - Role Session Name must be between 2 and 64 characters and may contain alphanumeric characters, periods, hyphens, and underscores." + == "Role session name must be between 2 and 64 characters long and may contain alphanumeric characters, hyphens, underscores, plus signs, equal signs, commas, periods, at signs, and tildes." ) def test_validate_role_session_name_valid_role_names(self): diff --git a/tests/providers/aws/aws_provider_test.py b/tests/providers/aws/aws_provider_test.py index 40810fbe06..528a524ac0 100644 --- a/tests/providers/aws/aws_provider_test.py +++ b/tests/providers/aws/aws_provider_test.py @@ -1291,9 +1291,8 @@ aws: ) # No profile to avoid ProfileNotFound error assert exception.type == AWSNoCredentialsError - assert ( - "AWSNoCredentialsError[1904]: No AWS credentials found - Unable to locate credentials" - in str(exception.value) + assert "AWSNoCredentialsError[1904]: No AWS credentials found" in str( + exception.value ) @mock_aws @@ -1334,7 +1333,7 @@ aws: assert exception.type == AWSArgumentTypeValidationError assert ( exception.value.args[0] - == "[1905] AWS argument type validation error - Session Duration must be between 900 and 43200 seconds." + == "[1905] Session Duration must be between 900 and 43200 seconds." ) @mock_aws @@ -1354,7 +1353,7 @@ aws: assert isinstance(connection.error, AWSArgumentTypeValidationError) assert ( connection.error.args[0] - == "[1905] AWS argument type validation error - Session Duration must be between 900 and 43200 seconds." + == "[1905] Session Duration must be between 900 and 43200 seconds." ) @mock_aws @@ -1370,7 +1369,7 @@ aws: assert exception.type == AWSArgumentTypeValidationError assert ( exception.value.args[0] - == "[1905] AWS argument type validation error - Role Session Name must be between 2 and 64 characters and may contain alphanumeric characters, periods, hyphens, and underscores." + == "[1905] Role Session Name must be between 2 and 64 characters and may contain alphanumeric characters, periods, hyphens, and underscores." ) @mock_aws