From d1053375b757ca4e6e1c1f6e9af1934b7b026c19 Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 12 Feb 2025 15:31:26 -0500 Subject: [PATCH 01/18] fix(aws): handle `AccessDenied` when retrieving resource policy (#6908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pedro Martín --- ...lambda_function_not_publicly_accessible.py | 2 + .../dynamodb_table_cross_account_access.py | 2 + ...cr_repositories_not_publicly_accessible.py | 2 + .../providers/aws/services/ecr/ecr_service.py | 1 + .../efs_not_publicly_accessible.py | 2 + .../providers/aws/services/efs/efs_service.py | 2 +- .../eventbridge_bus_cross_account_access.py | 2 + .../eventbridge_bus_exposed.py | 2 + ...ge_schema_registry_cross_account_access.py | 2 + ...e_data_catalogs_not_publicly_accessible.py | 2 + ...service_domains_not_publicly_accessible.py | 2 +- .../s3_bucket_cross_account_access.py | 2 + .../s3_bucket_policy_public_write_access.py | 2 + .../s3_bucket_public_access.py | 4 +- .../providers/aws/services/s3/s3_service.py | 2 +- .../secretsmanager_not_publicly_accessible.py | 2 + .../ses_identity_not_publicly_accessible.py | 2 + ...ynamodb_table_cross_account_access_test.py | 121 +++++++++++------- ...positories_not_publicly_accessible_test.py | 77 ++++++----- 19 files changed, 150 insertions(+), 83 deletions(-) diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py index 86c3cbfec8..43bad4a686 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py +++ b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py @@ -7,6 +7,8 @@ class awslambda_function_not_publicly_accessible(Check): def execute(self): findings = [] for function in awslambda_client.functions.values(): + if function.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=function) report.status = "PASS" diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py index da161a1c0b..e3c1e817ca 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py @@ -7,6 +7,8 @@ class dynamodb_table_cross_account_access(Check): def execute(self): findings = [] for table in dynamodb_client.tables.values(): + if table.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=table) report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py b/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py index aa332926a5..484e7a1631 100644 --- a/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py +++ b/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py @@ -8,6 +8,8 @@ class ecr_repositories_not_publicly_accessible(Check): findings = [] for registry in ecr_client.registries.values(): for repository in registry.repositories: + if repository.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=repository) report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/aws/services/ecr/ecr_service.py b/prowler/providers/aws/services/ecr/ecr_service.py index 9cd6846d6d..c892ad1fca 100644 --- a/prowler/providers/aws/services/ecr/ecr_service.py +++ b/prowler/providers/aws/services/ecr/ecr_service.py @@ -87,6 +87,7 @@ class ECR(AWSService): logger.warning( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + repository.policy = {} except Exception as error: if "RepositoryPolicyNotFoundException" not in str(error): diff --git a/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py b/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py index 2db08c396d..bf12756b20 100644 --- a/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py +++ b/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py @@ -7,6 +7,8 @@ class efs_not_publicly_accessible(Check): def execute(self): findings = [] for fs in efs_client.filesystems.values(): + if fs.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=fs) report.status = "PASS" report.status_extended = f"EFS {fs.id} has a policy which does not allow access to any client within the VPC." diff --git a/prowler/providers/aws/services/efs/efs_service.py b/prowler/providers/aws/services/efs/efs_service.py index 51570de3c9..38b7f03073 100644 --- a/prowler/providers/aws/services/efs/efs_service.py +++ b/prowler/providers/aws/services/efs/efs_service.py @@ -164,7 +164,7 @@ class FileSystem(BaseModel): id: str arn: str region: str - policy: Optional[dict] = {} + policy: Optional[dict] backup_policy: Optional[str] = "DISABLED" encrypted: bool availability_zone_id: Optional[str] diff --git a/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.py b/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.py index d78b46fc52..5fee9d0775 100644 --- a/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.py +++ b/prowler/providers/aws/services/eventbridge/eventbridge_bus_cross_account_access/eventbridge_bus_cross_account_access.py @@ -9,6 +9,8 @@ class eventbridge_bus_cross_account_access(Check): def execute(self): findings = [] for bus in eventbridge_client.buses.values(): + if bus.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=bus) report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/aws/services/eventbridge/eventbridge_bus_exposed/eventbridge_bus_exposed.py b/prowler/providers/aws/services/eventbridge/eventbridge_bus_exposed/eventbridge_bus_exposed.py index d0803c1805..53797202a6 100644 --- a/prowler/providers/aws/services/eventbridge/eventbridge_bus_exposed/eventbridge_bus_exposed.py +++ b/prowler/providers/aws/services/eventbridge/eventbridge_bus_exposed/eventbridge_bus_exposed.py @@ -9,6 +9,8 @@ class eventbridge_bus_exposed(Check): def execute(self): findings = [] for bus in eventbridge_client.buses.values(): + if bus.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=bus) report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/aws/services/eventbridge/eventbridge_schema_registry_cross_account_access/eventbridge_schema_registry_cross_account_access.py b/prowler/providers/aws/services/eventbridge/eventbridge_schema_registry_cross_account_access/eventbridge_schema_registry_cross_account_access.py index 910cc5580c..c3a2a29377 100644 --- a/prowler/providers/aws/services/eventbridge/eventbridge_schema_registry_cross_account_access/eventbridge_schema_registry_cross_account_access.py +++ b/prowler/providers/aws/services/eventbridge/eventbridge_schema_registry_cross_account_access/eventbridge_schema_registry_cross_account_access.py @@ -7,6 +7,8 @@ class eventbridge_schema_registry_cross_account_access(Check): def execute(self): findings = [] for registry in schema_client.registries.values(): + if registry.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=registry) report.status = "PASS" report.status_extended = f"EventBridge schema registry {registry.name} does not allow cross-account access." diff --git a/prowler/providers/aws/services/glue/glue_data_catalogs_not_publicly_accessible/glue_data_catalogs_not_publicly_accessible.py b/prowler/providers/aws/services/glue/glue_data_catalogs_not_publicly_accessible/glue_data_catalogs_not_publicly_accessible.py index 6ad4645320..1c6be44aae 100644 --- a/prowler/providers/aws/services/glue/glue_data_catalogs_not_publicly_accessible/glue_data_catalogs_not_publicly_accessible.py +++ b/prowler/providers/aws/services/glue/glue_data_catalogs_not_publicly_accessible/glue_data_catalogs_not_publicly_accessible.py @@ -7,6 +7,8 @@ class glue_data_catalogs_not_publicly_accessible(Check): def execute(self): findings = [] for data_catalog in glue_client.data_catalogs.values(): + if data_catalog.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=data_catalog) report.resource_id = glue_client.audited_account report.resource_arn = glue_client._get_data_catalog_arn_template( diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py index 2ffd9c40fb..19a890620b 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.py @@ -17,7 +17,7 @@ class opensearch_service_domains_not_publicly_accessible(Check): if domain.vpc_id: report.status_extended = f"Opensearch domain {domain.name} is in a VPC, then it is not publicly accessible." - elif domain.access_policy and is_policy_public( + elif domain.access_policy is not None and is_policy_public( domain.access_policy, opensearch_client.audited_account ): report.status = "FAIL" diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py index a98483c767..3178a08aa1 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py +++ b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py @@ -7,6 +7,8 @@ class s3_bucket_cross_account_access(Check): def execute(self): findings = [] for bucket in s3_client.buckets.values(): + if bucket.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=bucket) report.status = "PASS" report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy but it does not allow cross account access." diff --git a/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py b/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py index 4fef8cf330..5ab17358dc 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py +++ b/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py @@ -8,6 +8,8 @@ class s3_bucket_policy_public_write_access(Check): def execute(self): findings = [] for bucket in s3_client.buckets.values(): + if bucket.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=bucket) # Check if bucket policy allow public write access if not bucket.policy: diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py index 8a77958863..77b83f03cd 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py +++ b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py @@ -45,7 +45,9 @@ class s3_bucket_public_access(Check): report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket ACL." # 4. Check bucket policy - if is_policy_public(bucket.policy, s3_client.audited_account): + if bucket.policy is not None and is_policy_public( + bucket.policy, s3_client.audited_account + ): report.status = "FAIL" report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket policy." findings.append(report) diff --git a/prowler/providers/aws/services/s3/s3_service.py b/prowler/providers/aws/services/s3/s3_service.py index 655555ce3e..8939da0987 100644 --- a/prowler/providers/aws/services/s3/s3_service.py +++ b/prowler/providers/aws/services/s3/s3_service.py @@ -681,7 +681,7 @@ class Bucket(BaseModel): logging: bool = False public_access_block: Optional[PublicAccessBlock] acl_grantees: List[ACL_Grantee] = Field(default_factory=list) - policy: Dict = Field(default_factory=dict) + policy: Optional[dict] encryption: Optional[str] region: str logging_target_bucket: Optional[str] diff --git a/prowler/providers/aws/services/secretsmanager/secretsmanager_not_publicly_accessible/secretsmanager_not_publicly_accessible.py b/prowler/providers/aws/services/secretsmanager/secretsmanager_not_publicly_accessible/secretsmanager_not_publicly_accessible.py index 8c8601d158..abc82ab3c7 100644 --- a/prowler/providers/aws/services/secretsmanager/secretsmanager_not_publicly_accessible/secretsmanager_not_publicly_accessible.py +++ b/prowler/providers/aws/services/secretsmanager/secretsmanager_not_publicly_accessible/secretsmanager_not_publicly_accessible.py @@ -9,6 +9,8 @@ class secretsmanager_not_publicly_accessible(Check): def execute(self): findings = [] for secret in secretsmanager_client.secrets.values(): + if secret.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=secret) report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/aws/services/ses/ses_identity_not_publicly_accessible/ses_identity_not_publicly_accessible.py b/prowler/providers/aws/services/ses/ses_identity_not_publicly_accessible/ses_identity_not_publicly_accessible.py index 0238de01a7..f668b05d06 100644 --- a/prowler/providers/aws/services/ses/ses_identity_not_publicly_accessible/ses_identity_not_publicly_accessible.py +++ b/prowler/providers/aws/services/ses/ses_identity_not_publicly_accessible/ses_identity_not_publicly_accessible.py @@ -7,6 +7,8 @@ class ses_identity_not_publicly_accessible(Check): def execute(self): findings = [] for identity in ses_client.email_identities.values(): + if identity.policy is None: + continue report = Check_Report_AWS(metadata=self.metadata(), resource=identity) report.status = "PASS" report.status_extended = ( diff --git a/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py b/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py index 4071394a6d..b29cd086bb 100644 --- a/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py +++ b/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py @@ -104,12 +104,15 @@ class Test_dynamodb_table_cross_account_access: def test_no_tables(self): dynamodb_client = mock.MagicMock dynamodb_client.tables = {} - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, @@ -131,15 +134,19 @@ class Test_dynamodb_table_cross_account_access: arn=arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, + policy={}, ) } - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, @@ -172,12 +179,15 @@ class Test_dynamodb_table_cross_account_access: policy=test_restricted_policy, ) } - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, @@ -211,12 +221,15 @@ class Test_dynamodb_table_cross_account_access: ) } - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, @@ -251,12 +264,15 @@ class Test_dynamodb_table_cross_account_access: ) } - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, @@ -291,12 +307,15 @@ class Test_dynamodb_table_cross_account_access: ) } - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, @@ -330,12 +349,15 @@ class Test_dynamodb_table_cross_account_access: ) } - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, @@ -369,12 +391,15 @@ class Test_dynamodb_table_cross_account_access: ) } - with mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", - new=dynamodb_client, - ), mock.patch( - "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", - new=dynamodb_client, + with ( + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", + new=dynamodb_client, + ), + mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), ): from prowler.providers.aws.services.dynamodb.dynamodb_table_cross_account_access.dynamodb_table_cross_account_access import ( dynamodb_table_cross_account_access, diff --git a/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py b/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py index 9904aec2ca..82c31cd48d 100644 --- a/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py @@ -43,12 +43,15 @@ class Test_ecr_repositories_not_publicly_accessible: ecr_client = mock.MagicMock ecr_client.registries = {} - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", - ecr_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", + ecr_client, + ), ): from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( ecr_repositories_not_publicly_accessible, @@ -70,12 +73,15 @@ class Test_ecr_repositories_not_publicly_accessible: rules=[], ) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", - ecr_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", + ecr_client, + ), ): from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( ecr_repositories_not_publicly_accessible, @@ -109,12 +115,15 @@ class Test_ecr_repositories_not_publicly_accessible: rules=[], ) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", - ecr_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", + ecr_client, + ), ): from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( ecr_repositories_not_publicly_accessible, @@ -145,7 +154,7 @@ class Test_ecr_repositories_not_publicly_accessible: arn=repository_arn, region=AWS_REGION_EU_WEST_1, scan_on_push=True, - policy=None, + policy={}, images_details=None, lifecycle_policy=None, ) @@ -153,12 +162,15 @@ class Test_ecr_repositories_not_publicly_accessible: rules=[], ) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", - ecr_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", + ecr_client, + ), ): from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( ecr_repositories_not_publicly_accessible, @@ -199,12 +211,15 @@ class Test_ecr_repositories_not_publicly_accessible: rules=[], ) - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider(), - ), mock.patch( - "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", - ecr_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), + mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", + ecr_client, + ), ): from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( ecr_repositories_not_publicly_accessible, From c3a13b8a2981820c8b83d44e9c5f103299e68c86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 19:15:03 -0500 Subject: [PATCH 02/18] chore(deps): bump trufflesecurity/trufflehog from 3.88.6 to 3.88.7 (#6915) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/find-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index acf5f33217..d49c77c221 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.88.6 + uses: trufflesecurity/trufflehog@v3.88.7 with: path: ./ base: ${{ github.event.repository.default_branch }} From 16b4775e2d51e4f67637ab74babf1c6a25245e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 13 Feb 2025 13:48:19 +0100 Subject: [PATCH 03/18] fix(gcp): remove typos on CIS 3.0 (#6917) --- prowler/compliance/gcp/cis_3.0_gcp.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prowler/compliance/gcp/cis_3.0_gcp.json b/prowler/compliance/gcp/cis_3.0_gcp.json index 6ae75a260e..a0e860e81d 100644 --- a/prowler/compliance/gcp/cis_3.0_gcp.json +++ b/prowler/compliance/gcp/cis_3.0_gcp.json @@ -222,7 +222,7 @@ "Id": "1.11", "Description": "Ensure That Separation of Duties Is Enforced While Assigning KMS Related Roles to Users", "Checks": [ - "iam_role_kms_enforce_separation_of_duties”" + "iam_role_kms_enforce_separation_of_duties" ], "Attributes": [ { @@ -394,7 +394,7 @@ "Id": "2.2", "Description": "Ensure That Sinks Are Configured for All Log Entries", "Checks": [ - "cloudstorage_bucket_log_retention_policy_lock”" + "cloudstorage_bucket_log_retention_policy_lock" ], "Attributes": [ { From 0ff4df08363ca482a95d0c94f9b01481f00def45 Mon Sep 17 00:00:00 2001 From: Kay Agahd Date: Thu, 13 Feb 2025 15:34:48 +0100 Subject: [PATCH 04/18] fix(aws): SNS threw IndexError if SubscriptionArn is PendingConfirmation (#6896) --- prowler/providers/aws/services/sns/sns_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prowler/providers/aws/services/sns/sns_service.py b/prowler/providers/aws/services/sns/sns_service.py index a2f2d3be29..2eb93ee792 100644 --- a/prowler/providers/aws/services/sns/sns_service.py +++ b/prowler/providers/aws/services/sns/sns_service.py @@ -92,12 +92,12 @@ class SNS(AWSService): ) subscriptions: list[Subscription] = [ Subscription( - id=sub["SubscriptionArn"].split(":")[-1], + id=(parts := sub["SubscriptionArn"].split(":"))[-1], arn=sub["SubscriptionArn"], owner=sub["Owner"], protocol=sub["Protocol"], endpoint=sub["Endpoint"], - region=sub["SubscriptionArn"].split(":")[3], + region=parts[3] if len(parts) > 3 else "unknown", ) for sub in response["Subscriptions"] ] From 1fe125a65fe83df32ab0831042d6f5dc7247055c Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Thu, 13 Feb 2025 09:46:05 -0500 Subject: [PATCH 05/18] chore(docs): external K8s cluster Prowler App credentials (#6921) --- docs/tutorials/kubernetes/in-cluster.md | 1 + docs/tutorials/prowler-app.md | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/kubernetes/in-cluster.md b/docs/tutorials/kubernetes/in-cluster.md index ae733db84a..9b16b21f09 100644 --- a/docs/tutorials/kubernetes/in-cluster.md +++ b/docs/tutorials/kubernetes/in-cluster.md @@ -2,6 +2,7 @@ For in-cluster execution, you can use the supplied yaml files inside `/kubernetes`: +* [prowler-sa.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-sa.yaml) * [job.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/job.yaml) * [prowler-role.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-role.yaml) * [prowler-rolebinding.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-rolebinding.yaml) diff --git a/docs/tutorials/prowler-app.md b/docs/tutorials/prowler-app.md index d4d3d2f890..cd94504579 100644 --- a/docs/tutorials/prowler-app.md +++ b/docs/tutorials/prowler-app.md @@ -100,9 +100,11 @@ By default, the `kubeconfig` file is located at `~/.kube/config`. Kubernetes Credentials ???+ note - If you are adding an **Amazon EKS** cluster, follow these additional steps to ensure proper authentication: + If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these additional steps to ensure proper authentication: - 1. Apply the necessary Kubernetes resources to your EKS cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)): + ** Make sure your cluster allow traffic from the Prowler Cloud IP address `52.48.254.174/32` ** + + 1. Apply the necessary Kubernetes resources to your EKS, GKE, AKS or external cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)): ```console kubectl apply -f kubernetes/prowler-sa.yaml kubectl apply -f kubernetes/prowler-role.yaml @@ -119,11 +121,11 @@ By default, the `kubeconfig` file is located at `~/.kube/config`. 3. Update your `kubeconfig` to use the ServiceAccount token: ```console kubectl config set-credentials prowler-sa --token= - kubectl config set-context --user=prowler-sa + kubectl config set-context --user=prowler-sa ``` - Replace with the generated token and with your EKS cluster ARN. + Replace with the generated token and with your KubeConfig Context Name of your EKS, GKE or AKS cluster. - 4. Now you can add the modified `kubeconfig` as the credentials of the AWS EKS Cluster in Prowler Cloud. Then simply test the connection. + 4. Now you can add the modified `kubeconfig` in Prowler Cloud. Then simply test the connection. --- From 3a8f4d2ffb527167cb32babee4564bea194757fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Thu, 13 Feb 2025 16:54:38 +0100 Subject: [PATCH 06/18] feat(social-login): Add social login integration for Google and Github OAuth providers (#6906) --- api/.env.example | 9 + api/CHANGELOG.md | 4 +- api/poetry.lock | 537 +++++++++--------- api/pyproject.toml | 3 +- api/src/backend/api/adapters.py | 57 ++ api/src/backend/api/db_router.py | 17 +- api/src/backend/api/specs/v1.yaml | 106 +++- .../tests/integration/test_authentication.py | 121 ++++ api/src/backend/api/utils.py | 18 +- api/src/backend/api/v1/serializers.py | 138 +++-- api/src/backend/api/v1/urls.py | 6 + api/src/backend/api/v1/views.py | 95 +++- api/src/backend/config/django/base.py | 11 + .../backend/config/settings/social_login.py | 53 ++ 14 files changed, 838 insertions(+), 337 deletions(-) create mode 100644 api/src/backend/api/adapters.py create mode 100644 api/src/backend/config/settings/social_login.py diff --git a/api/.env.example b/api/.env.example index 4b0894b3ba..192317fa02 100644 --- a/api/.env.example +++ b/api/.env.example @@ -39,3 +39,12 @@ POSTGRES_DB=prowler_db VALKEY_HOST=[localhost|valkey] VALKEY_PORT=6379 VALKEY_DB=0 + +# Social login credentials +DJANGO_GOOGLE_OAUTH_CLIENT_ID="" +DJANGO_GOOGLE_OAUTH_CLIENT_SECRET="" +DJANGO_GOOGLE_OAUTH_CALLBACK_URL="" + +DJANGO_GITHUB_OAUTH_CLIENT_ID="" +DJANGO_GITHUB_OAUTH_CLIENT_SECRET="" +DJANGO_GITHUB_OAUTH_CALLBACK_URL="" diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 0a3acb7fd4..34f19eb29a 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -4,8 +4,10 @@ All notable changes to the **Prowler API** are documented in this file. --- -## [Unreleased] +## [v1.5.0] (Prowler UNRELEASED) +### Added +- Social login integration with Google and GitHub [(#6906)](https://github.com/prowler-cloud/prowler/pull/6906) --- diff --git a/api/poetry.lock b/api/poetry.lock index 491b06933a..c05ae59d8c 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -13,98 +13,103 @@ files = [ [[package]] name = "aiohappyeyeballs" -version = "2.4.4" +version = "2.4.6" description = "Happy Eyeballs for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, - {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, + {file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"}, + {file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"}, ] [[package]] name = "aiohttp" -version = "3.11.11" +version = "3.11.12" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, - {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, - {file = "aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c"}, - {file = "aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745"}, - {file = "aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773"}, - {file = "aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62"}, - {file = "aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e"}, - {file = "aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600"}, - {file = "aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5"}, - {file = "aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d"}, - {file = "aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226"}, - {file = "aiohttp-3.11.11-cp39-cp39-win32.whl", hash = "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3"}, - {file = "aiohttp-3.11.11-cp39-cp39-win_amd64.whl", hash = "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1"}, - {file = "aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff"}, + {file = "aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d"}, + {file = "aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b"}, + {file = "aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16"}, + {file = "aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"}, + {file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"}, + {file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce"}, + {file = "aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f"}, + {file = "aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8"}, + {file = "aiohttp-3.11.12-cp39-cp39-win32.whl", hash = "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462"}, + {file = "aiohttp-3.11.12-cp39-cp39-win_amd64.whl", hash = "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798"}, + {file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"}, ] [package.dependencies] @@ -809,13 +814,13 @@ zstd = ["zstandard (==0.22.0)"] [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -1368,6 +1373,24 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "dj-rest-auth" +version = "7.0.1" +description = "Authentication and Registration in Django Rest Framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dj-rest-auth-7.0.1.tar.gz", hash = "sha256:3f8c744cbcf05355ff4bcbef0c8a63645da38e29a0fdef3c3332d4aced52fb90"}, +] + +[package.dependencies] +Django = ">=4.2,<6.0" +django-allauth = {version = ">=64.0.0", extras = ["socialaccount"], optional = true, markers = "extra == \"with-social\""} +djangorestframework = ">=3.13.0" + +[package.extras] +with-social = ["django-allauth[socialaccount] (>=64.0.0)"] + [[package]] name = "django" version = "5.1.5" @@ -1388,6 +1411,30 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-allauth" +version = "65.4.1" +description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_allauth-65.4.1.tar.gz", hash = "sha256:60b32aef7dbbcc213319aa4fd8f570e985266ea1162ae6ef7a26a24efca85c8c"}, +] + +[package.dependencies] +asgiref = ">=3.8.1" +Django = ">=4.2.16" +pyjwt = {version = ">=1.7", extras = ["crypto"], optional = true, markers = "extra == \"socialaccount\""} +requests = {version = ">=2.0.0", optional = true, markers = "extra == \"socialaccount\""} +requests-oauthlib = {version = ">=0.3.0", optional = true, markers = "extra == \"socialaccount\""} + +[package.extras] +mfa = ["fido2 (>=1.1.2)", "qrcode (>=7.0.0)"] +openid = ["python3-openid (>=3.0.8)"] +saml = ["python3-saml (>=1.15.0,<2.0.0)"] +socialaccount = ["pyjwt[crypto] (>=1.7)", "requests (>=2.0.0)", "requests-oauthlib (>=0.3.0)"] +steam = ["python3-openid (>=3.0.8)"] + [[package]] name = "django-celery-beat" version = "2.7.0" @@ -1943,13 +1990,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.159.0" +version = "2.160.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.159.0-py2.py3-none-any.whl", hash = "sha256:baef0bb631a60a0bd7c0bf12a5499e3a40cd4388484de7ee55c1950bf820a0cf"}, - {file = "google_api_python_client-2.159.0.tar.gz", hash = "sha256:55197f430f25c907394b44fa078545ffef89d33fd4dca501b7db9f0d8e224bd6"}, + {file = "google_api_python_client-2.160.0-py2.py3-none-any.whl", hash = "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4"}, + {file = "google_api_python_client-2.160.0.tar.gz", hash = "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"}, ] [package.dependencies] @@ -2073,18 +2120,18 @@ files = [ [[package]] name = "h2" -version = "4.1.0" -description = "HTTP/2 State-Machine based protocol implementation" +version = "4.2.0" +description = "Pure-Python HTTP/2 protocol implementation" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.9" files = [ - {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, - {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, + {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, + {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, ] [package.dependencies] -hpack = ">=4.0,<5" -hyperframe = ">=6.0,<7" +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" [[package]] name = "hpack" @@ -2361,13 +2408,13 @@ zookeeper = ["kazoo (>=2.8.0)"] [[package]] name = "kubernetes" -version = "31.0.0" +version = "32.0.0" description = "Kubernetes python client" optional = false python-versions = ">=3.6" files = [ - {file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"}, - {file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"}, + {file = "kubernetes-32.0.0-py2.py3-none-any.whl", hash = "sha256:60fd8c29e8e43d9c553ca4811895a687426717deba9c0a66fb2dcc3f5ef96692"}, + {file = "kubernetes-32.0.0.tar.gz", hash = "sha256:319fa840345a482001ac5d6062222daeb66ec4d1bcb3087402aed685adf0aecb"}, ] [package.dependencies] @@ -2482,13 +2529,13 @@ files = [ [[package]] name = "marshmallow" -version = "3.26.0" +version = "3.26.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" files = [ - {file = "marshmallow-3.26.0-py3-none-any.whl", hash = "sha256:1287bca04e6a5f4094822ac153c03da5e214a0a60bcd557b140f3e66991b8ca1"}, - {file = "marshmallow-3.26.0.tar.gz", hash = "sha256:eb36762a1cc76d7abf831e18a3a1b26d3d481bbc74581b8e532a3d3a8115e1cb"}, + {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, + {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, ] [package.dependencies] @@ -2523,13 +2570,13 @@ files = [ [[package]] name = "microsoft-kiota-abstractions" -version = "1.6.8" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_abstractions-1.6.8-py3-none-any.whl", hash = "sha256:12819dee24d5aaa31e99683d938f65e50cbc446de087df244cd26c3326ec4e15"}, - {file = "microsoft_kiota_abstractions-1.6.8.tar.gz", hash = "sha256:7070affabfa7182841646a0c8491cbb240af366aff2b9132f0caa45c4837dd78"}, + {file = "microsoft_kiota_abstractions-1.9.2-py3-none-any.whl", hash = "sha256:a8853d272a84da59d6a2fe11a76c28e9c55bdab268a345ba48e918cb6822b607"}, + {file = "microsoft_kiota_abstractions-1.9.2.tar.gz", hash = "sha256:29cdafe8d0672f23099556e0b120dca6231c752cca9393e1e0092fa9ca594572"}, ] [package.dependencies] @@ -2539,99 +2586,94 @@ std-uritemplate = ">=2.0.0" [[package]] name = "microsoft-kiota-authentication-azure" -version = "1.6.8" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_authentication_azure-1.6.8-py3-none-any.whl", hash = "sha256:50455789b7133e27fbccec839d93e40d2637d18593a93921ae1338880c5b5b3b"}, - {file = "microsoft_kiota_authentication_azure-1.6.8.tar.gz", hash = "sha256:fef23f43cd4d3b9ef839c8b3d1f675ec4a1120c150f963d8c4551c5e19ac3b36"}, + {file = "microsoft_kiota_authentication_azure-1.9.2-py3-none-any.whl", hash = "sha256:56840f8b15df8aedfd143fb2deb7cc7fae4ac0bafb1a50546b7313a7b3ab4ca0"}, + {file = "microsoft_kiota_authentication_azure-1.9.2.tar.gz", hash = "sha256:171045f522a93d9340fbddc4cabb218f14f1d9d289e82e535b3d9291986c3d5a"}, ] [package.dependencies] aiohttp = ">=3.8.0" azure-core = ">=1.21.1" -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" +microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" opentelemetry-api = ">=1.27.0" opentelemetry-sdk = ">=1.27.0" [[package]] name = "microsoft-kiota-http" -version = "1.6.8" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_http-1.6.8-py3-none-any.whl", hash = "sha256:7ff76a308351d885453185d6a6538c47a64ebdc7661cce46a904e89e2ceb9a1d"}, - {file = "microsoft_kiota_http-1.6.8.tar.gz", hash = "sha256:67242690b79a30c0cadf823675249269e4bc020283e3d65b33af7d771df64df8"}, + {file = "microsoft_kiota_http-1.9.2-py3-none-any.whl", hash = "sha256:3a2d930a70d0184d9f4848473f929ee892462cae1acfaf33b2d193f1828c76c2"}, + {file = "microsoft_kiota_http-1.9.2.tar.gz", hash = "sha256:2ba3d04a3d1d5d600736eebc1e33533d54d87799ac4fbb92c9cce4a97809af61"}, ] [package.dependencies] -httpx = {version = ">=0.28", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" +httpx = {version = ">=0.25,<1.0.0", extras = ["http2"]} +microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" opentelemetry-api = ">=1.27.0" opentelemetry-sdk = ">=1.27.0" -urllib3 = ">=2.2.2,<3.0.0" [[package]] name = "microsoft-kiota-serialization-form" -version = "1.6.8" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_form-1.6.8-py3-none-any.whl", hash = "sha256:ca7dd19e173aa87c68b38c5056cc0921570c8c86f3ba5511d1616cf97e2f0f67"}, - {file = "microsoft_kiota_serialization_form-1.6.8.tar.gz", hash = "sha256:bb9eb98b3abf596b4bfe208014dff948361ff48a757316ac58e19c31ab8d640a"}, + {file = "microsoft_kiota_serialization_form-1.9.2-py3-none-any.whl", hash = "sha256:7b997efb2c8750b1d4fbc00878ba2a3e6e1df3fcefc8815226c90fcc9c54f218"}, + {file = "microsoft_kiota_serialization_form-1.9.2.tar.gz", hash = "sha256:badfbe65d8ec3369bd58b01022d13ef590edf14babeef94188efe3f4ec24fe41"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" [[package]] name = "microsoft-kiota-serialization-json" -version = "1.6.8" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_json-1.6.8-py3-none-any.whl", hash = "sha256:2734c2ad64cc089441279e4962f6fedf41af040730f6eab1533890cd5377aff5"}, - {file = "microsoft_kiota_serialization_json-1.6.8.tar.gz", hash = "sha256:89e2dd0eb4eaaa6ab74fa89ab5d84c5a53464e73b85eb7085f0aa4560a2b8183"}, + {file = "microsoft_kiota_serialization_json-1.9.2-py3-none-any.whl", hash = "sha256:8f4ecf485607fff3df5ce8fa9b9c957bc7f4bff1658b183703e180af753098e3"}, + {file = "microsoft_kiota_serialization_json-1.9.2.tar.gz", hash = "sha256:19f7beb69c67b2cb77ca96f77824ee78a693929e20237bb5476ea54f69118bf1"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" [[package]] name = "microsoft-kiota-serialization-multipart" -version = "1.6.8" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_multipart-1.6.8-py3-none-any.whl", hash = "sha256:1ecdd15dd1f78aed031d7d1828b6fbc00c633542d863c23f96fdd0a61bfb189a"}, - {file = "microsoft_kiota_serialization_multipart-1.6.8.tar.gz", hash = "sha256:3d95c6d7186588af7a1d3aa852ce42077f80487b8b3c60e36fe109a8b4918c03"}, + {file = "microsoft_kiota_serialization_multipart-1.9.2-py3-none-any.whl", hash = "sha256:641ad374046f1c7adff90d110bdc68d77418adb1e479a716f4ffea3647f0ead6"}, + {file = "microsoft_kiota_serialization_multipart-1.9.2.tar.gz", hash = "sha256:b1851409205668d83f5c7a35a8b6fca974b341985b4a92841e95aaec93b7ca0a"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" [[package]] name = "microsoft-kiota-serialization-text" -version = "1.6.8" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_serialization_text-1.6.8-py3-none-any.whl", hash = "sha256:4e5e287a614d362f864b5061dca0861c3f70b8792ec72967d1bff23944da1e80"}, - {file = "microsoft_kiota_serialization_text-1.6.8.tar.gz", hash = "sha256:687d4858337eaf4f351b12ed1c6c934d869560f54ee3855bfdde589660e07208"}, + {file = "microsoft_kiota_serialization_text-1.9.2-py3-none-any.whl", hash = "sha256:6e63129ea29eb9b976f4ed56fc6595d204e29fc309958b639299e9f9f4e5edb4"}, + {file = "microsoft_kiota_serialization_text-1.9.2.tar.gz", hash = "sha256:4289508ebac0cefdc4fa21c545051769a9409913972355ccda9116b647f978f2"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -python-dateutil = "2.9.0.post0" +microsoft-kiota-abstractions = ">=1.9.2,<1.10.0" [[package]] name = "msal" @@ -2669,20 +2711,20 @@ portalocker = ">=1.4,<3" [[package]] name = "msgraph-core" -version = "1.2.0" +version = "1.3.1" description = "Core component of the Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" files = [ - {file = "msgraph_core-1.2.0-py3-none-any.whl", hash = "sha256:4ce14bbe743c0f2dd8b53c7fcd338fc14081e0df6b14021f0d0e4bb63cd5a840"}, - {file = "msgraph_core-1.2.0.tar.gz", hash = "sha256:a4e42f692e664c60d63359e610bbf990f57b42d8080417261ff7042bbd59c98b"}, + {file = "msgraph_core-1.3.1-py3-none-any.whl", hash = "sha256:a6e3c19c36d43d00edfb49225647a357f8189351c226c3b7c020c5dfaa24171b"}, + {file = "msgraph_core-1.3.1.tar.gz", hash = "sha256:91c721b4c741d0e9a536e3c43e1cbd2254928b4d207e4f994e9cc7cb7a25bd74"}, ] [package.dependencies] httpx = {version = ">=0.23.0", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.0.0,<2.0.0" -microsoft-kiota-authentication-azure = ">=1.0.0,<2.0.0" -microsoft-kiota-http = ">=1.0.0,<2.0.0" +microsoft-kiota-abstractions = ">=1.8.0,<2.0.0" +microsoft-kiota-authentication-azure = ">=1.8.0,<2.0.0" +microsoft-kiota-http = ">=1.8.0,<2.0.0" [package.extras] dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] @@ -2891,6 +2933,32 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "narwhals" +version = "1.25.2" +description = "Extremely lightweight compatibility layer between dataframe libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "narwhals-1.25.2-py3-none-any.whl", hash = "sha256:e645f7fc1f8c0a3563a6cdcd0191586cdf88470ad90f0818abba7ceb6c181b00"}, + {file = "narwhals-1.25.2.tar.gz", hash = "sha256:37594746fc06fe4a588967a34a2974b1f3a7ad6ff1571b6e31ac5e58c9591000"}, +] + +[package.extras] +core = ["duckdb", "pandas", "polars", "pyarrow", "pyarrow-stubs"] +cudf = ["cudf (>=24.10.0)"] +dask = ["dask[dataframe] (>=2024.8)"] +dev = ["covdefaults", "hypothesis", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-randomly", "typing-extensions"] +docs = ["black", "duckdb", "jinja2", "markdown-exec[ansi]", "mkdocs", "mkdocs-autorefs", "mkdocs-material", "mkdocstrings[python]", "pandas", "polars (>=1.0.0)", "pyarrow"] +duckdb = ["duckdb (>=1.0)"] +extra = ["scikit-learn"] +ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] +modin = ["modin"] +pandas = ["pandas (>=0.25.3)"] +polars = ["polars (>=0.20.3)"] +pyarrow = ["pyarrow (>=11.0.0)"] +pyspark = ["pyspark (>=3.5.0)"] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -2974,13 +3042,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "opentelemetry-api" -version = "1.29.0" +version = "1.30.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, - {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, + {file = "opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09"}, + {file = "opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240"}, ] [package.dependencies] @@ -2989,34 +3057,34 @@ importlib-metadata = ">=6.0,<=8.5.0" [[package]] name = "opentelemetry-sdk" -version = "1.29.0" +version = "1.30.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_sdk-1.29.0-py3-none-any.whl", hash = "sha256:173be3b5d3f8f7d671f20ea37056710217959e774e2749d984355d1f9391a30a"}, - {file = "opentelemetry_sdk-1.29.0.tar.gz", hash = "sha256:b0787ce6aade6ab84315302e72bd7a7f2f014b0fb1b7c3295b88afe014ed0643"}, + {file = "opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091"}, + {file = "opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18"}, ] [package.dependencies] -opentelemetry-api = "1.29.0" -opentelemetry-semantic-conventions = "0.50b0" +opentelemetry-api = "1.30.0" +opentelemetry-semantic-conventions = "0.51b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.50b0" +version = "0.51b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_semantic_conventions-0.50b0-py3-none-any.whl", hash = "sha256:e87efba8fdb67fb38113efea6a349531e75ed7ffc01562f65b802fcecb5e115e"}, - {file = "opentelemetry_semantic_conventions-0.50b0.tar.gz", hash = "sha256:02dc6dbcb62f082de9b877ff19a3f1ffaa3c306300fa53bfac761c4567c83d38"}, + {file = "opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae"}, + {file = "opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47"}, ] [package.dependencies] deprecated = ">=1.2.6" -opentelemetry-api = "1.29.0" +opentelemetry-api = "1.30.0" [[package]] name = "packaging" @@ -3116,113 +3184,17 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pbr" -version = "6.1.0" +version = "6.1.1" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, - {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, -] - -[[package]] -name = "pendulum" -version = "3.0.0" -description = "Python datetimes made easy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, + {file = "pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76"}, + {file = "pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b"}, ] [package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] +setuptools = "*" [[package]] name = "platformdirs" @@ -3242,18 +3214,21 @@ type = ["mypy (>=1.11.2)"] [[package]] name = "plotly" -version = "5.24.1" +version = "6.0.0" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089"}, - {file = "plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae"}, + {file = "plotly-6.0.0-py3-none-any.whl", hash = "sha256:f708871c3a9349a68791ff943a5781b1ec04de7769ea69068adcd9202e57653a"}, + {file = "plotly-6.0.0.tar.gz", hash = "sha256:c4aad38b8c3d65e4a5e7dd308b084143b9025c2cc9d5317fc1f1d30958db87d3"}, ] [package.dependencies] +narwhals = ">=1.15.1" packaging = "*" -tenacity = ">=6.2.0" + +[package.extras] +express = ["numpy"] [[package]] name = "pluggy" @@ -3470,18 +3445,18 @@ cryptography = "43.0.1" dash = "2.18.2" dash-bootstrap-components = "1.6.0" detect-secrets = "1.5.0" -google-api-python-client = "2.159.0" +google-api-python-client = "2.160.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" -kubernetes = "31.0.0" -microsoft-kiota-abstractions = "1.6.8" +kubernetes = "32.0.0" +microsoft-kiota-abstractions = "1.9.2" msgraph-sdk = "1.18.0" numpy = "2.0.2" pandas = "2.2.3" -py-ocsf-models = "0.2.0" +py-ocsf-models = "0.3.0" pydantic = "1.10.18" python-dateutil = "^2.9.0.post0" -pytz = "2024.2" +pytz = "2025.1" schema = "0.7.7" shodan = "1.31.0" slack-sdk = "3.34.0" @@ -3492,7 +3467,7 @@ tzlocal = "5.2" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "47bc2ed2dc472fc24ecab35970205448ab3b090d" +resolved_reference = "7469377079bb88487a741625dcd431aa5375e793" [[package]] name = "psutil" @@ -3606,13 +3581,13 @@ files = [ [[package]] name = "py-ocsf-models" -version = "0.2.0" +version = "0.3.0" description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." optional = false python-versions = "<3.13,>=3.9" files = [ - {file = "py_ocsf_models-0.2.0-py3-none-any.whl", hash = "sha256:ac75fd21077694b343ebaad3479194db113c274879b114277560ff287d5cd7b5"}, - {file = "py_ocsf_models-0.2.0.tar.gz", hash = "sha256:3e12648d05329e6776a0e6b1ffea87a3eb60aa7d8cb2c4afd69e5724f443ce03"}, + {file = "py_ocsf_models-0.3.0-py3-none-any.whl", hash = "sha256:3d31e379be5e4271f7faf62dee9c36798559a1f7f98dff142c0e4cfdb35e291c"}, + {file = "py_ocsf_models-0.3.0.tar.gz", hash = "sha256:ad46b7d9761b74010f06a894df2d9541989252b7ff738cd5c7edbf4283df2279"}, ] [package.dependencies] @@ -4040,13 +4015,13 @@ files = [ [[package]] name = "pytz" -version = "2024.2" +version = "2025.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, ] [[package]] @@ -4661,13 +4636,13 @@ doc = ["sphinx"] [[package]] name = "std-uritemplate" -version = "2.0.1" +version = "2.0.3" description = "std-uritemplate implementation for Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "std_uritemplate-2.0.1-py3-none-any.whl", hash = "sha256:6405d13f9ff3b8f70e1fa4eaefa373049659e1a61870c4b651441821a3fc984d"}, - {file = "std_uritemplate-2.0.1.tar.gz", hash = "sha256:f4684ae050167e237ed5819423139893e0b5b7f08dc6b7467bba3fdd64608be8"}, + {file = "std_uritemplate-2.0.3-py3-none-any.whl", hash = "sha256:434df26453bf68c6077879fed6609b2c39e2fc73080e74cd157269d5f8abdb3e"}, + {file = "std_uritemplate-2.0.3.tar.gz", hash = "sha256:ad4cb1d671bcf4a3608b3598c687be4b0929867c53a2d69c105989da6a5a2d4c"}, ] [[package]] @@ -4996,13 +4971,13 @@ files = [ [[package]] name = "xlsxwriter" -version = "3.2.1" +version = "3.2.2" description = "A Python module for creating Excel XLSX files." optional = false python-versions = ">=3.6" files = [ - {file = "XlsxWriter-3.2.1-py3-none-any.whl", hash = "sha256:7e8f7c60b7a1660ef791d46ab5de78469cb978b991ca841af61f5832d2f9f4fe"}, - {file = "XlsxWriter-3.2.1.tar.gz", hash = "sha256:97618759cb264fb6a93397f660cca156ffa9561743b1823dafb60dc4474e1902"}, + {file = "XlsxWriter-3.2.2-py3-none-any.whl", hash = "sha256:272ce861e7fa5e82a4a6ebc24511f2cb952fde3461f6c6e1a1e81d3272db1471"}, + {file = "xlsxwriter-3.2.2.tar.gz", hash = "sha256:befc7f92578a85fed261639fb6cde1fd51b79c5e854040847dde59d4317077dc"}, ] [[package]] @@ -5123,4 +5098,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "6465edb36efd1fa6db06d4103fea8046951acc3f4f8b357facaaa34ae2bc74bd" +content-hash = "d762c984b70da32828fc0b2c8b9d4849a9342b8a9ac7060c47d6e82071b740f1" diff --git a/api/pyproject.toml b/api/pyproject.toml index 2cccb9ddb3..2a219106cc 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -8,10 +8,11 @@ description = "Prowler's API (Django/DRF)" license = "Apache-2.0" name = "prowler-api" package-mode = false -version = "1.4.0" +version = "1.5.0" [tool.poetry.dependencies] celery = {extras = ["pytest"], version = "^5.4.0"} +dj-rest-auth = {extras = ["with_social", "jwt"], version = "7.0.1"} django = "5.1.5" django-celery-beat = "^2.7.0" django-celery-results = "^2.5.1" diff --git a/api/src/backend/api/adapters.py b/api/src/backend/api/adapters.py new file mode 100644 index 0000000000..9a8cb2f044 --- /dev/null +++ b/api/src/backend/api/adapters.py @@ -0,0 +1,57 @@ +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.db import transaction + +from api.db_router import MainRouter +from api.db_utils import rls_transaction +from api.models import Membership, Role, Tenant, User, UserRoleRelationship + + +class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter): + @staticmethod + def get_user_by_email(email: str): + try: + return User.objects.get(email=email) + except User.DoesNotExist: + return None + + def pre_social_login(self, request, sociallogin): + # Link existing accounts with the same email address + email = sociallogin.account.extra_data.get("email") + if email: + existing_user = self.get_user_by_email(email) + if existing_user: + sociallogin.connect(request, existing_user) + + def save_user(self, request, sociallogin, form=None): + """ + Called after the user data is fully populated from the provider + and is about to be saved to the DB for the first time. + """ + with transaction.atomic(using=MainRouter.admin_db): + user = super().save_user(request, sociallogin, form) + user.save(using=MainRouter.admin_db) + + tenant = Tenant.objects.using(MainRouter.admin_db).create( + name=f"{user.email.split('@')[0]} default tenant" + ) + with rls_transaction(str(tenant.id)): + Membership.objects.using(MainRouter.admin_db).create( + user=user, tenant=tenant, role=Membership.RoleChoices.OWNER + ) + role = Role.objects.using(MainRouter.admin_db).create( + name="admin", + tenant_id=tenant.id, + manage_users=True, + manage_account=True, + manage_billing=True, + manage_providers=True, + manage_integrations=True, + manage_scans=True, + unlimited_visibility=True, + ) + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=user, + role=role, + tenant_id=tenant.id, + ) + return user diff --git a/api/src/backend/api/db_router.py b/api/src/backend/api/db_router.py index 939672f88e..dc34c1191a 100644 --- a/api/src/backend/api/db_router.py +++ b/api/src/backend/api/db_router.py @@ -1,22 +1,29 @@ +ALLOWED_APPS = ("django", "socialaccount", "account", "authtoken", "silk") + + class MainRouter: default_db = "default" admin_db = "admin" def db_for_read(self, model, **hints): # noqa: F841 model_table_name = model._meta.db_table - if model_table_name.startswith("django_") or model_table_name.startswith( - "silk_" + if model_table_name.startswith("django_") or any( + model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS ): return self.admin_db return None def db_for_write(self, model, **hints): # noqa: F841 model_table_name = model._meta.db_table - if model_table_name.startswith("django_") or model_table_name.startswith( - "silk_" - ): + if any(model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS): return self.admin_db return None def allow_migrate(self, db, app_label, model_name=None, **hints): # noqa: F841 return db == self.admin_db + + def allow_relation(self, obj1, obj2, **hints): # noqa: F841 + # Allow relations if both objects are in either "default" or "admin" db connectors + if {obj1._state.db, obj2._state.db} <= {self.default_db, self.admin_db}: + return True + return None diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index ca5f1a5b07..b7118f7508 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.4.0 + version: 1.5.0 description: |- Prowler API specification. @@ -4996,6 +4996,35 @@ paths: schema: $ref: '#/components/schemas/TokenRefreshResponse' description: '' + /api/v1/tokens/switch: + post: + operationId: tokens_switch_create + description: Switch tenant by providing a valid tenant ID. The authenticated + user must belong to the tenant. + summary: Switch tenant using a valid tenant ID + tags: + - Token + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/TokenSwitchTenantRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenSwitchTenantRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenSwitchTenantRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/TokenSwitchTenantResponse' + description: '' /api/v1/users: get: operationId: users_list @@ -10047,6 +10076,81 @@ components: $ref: '#/components/schemas/Token' required: - data + TokenSwitchTenant: + type: object + required: + - type + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/TokenSwitchTenantTypeEnum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + attributes: + type: object + properties: + tenant_id: + type: string + format: uuid + writeOnly: true + description: The tenant ID for which to request a new token. + access: + type: string + readOnly: true + refresh: + type: string + readOnly: true + required: + - tenant_id + TokenSwitchTenantRequest: + type: object + properties: + data: + type: object + required: + - type + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - tokens-switch-tenant + attributes: + type: object + properties: + tenant_id: + type: string + format: uuid + writeOnly: true + description: The tenant ID for which to request a new token. + access: + type: string + readOnly: true + minLength: 1 + refresh: + type: string + readOnly: true + minLength: 1 + required: + - tenant_id + required: + - data + TokenSwitchTenantResponse: + type: object + properties: + data: + $ref: '#/components/schemas/TokenSwitchTenant' + required: + - data + TokenSwitchTenantTypeEnum: + type: string + enum: + - tokens-switch-tenant TokenTypeEnum: type: string enum: diff --git a/api/src/backend/api/tests/integration/test_authentication.py b/api/src/backend/api/tests/integration/test_authentication.py index 85dbbad315..f41e8207c4 100644 --- a/api/src/backend/api/tests/integration/test_authentication.py +++ b/api/src/backend/api/tests/integration/test_authentication.py @@ -3,6 +3,8 @@ from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header from django.urls import reverse from rest_framework.test import APIClient +from api.models import Membership, User + @pytest.mark.django_db def test_basic_authentication(): @@ -177,3 +179,122 @@ def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fi user2_me = client.get(reverse("user-me"), headers=user2_headers) assert user2_me.status_code == 200 assert user2_me.json()["data"]["attributes"]["email"] == user2_email + + +@pytest.mark.django_db +class TestTokenSwitchTenant: + def test_switch_tenant_with_valid_token(self, tenants_fixture, providers_fixture): + client = APIClient() + + test_user = "test_email@prowler.com" + test_password = "test_password" + + # Check that we can create a new user without any kind of authentication + user_creation_response = client.post( + reverse("user-list"), + data={ + "data": { + "type": "users", + "attributes": { + "name": "test", + "email": test_user, + "password": test_password, + }, + } + }, + format="vnd.api+json", + ) + assert user_creation_response.status_code == 201 + + # Create a new relationship between this user and another tenant + tenant_id = tenants_fixture[0].id + user_instance = User.objects.get(email=test_user) + Membership.objects.create(user=user_instance, tenant_id=tenant_id) + + # Check that using our new user's credentials we can authenticate and get the providers + access_token, _ = get_api_tokens(client, test_user, test_password) + auth_headers = get_authorization_header(access_token) + + user_me_response = client.get( + reverse("user-me"), + headers=auth_headers, + ) + assert user_me_response.status_code == 200 + # Assert this user belongs to two tenants + assert ( + user_me_response.json()["data"]["relationships"]["memberships"]["meta"][ + "count" + ] + == 2 + ) + + provider_response = client.get( + reverse("provider-list"), + headers=auth_headers, + ) + assert provider_response.status_code == 200 + # Empty response since there are no providers in this tenant + assert not provider_response.json()["data"] + + switch_tenant_response = client.post( + reverse("token-switch"), + data={ + "data": { + "type": "tokens-switch-tenant", + "attributes": {"tenant_id": tenant_id}, + } + }, + headers=auth_headers, + ) + assert switch_tenant_response.status_code == 200 + new_access_token = switch_tenant_response.json()["data"]["attributes"]["access"] + new_auth_headers = get_authorization_header(new_access_token) + + provider_response = client.get( + reverse("provider-list"), + headers=new_auth_headers, + ) + assert provider_response.status_code == 200 + # Now it must be data because we switched to another tenant with providers + assert provider_response.json()["data"] + + def test_switch_tenant_with_invalid_token(self, create_test_user, tenants_fixture): + client = APIClient() + + access_token, refresh_token = get_api_tokens( + client, create_test_user.email, TEST_PASSWORD + ) + auth_headers = get_authorization_header(access_token) + + invalid_token_response = client.post( + reverse("token-switch"), + data={ + "data": { + "type": "tokens-switch-tenant", + "attributes": {"tenant_id": "invalid_tenant_id"}, + } + }, + headers=auth_headers, + ) + assert invalid_token_response.status_code == 400 + assert invalid_token_response.json()["errors"][0]["code"] == "invalid" + assert ( + invalid_token_response.json()["errors"][0]["detail"] + == "Must be a valid UUID." + ) + + invalid_tenant_response = client.post( + reverse("token-switch"), + data={ + "data": { + "type": "tokens-switch-tenant", + "attributes": {"tenant_id": tenants_fixture[-1].id}, + } + }, + headers=auth_headers, + ) + assert invalid_tenant_response.status_code == 400 + assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid" + assert invalid_tenant_response.json()["errors"][0]["detail"] == ( + "Tenant does not exist or user is not a " "member." + ) diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 1f60669039..9dd86daeaf 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -1,15 +1,25 @@ from datetime import datetime, timezone +from allauth.socialaccount.providers.oauth2.client import OAuth2Client +from rest_framework.exceptions import NotFound, ValidationError + +from api.db_router import MainRouter +from api.exceptions import InvitationTokenExpiredException +from api.models import Invitation, Provider from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.common.models import Connection from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider -from rest_framework.exceptions import ValidationError, NotFound -from api.db_router import MainRouter -from api.exceptions import InvitationTokenExpiredException -from api.models import Provider, Invitation + +class CustomOAuth2Client(OAuth2Client): + def __init__(self, client_id, secret, *args, **kwargs): + # Remove any duplicate "scope_delimiter" from kwargs + # Bug present in dj-rest-auth after version v7.0.1 + # https://github.com/iMerica/dj-rest-auth/issues/673 + kwargs.pop("scope_delimiter", None) + super().__init__(client_id, secret, *args, **kwargs) def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict: diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 4f17d2d61c..1317904f88 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -38,7 +38,65 @@ from api.rls import Tenant # Tokens -class TokenSerializer(TokenObtainPairSerializer): +def generate_tokens(user: User, tenant_id: str) -> dict: + try: + refresh = RefreshToken.for_user(user) + except InvalidKeyError: + # Handle invalid key error + raise ValidationError( + { + "detail": "Token generation failed due to invalid key configuration. Provide valid " + "DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment." + } + ) + except Exception as e: + raise ValidationError({"detail": str(e)}) + + # Post-process the tokens + # Set the tenant_id + refresh["tenant_id"] = tenant_id + + # Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a + # way to set the nbf claim + refresh.payload["nbf"] = refresh["iat"] + + # Get the access token + access = refresh.access_token + + if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]: + update_last_login(None, user) + + return {"access": str(access), "refresh": str(refresh)} + + +class BaseTokenSerializer(TokenObtainPairSerializer): + def custom_validate(self, attrs, social: bool = False): + email = attrs.get("email") + password = attrs.get("password") + tenant_id = str(attrs.get("tenant_id", "")) + + # Authenticate user + user = ( + User.objects.get(email=email) + if social + else authenticate(username=email, password=password) + ) + if user is None: + raise ValidationError("Invalid credentials") + + if tenant_id: + if not user.is_member_of_tenant(tenant_id): + raise ValidationError("Tenant does not exist or user is not a member.") + else: + first_membership = user.memberships.order_by("date_joined").first() + if first_membership is None: + raise ValidationError("User has no memberships.") + tenant_id = str(first_membership.tenant_id) + + return generate_tokens(user, tenant_id) + + +class TokenSerializer(BaseTokenSerializer): email = serializers.EmailField(write_only=True) password = serializers.CharField(write_only=True) tenant_id = serializers.UUIDField( @@ -56,53 +114,25 @@ class TokenSerializer(TokenObtainPairSerializer): resource_name = "tokens" def validate(self, attrs): - email = attrs.get("email") - password = attrs.get("password") - tenant_id = str(attrs.get("tenant_id", "")) + return super().custom_validate(attrs) - # Authenticate user - user = authenticate(username=email, password=password) - if user is None: - raise ValidationError("Invalid credentials") - if tenant_id: - if not user.is_member_of_tenant(tenant_id): - raise ValidationError("Tenant does not exist or user is not a member.") - else: - first_membership = user.memberships.order_by("date_joined").first() - if first_membership is None: - raise ValidationError("User has no memberships.") - tenant_id = str(first_membership.tenant_id) +class TokenSocialLoginSerializer(BaseTokenSerializer): + email = serializers.EmailField(write_only=True) - # Generate tokens - try: - refresh = RefreshToken.for_user(user) - except InvalidKeyError: - # Handle invalid key error - raise ValidationError( - { - "detail": "Token generation failed due to invalid key configuration. Provide valid " - "DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment." - } - ) - except Exception as e: - raise ValidationError({"detail": str(e)}) + # Output tokens + refresh = serializers.CharField(read_only=True) + access = serializers.CharField(read_only=True) - # Post-process the tokens - # Set the tenant_id - refresh["tenant_id"] = tenant_id + class JSONAPIMeta: + resource_name = "tokens" - # Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a - # way to set the nbf claim - refresh.payload["nbf"] = refresh["iat"] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields.pop("password", None) - # Get the access token - access = refresh.access_token - - if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]: - update_last_login(None, user) - - return {"access": str(access), "refresh": str(refresh)} + def validate(self, attrs): + return super().custom_validate(attrs, social=True) # TODO: Check if we can change the parent class to TokenRefreshSerializer from rest_framework_simplejwt.serializers @@ -140,6 +170,30 @@ class TokenRefreshSerializer(serializers.Serializer): raise ValidationError({"refresh": "Invalid or expired token"}) +class TokenSwitchTenantSerializer(serializers.Serializer): + tenant_id = serializers.UUIDField( + write_only=True, help_text="The tenant ID for which to request a new token." + ) + access = serializers.CharField(read_only=True) + refresh = serializers.CharField(read_only=True) + + class JSONAPIMeta: + resource_name = "tokens-switch-tenant" + + def validate(self, attrs): + request = self.context["request"] + user = request.user + + if not user.is_authenticated: + raise ValidationError("Invalid or expired token.") + + tenant_id = str(attrs.get("tenant_id")) + if not user.is_member_of_tenant(tenant_id): + raise ValidationError("Tenant does not exist or user is not a member.") + + return generate_tokens(user, tenant_id) + + # Base diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index 4ff587d913..f26e0cb8f3 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -6,7 +6,10 @@ from api.v1.views import ( ComplianceOverviewViewSet, CustomTokenObtainView, CustomTokenRefreshView, + CustomTokenSwitchTenantView, FindingViewSet, + GithubSocialLoginView, + GoogleSocialLoginView, InvitationAcceptViewSet, InvitationViewSet, MembershipViewSet, @@ -56,6 +59,7 @@ users_router.register(r"memberships", MembershipViewSet, basename="user-membersh urlpatterns = [ path("tokens", CustomTokenObtainView.as_view(), name="token-obtain"), path("tokens/refresh", CustomTokenRefreshView.as_view(), name="token-refresh"), + path("tokens/switch", CustomTokenSwitchTenantView.as_view(), name="token-switch"), path( "providers/secrets", ProviderSecretViewSet.as_view({"get": "list", "post": "create"}), @@ -106,6 +110,8 @@ urlpatterns = [ ), name="provider_group-providers-relationship", ), + path("tokens/google", GoogleSocialLoginView.as_view(), name="token-google"), + path("tokens/github", GithubSocialLoginView.as_view(), name="token-github"), path("", include(router.urls)), path("", include(tenants_router.urls)), path("", include(users_router.urls)), diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 722ecbfd7d..88500698da 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1,4 +1,11 @@ +from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter +from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from celery.result import AsyncResult +from config.settings.social_login import ( + GITHUB_OAUTH_CALLBACK_URL, + GOOGLE_OAUTH_CALLBACK_URL, +) +from dj_rest_auth.registration.views import SocialLoginView from django.conf import settings as django_settings from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.search import SearchQuery @@ -81,7 +88,7 @@ from api.models import ( from api.pagination import ComplianceOverviewPagination from api.rbac.permissions import Permissions, get_providers, get_role from api.rls import Tenant -from api.utils import validate_invitation +from api.utils import CustomOAuth2Client, validate_invitation from api.uuid_utils import datetime_to_uuid7 from api.v1.serializers import ( ComplianceOverviewFullSerializer, @@ -121,6 +128,8 @@ from api.v1.serializers import ( TenantSerializer, TokenRefreshSerializer, TokenSerializer, + TokenSocialLoginSerializer, + TokenSwitchTenantSerializer, UserCreateSerializer, UserRoleRelationshipSerializer, UserSerializer, @@ -187,13 +196,43 @@ class CustomTokenRefreshView(GenericAPIView): ) +@extend_schema( + tags=["Token"], + summary="Switch tenant using a valid tenant ID", + description="Switch tenant by providing a valid tenant ID. The authenticated user must belong to the tenant.", +) +class CustomTokenSwitchTenantView(GenericAPIView): + permission_classes = [permissions.IsAuthenticated] + resource_name = "tokens-switch-tenant" + serializer_class = TokenSwitchTenantSerializer + http_method_names = ["post"] + + def post(self, request): + serializer = TokenSwitchTenantSerializer( + data=request.data, context={"request": request} + ) + + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + + return Response( + data={ + "type": "tokens-switch-tenant", + "attributes": serializer.validated_data, + }, + status=status.HTTP_200_OK, + ) + + @extend_schema(exclude=True) class SchemaView(SpectacularAPIView): serializer_class = None def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.4.0" + spectacular_settings.VERSION = "1.5.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -253,6 +292,58 @@ class SchemaView(SpectacularAPIView): return super().get(request, *args, **kwargs) +@extend_schema(exclude=True) +class GoogleSocialLoginView(SocialLoginView): + adapter_class = GoogleOAuth2Adapter + client_class = CustomOAuth2Client + callback_url = GOOGLE_OAUTH_CALLBACK_URL + + def get_response(self): + original_response = super().get_response() + + if self.user and self.user.is_authenticated: + serializer = TokenSocialLoginSerializer(data={"email": self.user.email}) + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + return Response( + data={ + "type": "google-social-tokens", + "attributes": serializer.validated_data, + }, + status=status.HTTP_200_OK, + ) + return original_response + + +@extend_schema(exclude=True) +class GithubSocialLoginView(SocialLoginView): + adapter_class = GitHubOAuth2Adapter + client_class = CustomOAuth2Client + callback_url = GITHUB_OAUTH_CALLBACK_URL + + def get_response(self): + original_response = super().get_response() + + if self.user and self.user.is_authenticated: + serializer = TokenSocialLoginSerializer(data={"email": self.user.email}) + + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + + return Response( + data={ + "type": "github-social-tokens", + "attributes": serializer.validated_data, + }, + status=status.HTTP_200_OK, + ) + return original_response + + @extend_schema_view( list=extend_schema( tags=["User"], diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 6494bd0ff3..7820e9126c 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -4,6 +4,7 @@ from config.custom_logging import LOGGING # noqa from config.env import BASE_DIR, env # noqa from config.settings.celery import * # noqa from config.settings.partitions import * # noqa +from config.settings.social_login import * # noqa SECRET_KEY = env("SECRET_KEY", default="secret") DEBUG = env.bool("DJANGO_DEBUG", default=False) @@ -29,6 +30,13 @@ INSTALLED_APPS = [ "django_celery_results", "django_celery_beat", "rest_framework_simplejwt.token_blacklist", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.google", + "allauth.socialaccount.providers.github", + "dj_rest_auth.registration", + "rest_framework.authtoken", ] MIDDLEWARE = [ @@ -42,8 +50,11 @@ MIDDLEWARE = [ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "api.middleware.APILoggingMiddleware", + "allauth.account.middleware.AccountMiddleware", ] +SITE_ID = 1 + CORS_ALLOWED_ORIGINS = ["http://localhost", "http://127.0.0.1"] ROOT_URLCONF = "config.urls" diff --git a/api/src/backend/config/settings/social_login.py b/api/src/backend/config/settings/social_login.py new file mode 100644 index 0000000000..035a166279 --- /dev/null +++ b/api/src/backend/config/settings/social_login.py @@ -0,0 +1,53 @@ +from config.env import env + +# Google Oauth settings +GOOGLE_OAUTH_CLIENT_ID = env("DJANGO_GOOGLE_OAUTH_CLIENT_ID", default="") +GOOGLE_OAUTH_CLIENT_SECRET = env("DJANGO_GOOGLE_OAUTH_CLIENT_SECRET", default="") +GOOGLE_OAUTH_CALLBACK_URL = env("DJANGO_GOOGLE_OAUTH_CALLBACK_URL", default="") + +GITHUB_OAUTH_CLIENT_ID = env("DJANGO_GITHUB_OAUTH_CLIENT_ID", default="") +GITHUB_OAUTH_CLIENT_SECRET = env("DJANGO_GITHUB_OAUTH_CLIENT_SECRET", default="") +GITHUB_OAUTH_CALLBACK_URL = env("DJANGO_GITHUB_OAUTH_CALLBACK_URL", default="") + +# Allauth settings +ACCOUNT_LOGIN_METHODS = {"email"} # Use Email / Password authentication +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = "none" # Do not require email confirmation +ACCOUNT_USER_MODEL_USERNAME_FIELD = None +REST_AUTH = { + "TOKEN_MODEL": None, + "REST_USE_JWT": True, +} +# django-allauth (social) +# Authenticate if local account with this email address already exists +SOCIALACCOUNT_EMAIL_AUTHENTICATION = True +# Connect local account and social account if local account with that email address already exists +SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True +SOCIALACCOUNT_ADAPTER = "api.adapters.ProwlerSocialAccountAdapter" +SOCIALACCOUNT_PROVIDERS = { + "google": { + "APP": { + "client_id": GOOGLE_OAUTH_CLIENT_ID, + "secret": GOOGLE_OAUTH_CLIENT_SECRET, + "key": "", + }, + "SCOPE": [ + "email", + "profile", + ], + "AUTH_PARAMS": { + "access_type": "online", + }, + }, + "github": { + "APP": { + "client_id": GITHUB_OAUTH_CLIENT_ID, + "secret": GITHUB_OAUTH_CLIENT_SECRET, + }, + "SCOPE": [ + "user", + "read:org", + ], + }, +} From 88810d2bb562020b845e22914df9986f06d5a6c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 11:36:07 -0500 Subject: [PATCH 07/18] chore(deps-dev): bump mkdocs-material from 9.6.3 to 9.6.4 (#6913) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1697f1d1fd..2fad55312d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2537,13 +2537,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd [[package]] name = "mkdocs-material" -version = "9.6.3" +version = "9.6.4" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.6.3-py3-none-any.whl", hash = "sha256:1125622067e26940806701219303b27c0933e04533560725d97ec26fd16a39cf"}, - {file = "mkdocs_material-9.6.3.tar.gz", hash = "sha256:c87f7d1c39ce6326da5e10e232aed51bae46252e646755900f4b0fc9192fa832"}, + {file = "mkdocs_material-9.6.4-py3-none-any.whl", hash = "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f"}, + {file = "mkdocs_material-9.6.4.tar.gz", hash = "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867"}, ] [package.dependencies] @@ -5091,4 +5091,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "7182befee2c0494d066d4715935ee4aaf6a7dfe24e6cfea429be90fad03c4024" +content-hash = "a50820c535211dcb2da888c047d7f2e2300ef1580026eedb72995620840a45e0" diff --git a/pyproject.toml b/pyproject.toml index 05f233d132..305e265ddf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ optional = true [tool.poetry.group.docs.dependencies] mkdocs = "1.6.1" mkdocs-git-revision-date-localized-plugin = "1.3.0" -mkdocs-material = "9.6.3" +mkdocs-material = "9.6.4" mkdocs-material-extensions = "1.3.1" [tool.poetry.scripts] From 736d40546aa15ff17be146d0e05e75276b63b666 Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Thu, 13 Feb 2025 12:18:50 -0500 Subject: [PATCH 08/18] fix(gcp): handle DNS Managed Zone with no DNSSEC (#6924) --- prowler/providers/gcp/services/dns/dns_service.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/prowler/providers/gcp/services/dns/dns_service.py b/prowler/providers/gcp/services/dns/dns_service.py index 5783dbf137..dc01c5cf95 100644 --- a/prowler/providers/gcp/services/dns/dns_service.py +++ b/prowler/providers/gcp/services/dns/dns_service.py @@ -24,11 +24,13 @@ class DNS(GCPService): ManagedZone( name=managed_zone["name"], id=managed_zone["id"], - dnssec=managed_zone.get("dnssecConfig", {})["state"] + dnssec=managed_zone.get("dnssecConfig", {}).get( + "state", "" + ) == "on", - key_specs=managed_zone.get("dnssecConfig", {})[ - "defaultKeySpecs" - ], + key_specs=managed_zone.get("dnssecConfig", {}).get( + "defaultKeySpecs", [] + ), project_id=project_id, ) ) From 580e11126c3c7a0332f11f5e790e03339801414c Mon Sep 17 00:00:00 2001 From: Kay Agahd Date: Thu, 13 Feb 2025 18:22:09 +0100 Subject: [PATCH 09/18] fix(aws): codebuild service threw KeyError for projects type CODEPIPELINE (#6919) --- .../providers/aws/services/codebuild/codebuild_service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/prowler/providers/aws/services/codebuild/codebuild_service.py b/prowler/providers/aws/services/codebuild/codebuild_service.py index 1ed4226586..a1e00790bb 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_service.py +++ b/prowler/providers/aws/services/codebuild/codebuild_service.py @@ -84,12 +84,13 @@ class Codebuild(AWSService): if project_info["source"]["type"] != "NO_SOURCE": project.source = Source( type=project_info["source"]["type"], - location=project_info["source"]["location"], + location=project_info["source"].get("location", ""), ) project.secondary_sources = [] for secondary_source in project_info.get("secondarySources", []): source_obj = Source( - type=secondary_source["type"], location=secondary_source["location"] + type=secondary_source["type"], + location=secondary_source.get("location", ""), ) project.secondary_sources.append(source_obj) environment = project_info.get("environment", {}) From d76c5bd658411403f62f49ff610de26f878f9b3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:17:25 -0500 Subject: [PATCH 10/18] chore(deps): bump trufflesecurity/trufflehog from 3.88.7 to 3.88.8 (#6931) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/find-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index d49c77c221..bf77ddc487 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.88.7 + uses: trufflesecurity/trufflehog@v3.88.8 with: path: ./ base: ${{ github.event.repository.default_branch }} From b79508956a57c756f4dc6f23676d5ec14b348e98 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 14 Feb 2025 10:32:34 +0100 Subject: [PATCH 11/18] fix(issue pages): apply sorting by default in issue pages (#6934) --- ui/components/ui/sidebar/sidebar-items.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/components/ui/sidebar/sidebar-items.tsx b/ui/components/ui/sidebar/sidebar-items.tsx index 3123e81291..ce4c2b1e89 100644 --- a/ui/components/ui/sidebar/sidebar-items.tsx +++ b/ui/components/ui/sidebar/sidebar-items.tsx @@ -152,37 +152,37 @@ export const sectionItems: SidebarItem[] = [ items: [ { key: "cloud-misconfigurations", - href: "/findings?filter[status__in]=FAIL&sort=severity", + href: "/findings?filter[status__in]=FAIL&sort=severity,-inserted_at", icon: "solar:danger-triangle-linear", title: "Cloud Misconfigurations", }, { key: "iam-issues", - 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", + 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", icon: "solar:shield-user-linear", title: "IAM Issues", }, { key: "aws-findings", - href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=aws&sort=severity", + href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=aws&sort=severity,-inserted_at", icon: "ri:amazon-line", title: "AWS Top Failed Findings", }, { key: "azure-findings", - href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=azure&sort=severity", + href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=azure&sort=severity,-inserted_at", icon: "ri:microsoft-line", title: "Azure Top Failed Findings", }, { key: "gcp-findings", - href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=gcp&sort=severity", + href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=gcp&sort=severity,-inserted_at", icon: "ri:google-line", title: "GCP Top Failed Findings", }, { key: "kubernetes-findings", - href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=kubernetes&sort=severity", + href: "/findings?filter[status__in]=FAIL&filter[severity__in]=critical%2Chigh%2Cmedium&filter[provider_type__in]=kubernetes&sort=severity,-inserted_at", icon: "ri:steering-2-line", title: "Kubernetes Top Failed Findings", }, From a32ca192510cdf4e437ce4081925abc50dc5ae8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:16:22 +0100 Subject: [PATCH 12/18] chore(gha): add tag for api and ui images on push to master (#6920) --- .github/workflows/api-build-lint-push-containers.yml | 7 +++++++ .github/workflows/ui-build-lint-push-containers.yml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/api-build-lint-push-containers.yml b/.github/workflows/api-build-lint-push-containers.yml index 1a463ff7bc..18553d73b7 100644 --- a/.github/workflows/api-build-lint-push-containers.yml +++ b/.github/workflows/api-build-lint-push-containers.yml @@ -63,6 +63,12 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set short git commit SHA + id: vars + run: | + shortSha=$(git rev-parse --short ${{ github.sha }}) + echo "SHORT_SHA=shortSha" >> $GITHUB_ENV + - name: Login to DockerHub uses: docker/login-action@v3 with: @@ -82,6 +88,7 @@ jobs: push: true tags: | ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.SHORT_SHA }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/ui-build-lint-push-containers.yml b/.github/workflows/ui-build-lint-push-containers.yml index a34c2379e6..79e8d52a9e 100644 --- a/.github/workflows/ui-build-lint-push-containers.yml +++ b/.github/workflows/ui-build-lint-push-containers.yml @@ -63,6 +63,12 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set short git commit SHA + id: vars + run: | + shortSha=$(git rev-parse --short ${{ github.sha }}) + echo "SHORT_SHA=shortSha" >> $GITHUB_ENV + - name: Login to DockerHub uses: docker/login-action@v3 with: @@ -82,6 +88,7 @@ jobs: push: true tags: | ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.SHORT_SHA }} cache-from: type=gha cache-to: type=gha,mode=max From ff72b7eea18e16ab5b32b5de3af3eae167b16c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:26:26 +0100 Subject: [PATCH 13/18] fix(gha): fix short sha step (#6939) --- .github/workflows/api-build-lint-push-containers.yml | 2 +- .github/workflows/ui-build-lint-push-containers.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/api-build-lint-push-containers.yml b/.github/workflows/api-build-lint-push-containers.yml index 18553d73b7..62db7e4172 100644 --- a/.github/workflows/api-build-lint-push-containers.yml +++ b/.github/workflows/api-build-lint-push-containers.yml @@ -67,7 +67,7 @@ jobs: id: vars run: | shortSha=$(git rev-parse --short ${{ github.sha }}) - echo "SHORT_SHA=shortSha" >> $GITHUB_ENV + echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV - name: Login to DockerHub uses: docker/login-action@v3 diff --git a/.github/workflows/ui-build-lint-push-containers.yml b/.github/workflows/ui-build-lint-push-containers.yml index 79e8d52a9e..31a66012db 100644 --- a/.github/workflows/ui-build-lint-push-containers.yml +++ b/.github/workflows/ui-build-lint-push-containers.yml @@ -67,7 +67,7 @@ jobs: id: vars run: | shortSha=$(git rev-parse --short ${{ github.sha }}) - echo "SHORT_SHA=shortSha" >> $GITHUB_ENV + echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV - name: Login to DockerHub uses: docker/login-action@v3 From 0ec4366f4c60cdb73ceb7c84e19877aba7b06341 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:09:33 -0500 Subject: [PATCH 14/18] chore(deps): bump google-api-python-client from 2.160.0 to 2.161.0 (#6933) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2fad55312d..80468d7c88 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1693,13 +1693,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.160.0" +version = "2.161.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.160.0-py2.py3-none-any.whl", hash = "sha256:63d61fb3e4cf3fb31a70a87f45567c22f6dfe87bbfa27252317e3e2c42900db4"}, - {file = "google_api_python_client-2.160.0.tar.gz", hash = "sha256:a8ccafaecfa42d15d5b5c3134ced8de08380019717fc9fb1ed510ca58eca3b7e"}, + {file = "google_api_python_client-2.161.0-py2.py3-none-any.whl", hash = "sha256:9476a5a4f200bae368140453df40f9cda36be53fa7d0e9a9aac4cdb859a26448"}, + {file = "google_api_python_client-2.161.0.tar.gz", hash = "sha256:324c0cce73e9ea0a0d2afd5937e01b7c2d6a4d7e2579cdb6c384f9699d6c9f37"}, ] [package.dependencies] @@ -5091,4 +5091,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "a50820c535211dcb2da888c047d7f2e2300ef1580026eedb72995620840a45e0" +content-hash = "f20ee317a5d1cb29b16bf7bb0e8a7c6532358042c5164ca99c2cc4763641eb73" diff --git a/pyproject.toml b/pyproject.toml index 305e265ddf..f311432c63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ cryptography = "43.0.1" dash = "2.18.2" dash-bootstrap-components = "1.6.0" detect-secrets = "1.5.0" -google-api-python-client = "2.160.0" +google-api-python-client = "2.161.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "32.0.0" From bcbc9bf6751181f81e081f77ad25072963d22ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Fri, 14 Feb 2025 21:16:21 +0100 Subject: [PATCH 15/18] fix(gcp): Correct false positive when `sslMode=ENCRYPTED_ONLY` in CloudSQL (#6936) --- .../cloudsql_instance_ssl_connections.py | 7 +-- .../cloudsql_instance_ssl_connections_test.py | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.py b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.py index 4882defaa6..f2badd878a 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.py +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.py @@ -11,12 +11,13 @@ class cloudsql_instance_ssl_connections(Check): report.status_extended = ( f"Database Instance {instance.name} requires SSL connections." ) - if ( - not instance.require_ssl - or instance.ssl_mode == "ALLOW_UNENCRYPTED_AND_ENCRYPTED" + + if (instance.ssl_mode == "ALLOW_UNENCRYPTED_AND_ENCRYPTED") or ( + instance.ssl_mode == "SSL_MODE_UNSPECIFIED" and not instance.require_ssl ): report.status = "FAIL" report.status_extended = f"Database Instance {instance.name} does not require SSL connections." + findings.append(report) return findings diff --git a/tests/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections_test.py b/tests/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections_test.py index 83712c3a8d..8b398aee00 100644 --- a/tests/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections_test.py +++ b/tests/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections_test.py @@ -74,6 +74,52 @@ class Test_cloudsql_instance_ssl_connections: assert result[0].location == GCP_EU1_LOCATION assert result[0].project_id == GCP_PROJECT_ID + def test_cloudsql_instance_ssl_connections_disabled_and_ssl_mode_encrypted(self): + cloudsql_client = mock.MagicMock() + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), mock.patch( + "prowler.providers.gcp.services.cloudsql.cloudsql_instance_ssl_connections.cloudsql_instance_ssl_connections.cloudsql_client", + new=cloudsql_client, + ): + from prowler.providers.gcp.services.cloudsql.cloudsql_instance_ssl_connections.cloudsql_instance_ssl_connections import ( + cloudsql_instance_ssl_connections, + ) + from prowler.providers.gcp.services.cloudsql.cloudsql_service import ( + Instance, + ) + + cloudsql_client.instances = [ + Instance( + name="instance1", + version="POSTGRES_15", + ip_addresses=[], + region=GCP_EU1_LOCATION, + public_ip=False, + require_ssl=False, + ssl_mode="ENCRYPTED_ONLY", + automated_backups=True, + authorized_networks=[], + flags=[], + project_id=GCP_PROJECT_ID, + ) + ] + + check = cloudsql_instance_ssl_connections() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Database Instance instance1 requires SSL connections." + ) + assert result[0].resource_id == "instance1" + assert result[0].resource_name == "instance1" + assert result[0].location == GCP_EU1_LOCATION + assert result[0].project_id == GCP_PROJECT_ID + def test_cloudsql_instance_ssl_connections_enabled_and_ssl_mode_not_encrypted(self): cloudsql_client = mock.MagicMock() From 7f6bfb7b3ecc420cc71e2262215d59e468e8a3e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:54:52 +0100 Subject: [PATCH 16/18] chore(deps): bump trufflesecurity/trufflehog from 3.88.8 to 3.88.9 (#6943) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/find-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index bf77ddc487..6c6a634d09 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.88.8 + uses: trufflesecurity/trufflehog@v3.88.9 with: path: ./ base: ${{ github.event.repository.default_branch }} From 928ccfefb8897783ea82fb18ed64c6c547af8e9b Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Mon, 17 Feb 2025 21:40:15 +0545 Subject: [PATCH 17/18] chore(regions_update): Changes in regions for AWS services (#6944) Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 4e05963af0..29f4541927 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -9737,6 +9737,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -9750,6 +9751,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", From ed5a0ae45a3a609c7f57170baa8e1a1a60720a07 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:08:28 +0100 Subject: [PATCH 18/18] fix(cloudfront): Incorrect bucket name retrievement (#6947) --- .../cloudfront_distributions_s3_origin_non_existent_bucket.py | 2 +- prowler/providers/aws/services/s3/s3_service.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py index a9da703239..ea932ba027 100644 --- a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py @@ -16,7 +16,7 @@ class cloudfront_distributions_s3_origin_non_existent_bucket(Check): for origin in distribution.origins: if origin.s3_origin_config: - bucket_name = origin.domain_name.split(".")[0] + bucket_name = origin.domain_name.split(".s3")[0] if not s3_client._head_bucket(bucket_name): non_existent_buckets.append(bucket_name) diff --git a/prowler/providers/aws/services/s3/s3_service.py b/prowler/providers/aws/services/s3/s3_service.py index 8939da0987..3894e1001d 100644 --- a/prowler/providers/aws/services/s3/s3_service.py +++ b/prowler/providers/aws/services/s3/s3_service.py @@ -496,6 +496,7 @@ class S3(AWSService): ) return False else: + # Bucket exists but we don't have access to it logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" )