From 86950c3a0a3a6a84e3e3c773cf1a5ade13f992be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:47:09 +0100 Subject: [PATCH 01/27] chore(deps): bump msgraph-sdk from 1.17.0 to 1.18.0 (#6679) 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 4333e3d413a..3d8acd35f82 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2744,13 +2744,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] [[package]] name = "msgraph-sdk" -version = "1.17.0" +version = "1.18.0" description = "The Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" files = [ - {file = "msgraph_sdk-1.17.0-py3-none-any.whl", hash = "sha256:5582a258ded19a486ab407a67b5f65d666758a63864da77bd20c2581d1c00fba"}, - {file = "msgraph_sdk-1.17.0.tar.gz", hash = "sha256:577e41942b0f794b8cf2f54db030bc039a750a81b515dcd0ba1d66fd961fa7bf"}, + {file = "msgraph_sdk-1.18.0-py3-none-any.whl", hash = "sha256:f09b015bb9d7690bc6f30c9a28f9a414107aaf06be4952c27b3653dcdf33f2a3"}, + {file = "msgraph_sdk-1.18.0.tar.gz", hash = "sha256:ef49166ada7b459b5a843ceb3d253c1ab99d8987ebf3112147eb6cbcaa101793"}, ] [package.dependencies] @@ -5211,4 +5211,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "0187fa6502bcbf36a5d825d8b0af75c2b37849ec265bf8e9278ef66479dc327e" +content-hash = "3686f90c22b22e439769ee79ae9e007d9730b7d33426f0501ae5bf4f51217b81" diff --git a/pyproject.toml b/pyproject.toml index 36c7931dedb..785b34afe31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "31.0.0" microsoft-kiota-abstractions = "1.6.8" -msgraph-sdk = "1.17.0" +msgraph-sdk = "1.18.0" numpy = "2.0.2" pandas = "2.2.3" py-ocsf-models = "0.2.0" From 62139e252ab77433da6fb030cceddd27c87ba6f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:40:11 +0100 Subject: [PATCH 02/27] chore(deps): bump azure-mgmt-web from 7.3.1 to 8.0.0 (#6680) 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 3d8acd35f82..4e293e6759e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -629,13 +629,13 @@ msrest = ">=0.7.1" [[package]] name = "azure-mgmt-web" -version = "7.3.1" +version = "8.0.0" description = "Microsoft Azure Web Apps Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-mgmt-web-7.3.1.tar.gz", hash = "sha256:87b771436bc99a7a8df59d0ad185b96879a06dce14764a06b3fc3dafa8fcb56b"}, - {file = "azure_mgmt_web-7.3.1-py3-none-any.whl", hash = "sha256:ccf881e3ab31c3fdbf9cbff32773d9c0006b5dcd621ea074d7ec89e51049fb72"}, + {file = "azure_mgmt_web-8.0.0-py3-none-any.whl", hash = "sha256:0536aac05bfc673b56ed930f2966b77856e84df675d376e782a7af6bb92449af"}, + {file = "azure_mgmt_web-8.0.0.tar.gz", hash = "sha256:c8d9c042c09db7aacb20270a9effed4d4e651e365af32d80897b84dc7bf35098"}, ] [package.dependencies] @@ -5211,4 +5211,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "3686f90c22b22e439769ee79ae9e007d9730b7d33426f0501ae5bf4f51217b81" +content-hash = "f2d3920001616ff9b66353b61d20dadc358787aa154c31fcbd8f4baf45c36498" diff --git a/pyproject.toml b/pyproject.toml index 785b34afe31..ff3156c743b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ azure-mgmt-security = "7.0.0" azure-mgmt-sql = "3.0.1" azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" -azure-mgmt-web = "7.3.1" +azure-mgmt-web = "8.0.0" azure-storage-blob = "12.24.1" boto3 = "1.35.99" botocore = "1.35.99" From bcc246d950b3e6aaa8cdd831a5912e2731e68321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Fri, 24 Jan 2025 16:42:45 +0100 Subject: [PATCH 03/27] fix(cloudsql): add trusted client certificates case for `cloudsql_instance_ssl_connections` (#6682) --- .../cloudsql_instance_ssl_connections.py | 5 +- .../cloudsql_instance_ssl_connections_test.py | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) 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 2fa33fbdd83..4882defaa65 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,7 +11,10 @@ def execute(self) -> Check_Report_GCP: report.status_extended = ( f"Database Instance {instance.name} requires SSL connections." ) - if not instance.require_ssl or instance.ssl_mode != "ENCRYPTED_ONLY": + if ( + not instance.require_ssl + or instance.ssl_mode == "ALLOW_UNENCRYPTED_AND_ENCRYPTED" + ): report.status = "FAIL" report.status_extended = f"Database Instance {instance.name} does not require SSL connections." findings.append(report) 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 d42d447019a..83712c3a8d4 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 @@ -167,3 +167,51 @@ def test_cloudsql_instance_ssl_connections_disabled_and_ssl_mode_not_encrypted( 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_with_trusted_client_certificates( + 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=True, + ssl_mode="TRUSTED_CLIENT_CERTIFICATE_REQUIRED", + 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 From ccdb54d7c361c3eb88651d75e15c1c2fce07e735 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:14:17 +0100 Subject: [PATCH 04/27] feat(m365): add Microsoft 365 provider (#5902) Co-authored-by: Daniel Barranquero Co-authored-by: HugoPBrito Co-authored-by: MrCloudSec --- prowler/__main__.py | 37 +- prowler/compliance/microsoft365/__init__.py | 0 .../microsoft365/cis_4.0_microsoft365.json | 134 +++ prowler/config/config.py | 1 + .../config/microsoft365_mutelist_example.yaml | 44 + prowler/lib/check/models.py | 23 + prowler/lib/cli/parser.py | 5 +- .../compliance/cis/cis_microsoft365.py | 99 ++ prowler/lib/outputs/compliance/cis/models.py | 31 + prowler/lib/outputs/finding.py | 14 + prowler/lib/outputs/html/html.py | 46 + prowler/lib/outputs/outputs.py | 2 + prowler/lib/outputs/summary_table.py | 3 + prowler/providers/common/provider.py | 11 + prowler/providers/microsoft365/__init__.py | 0 .../microsoft365/exceptions/exceptions.py | 279 ++++++ .../providers/microsoft365/lib/__init__.py | 0 .../microsoft365/lib/arguments/__init__.py | 0 .../microsoft365/lib/arguments/arguments.py | 48 + .../microsoft365/lib/mutelist/__init__.py | 0 .../microsoft365/lib/mutelist/mutelist.py | 17 + .../microsoft365/lib/regions/__init__.py | 0 .../lib/regions/microsoft365_regions.py | 27 + .../microsoft365/lib/service/__init__.py | 0 .../microsoft365/lib/service/service.py | 14 + .../microsoft365/microsoft365_provider.py | 925 ++++++++++++++++++ prowler/providers/microsoft365/models.py | 44 + .../services/admincenter/__init__.py | 0 .../admincenter/admincenter_client.py | 6 + .../__init__.py | 0 ...groups_not_public_visibility.metadata.json | 30 + ...dmincenter_groups_not_public_visibility.py | 25 + .../admincenter/admincenter_service.py | 151 +++ .../__init__.py | 0 ...ns_reduced_license_footprint.metadata.json | 30 + ..._users_admins_reduced_license_footprint.py | 35 + .../__init__.py | 0 ...n_two_and_four_global_admins.metadata.json | 30 + ...sers_between_two_and_four_global_admins.py | 39 + tests/lib/cli/parser_test.py | 6 +- tests/providers/azure/azure_provider_test.py | 2 +- .../fixtures/microsoft365_mutelist.yaml | 16 + .../mutelist/microsoft365_mutelist_test.py | 103 ++ .../lib/regions/microsoft365_regions_test.py | 38 + .../microsoft365/microsoft365_fixtures.py | 40 + .../microsoft365_provider_test.py | 314 ++++++ ...enter_groups_not_public_visibility_test.py | 97 ++ .../admincenter/admincenter_service_test.py | 84 ++ ...s_admins_reduced_license_footprint_test.py | 159 +++ ...between_two_and_four_global_admins_test.py | 172 ++++ 50 files changed, 3173 insertions(+), 8 deletions(-) create mode 100644 prowler/compliance/microsoft365/__init__.py create mode 100644 prowler/compliance/microsoft365/cis_4.0_microsoft365.json create mode 100644 prowler/config/microsoft365_mutelist_example.yaml create mode 100644 prowler/lib/outputs/compliance/cis/cis_microsoft365.py create mode 100644 prowler/providers/microsoft365/__init__.py create mode 100644 prowler/providers/microsoft365/exceptions/exceptions.py create mode 100644 prowler/providers/microsoft365/lib/__init__.py create mode 100644 prowler/providers/microsoft365/lib/arguments/__init__.py create mode 100644 prowler/providers/microsoft365/lib/arguments/arguments.py create mode 100644 prowler/providers/microsoft365/lib/mutelist/__init__.py create mode 100644 prowler/providers/microsoft365/lib/mutelist/mutelist.py create mode 100644 prowler/providers/microsoft365/lib/regions/__init__.py create mode 100644 prowler/providers/microsoft365/lib/regions/microsoft365_regions.py create mode 100644 prowler/providers/microsoft365/lib/service/__init__.py create mode 100644 prowler/providers/microsoft365/lib/service/service.py create mode 100644 prowler/providers/microsoft365/microsoft365_provider.py create mode 100644 prowler/providers/microsoft365/models.py create mode 100644 prowler/providers/microsoft365/services/admincenter/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_client.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_service.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py create mode 100644 tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml create mode 100644 tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py create mode 100644 tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py create mode 100644 tests/providers/microsoft365/microsoft365_fixtures.py create mode 100644 tests/providers/microsoft365/microsoft365_provider_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_service_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py diff --git a/prowler/__main__.py b/prowler/__main__.py index d3ba23db6be..d1aee094eb5 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -51,6 +51,7 @@ from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS +from prowler.lib.outputs.compliance.cis.cis_microsoft365 import Microsoft365CIS from prowler.lib.outputs.compliance.compliance import display_compliance_table from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS @@ -78,6 +79,7 @@ from prowler.providers.common.quick_inventory import run_provider_quick_inventory from prowler.providers.gcp.models import GCPOutputOptions from prowler.providers.kubernetes.models import KubernetesOutputOptions +from prowler.providers.microsoft365.models import Microsoft365OutputOptions def prowler(): @@ -259,6 +261,10 @@ def prowler(): output_options = KubernetesOutputOptions( args, bulk_checks_metadata, global_provider.identity ) + elif provider == "microsoft365": + output_options = Microsoft365OutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) # Run the quick inventory for the provider if available if hasattr(args, "quick_inventory") and args.quick_inventory: @@ -307,7 +313,6 @@ def prowler(): if "SLACK_API_TOKEN" in environ and ( "SLACK_CHANNEL_NAME" in environ or "SLACK_CHANNEL_ID" in environ ): - token = environ["SLACK_API_TOKEN"] channel = ( environ["SLACK_CHANNEL_NAME"] @@ -629,6 +634,36 @@ def prowler(): generated_outputs["compliance"].append(generic_compliance) generic_compliance.batch_write_data_to_file() + elif provider == "microsoft365": + for compliance_name in input_compliance_frameworks: + if compliance_name.startswith("cis_"): + # Generate CIS Finding Object + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + cis = Microsoft365CIS( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + create_file_descriptor=True, + file_path=filename, + ) + generated_outputs["compliance"].append(cis) + cis.batch_write_data_to_file() + else: + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + generic_compliance = GenericCompliance( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + create_file_descriptor=True, + file_path=filename, + ) + generated_outputs["compliance"].append(generic_compliance) + generic_compliance.batch_write_data_to_file() + # AWS Security Hub Integration if provider == "aws": # Send output to S3 if needed (-B / -D) for all the output formats diff --git a/prowler/compliance/microsoft365/__init__.py b/prowler/compliance/microsoft365/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/compliance/microsoft365/cis_4.0_microsoft365.json b/prowler/compliance/microsoft365/cis_4.0_microsoft365.json new file mode 100644 index 00000000000..ca26b4a467a --- /dev/null +++ b/prowler/compliance/microsoft365/cis_4.0_microsoft365.json @@ -0,0 +1,134 @@ +{ + "Framework": "CIS", + "Version": "4.0", + "Provider": "Microsoft365", + "Description": "The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for establishing a secure configuration posture for Microsoft 365 Cloud offerings running on any OS.", + "Requirements": [ + { + "Id": "1.1.1", + "Description": "Ensure Administrative accounts are cloud-only", + "Checks": [], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Administrative accounts are special privileged accounts that could have varying levels of access to data, users, and settings. Regular user accounts should never be utilized for administrative tasks and care should be taken, in the case of a hybrid environment, to keep Administrative accounts separated from on-prem accounts. Administrative accounts should not have applications assigned so that they have no access to potentially vulnerable services (e.g., email, Teams, SharePoint, etc.) and only access to perform tasks as needed for administrative purposes. Ensure administrative accounts are not On-premises sync enabled.", + "RationaleStatement": "In a hybrid environment, having separate accounts will help ensure that in the event of a breach in the cloud, that the breach does not affect the on-prem environment and vice versa.", + "ImpactStatement": "Administrative users will have to switch accounts and utilize login/logout functionality when performing administrative tasks, as well as not benefiting from SSO. This will require a migration process from the 'daily driver' account to a dedicated admin account. When migrating permissions to the admin account, both M365 and Azure RBAC roles should be migrated as well. Once the new admin accounts are created, both of these permission sets should be moved from the daily driver account to the new admin account. Failure to migrate Azure RBAC roles can cause an admin to not be able to see their subscriptions/resources while using their admin accounts.", + "RemediationProcedure": "Review all administrative accounts and ensure they are configured as cloud-only. Remove any on-premises synchronization for these accounts. Assign necessary roles and permissions exclusively to the dedicated cloud administrative accounts.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the list of administrative accounts. Verify that none of them are on-premises sync enabled.", + "AdditionalInformation": "This recommendation is particularly relevant for hybrid environments and is aimed at enhancing the security of administrative accounts by isolating them from on-prem infrastructure.", + "DefaultValue": "By default, administrative accounts may be either cloud-only or hybrid. This setting needs to be verified and adjusted according to the recommendation.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.1" + } + ] + }, + { + "Id": "1.1.2", + "Description": "Ensure two emergency access accounts have been defined", + "Checks": [], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Emergency access or 'break glass' accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. These emergencies could include technical failures of a cellular provider or Microsoft-related service such as MFA, or the last remaining Global Administrator account becoming inaccessible. Ensure two Emergency Access accounts have been defined.", + "RationaleStatement": "In various situations, an organization may require the use of a break glass account to gain emergency access. Losing access to administrative functions could result in a significant loss of support capability, reduced visibility into the security posture, and potential financial losses.", + "ImpactStatement": "Improper implementation of emergency access accounts could weaken the organization's security posture. To mitigate this, at least one account should be excluded from all conditional access rules, and strong authentication mechanisms (e.g., long, high-entropy passwords or FIDO2 security keys) must be used to secure the accounts.", + "RemediationProcedure": "Create two emergency access accounts and configure them according to Microsoft's recommendations. Ensure that these accounts are not assigned to specific users and are excluded from all conditional access rules. Secure the accounts with strong passwords or passwordless authentication methods, such as FIDO2 security keys. Regularly review and test these accounts to confirm their functionality.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and verify the existence of at least two emergency access accounts. Check their configurations to ensure they comply with Microsoft's recommendations, including exclusion from conditional access rules and proper security settings.", + "AdditionalInformation": "Emergency access accounts are critical for maintaining administrative control during unexpected events. Regular audits and strict access controls are recommended to prevent misuse.", + "DefaultValue": "By default, emergency access accounts are not configured. Organizations must create and secure these accounts proactively.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.2; Microsoft documentation on emergency access accounts." + } + ] + }, + { + "Id": "1.1.3", + "Description": "Ensure that between two and four global admins are designated", + "Checks": [ + "admincenter_users_between_two_and_four_global_admins" + ], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "More than one global administrator should be designated so a single admin can be monitored and to provide redundancy should a single admin leave an organization. Additionally, there should be no more than four global admins set for any tenant. Ideally, global administrators will have no licenses assigned to them.", + "RationaleStatement": "If there is only one global tenant administrator, he or she can perform malicious activity without the possibility of being discovered by another admin. If there are numerous global tenant administrators, the more likely it is that one of their accounts will be successfully breached by an external attacker.", + "ImpactStatement": "If there is only one global administrator in a tenant, an additional global administrator will need to be identified and configured. If there are more than four global administrators, a review of role requirements for current global administrators will be required to identify which of the users require global administrator access.", + "RemediationProcedure": "Review the list of global administrators in the tenant and ensure there are at least two but no more than four accounts assigned this role. Remove excess global administrator accounts and create additional ones if necessary. Avoid assigning licenses to these accounts.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the list of global administrators. Verify that there are at least two but no more than four global administrators configured.", + "AdditionalInformation": "Global administrators play a critical role in tenant management. Ensuring a proper number of global administrators improves redundancy and security.", + "DefaultValue": "By default, there may be a single global administrator configured for the tenant. Organizations need to manually adjust the count as per best practices.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.3" + } + ] + }, + { + "Id": "1.1.4", + "Description": "Ensure administrative accounts use licenses with a reduced application footprint", + "Checks": [ + "admincenter_users_admins_reduced_license_footprint" + ], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Administrative accounts are special privileged accounts with varying levels of access to data, users, and settings. It is recommended that privileged accounts either not be licensed or use Microsoft Entra ID P1 or Microsoft Entra ID P2 licenses to minimize application exposure.", + "RationaleStatement": "Ensuring administrative accounts do not use licenses with applications assigned to them reduces the attack surface of high-privileged identities. This minimizes the likelihood of these accounts being targeted by social engineering attacks or exposed to malicious content via licensed applications. Administrative activities should be restricted to dedicated accounts without access to collaborative tools like mailboxes.", + "ImpactStatement": "Administrative users will need to switch accounts to perform privileged actions, requiring login/logout functionality and potentially losing the convenience of SSO. Alerts sent to Global Administrators or TenantAdmins by default might not be received if these accounts lack application-based licenses. Proper alert routing must be configured to avoid missed notifications.", + "RemediationProcedure": "Review the licenses assigned to administrative accounts. Remove licenses granting access to collaborative applications and assign Microsoft Entra ID P1 or P2 licenses if participation in Microsoft 365 security services is required. Configure alerts to be sent to valid email addresses for monitoring.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the licenses assigned to administrative accounts. Confirm that administrative accounts either have no licenses or are limited to Microsoft Entra ID P1 or P2 licenses without collaborative applications enabled.", + "AdditionalInformation": "Reducing the application footprint of administrative accounts improves security by minimizing potential attack vectors. Special care should be taken to configure alert routing properly to ensure critical notifications are not missed.", + "DefaultValue": "By default, administrative accounts may have licenses assigned based on organizational setup. Manual review and adjustment are necessary to comply with this recommendation.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.4; Microsoft documentation on Entra ID licenses and privileged account security." + } + ] + }, + { + "Id": "1.2.1", + "Description": "Ensure that only organizationally managed/approved public groups exist", + "Checks": [ + "admincenter_groups_not_public_visibility" + ], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft 365 Groups enable shared resource access across Microsoft 365 services. The default privacy setting for groups is 'Public,' which allows users within the organization to access the group's resources. Ensure that only organizationally managed and approved public groups exist to prevent unauthorized access to sensitive information.", + "RationaleStatement": "Public groups can be accessed by any user within the organization via several methods, such as self-adding through the Azure portal, sending an access request, or directly using the SharePoint URL. Without control over group privacy, sensitive organizational data might be exposed to unintended users.", + "ImpactStatement": "Implementing this recommendation may result in an increased volume of access requests for group owners, particularly for groups previously intended to be public.", + "RemediationProcedure": "Audit all Microsoft 365 public groups and ensure they are organizationally managed and approved. Convert unnecessary public groups to private groups and enforce strict policies for creating and approving public groups. Group owners should be instructed to monitor and review access requests.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the list of public groups. Verify that all public groups have been approved and are necessary for organizational purposes.", + "AdditionalInformation": "Public groups expose data to all users within the organization, increasing the risk of accidental or unauthorized access. Periodic reviews of group privacy settings are recommended.", + "DefaultValue": "By default, groups created in Microsoft 365 are set to 'Public' privacy.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.2.1; Microsoft documentation on managing group privacy." + } + ] + }, + { + "Id": "1.2.2", + "Description": "Ensure sign-in to shared mailboxes is blocked", + "Checks": [], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Manuel", + "Description": "Shared mailboxes are used when multiple people need access to the same mailbox for functions such as support or reception. These mailboxes are created with a corresponding user account that includes a system-generated password. To enhance security, sign-in should be blocked for these shared mailbox accounts, ensuring access is granted only through delegation.", + "RationaleStatement": "Blocking sign-in for shared mailbox accounts prevents unauthorized access or direct sign-in, ensuring that all interactions with the shared mailbox are through authorized delegation. This reduces the risk of attackers exploiting shared mailboxes for malicious purposes such as sending emails with spoofed identities.", + "ImpactStatement": "Blocking sign-in to shared mailboxes requires users to access these mailboxes only through delegation. Administrators will need to monitor and ensure proper access permissions are assigned.", + "RemediationProcedure": "Log in to the Microsoft 365 Admin Center and locate the shared mailboxes. For each shared mailbox, verify that sign-in is blocked by reviewing the associated user account settings. If sign-in is not blocked, adjust the account settings to enforce this configuration.", + "AuditProcedure": "Review all shared mailboxes in the Microsoft 365 Admin Center. Confirm that the user accounts associated with these mailboxes have sign-in blocked.", + "AdditionalInformation": "Shared mailboxes are often a target for exploitation due to their broad access and functional role. Blocking sign-in significantly reduces the attack surface.", + "DefaultValue": "By default, shared mailboxes may have sign-in enabled unless explicitly configured otherwise.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.2.2; Microsoft documentation on managing shared mailboxes." + } + ] + } + ] +} diff --git a/prowler/config/config.py b/prowler/config/config.py index 2da5571af2d..ee3f3802547 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -28,6 +28,7 @@ class Provider(str, Enum): GCP = "gcp" AZURE = "azure" KUBERNETES = "kubernetes" + MICROSOFT365 = "microsoft365" # Compliance diff --git a/prowler/config/microsoft365_mutelist_example.yaml b/prowler/config/microsoft365_mutelist_example.yaml new file mode 100644 index 00000000000..c33b442c04f --- /dev/null +++ b/prowler/config/microsoft365_mutelist_example.yaml @@ -0,0 +1,44 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Account == Microsoft365 Tenant and Region == Microsoft365 Location +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTELIST EXAMPLE ########################### +Mutelist: + Accounts: + "*": + Checks: + "admincenter_groups_not_public_visibility": + Regions: + - "westeurope" + Resources: + - "sqlserver1" # Will ignore sqlserver1 in check sqlserver_tde_encryption_enabled located in westeurope + Description: "Findings related with the check sqlserver_tde_encryption_enabled is muted for westeurope region and sqlserver1 resource" + "defender_*": + Regions: + - "*" + Resources: + - "*" # Will ignore every Defender check in every location + "*": + Regions: + - "*" + Resources: + - "test" + Tags: + - "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and + - "project=test|project=stage" # either of ('project=test' OR project=stage) in Azure subscription 1 and every location + + "*": + Checks: + "admincenter_*": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Accounts: + - "Tenant1" + Regions: + - "eastus" + - "eastus2" # Will ignore every resource in admincenter checks except the ones in Tenant1 located in eastus or eastus2 diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 68512778ff5..2aca0e83c18 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -535,6 +535,29 @@ def __init__(self, metadata: Dict, resource: Any) -> None: self.namespace = "cluster-wide" +@dataclass +class Check_Report_Microsoft365(Check_Report): + """Contains the Microsoft365 Check's finding information.""" + + resource_name: str + resource_id: str + location: str + + def __init__(self, metadata: Dict, resource: Any) -> None: + """Initialize the Microsoft365 Check's finding information. + + Args: + metadata: The metadata of the check. + resource: Basic information about the resource. Defaults to None. + """ + super().__init__(metadata, resource) + self.resource_name = getattr( + resource, "name", getattr(resource, "resource_name", "") + ) + self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", "")) + self.location = getattr(resource, "location", "global") + + # Testing Pending def load_check_metadata(metadata_file: str) -> CheckMetadata: """ diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index 868de4d51c0..cff89401340 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -26,7 +26,7 @@ def __init__(self): self.parser = argparse.ArgumentParser( prog="prowler", formatter_class=RawTextHelpFormatter, - usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,dashboard} ...", + usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,dashboard} ...", epilog=""" Available Cloud Providers: {aws,azure,gcp,kubernetes} @@ -34,6 +34,7 @@ def __init__(self): azure Azure Provider gcp GCP Provider kubernetes Kubernetes Provider + microsoft365 Microsoft 365 Provider Available components: dashboard Local dashboard @@ -72,7 +73,7 @@ def __init__(self): # Init Providers Arguments init_providers_parser(self) - # Dahboard Parser + # Dashboard Parser init_dashboard_parser(self) def parse(self, args=None) -> argparse.Namespace: diff --git a/prowler/lib/outputs/compliance/cis/cis_microsoft365.py b/prowler/lib/outputs/compliance/cis/cis_microsoft365.py new file mode 100644 index 00000000000..c2698295267 --- /dev/null +++ b/prowler/lib/outputs/compliance/cis/cis_microsoft365.py @@ -0,0 +1,99 @@ +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.cis.models import Microsoft365CISModel +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.finding import Finding + + +class Microsoft365CIS(ComplianceOutput): + """ + This class represents the Azure CIS compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into Azure CIS compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into Azure CIS compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = Microsoft365CISModel( + Provider=finding.provider, + Description=compliance.Description, + SubscriptionId=finding.account_uid, + Location=finding.region, + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_Profile=attribute.Profile, + Requirements_Attributes_AssessmentStatus=attribute.AssessmentStatus, + Requirements_Attributes_Description=attribute.Description, + Requirements_Attributes_RationaleStatement=attribute.RationaleStatement, + Requirements_Attributes_ImpactStatement=attribute.ImpactStatement, + Requirements_Attributes_RemediationProcedure=attribute.RemediationProcedure, + Requirements_Attributes_AuditProcedure=attribute.AuditProcedure, + Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, + Requirements_Attributes_DefaultValue=attribute.DefaultValue, + Requirements_Attributes_References=attribute.References, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = Microsoft365CISModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + SubscriptionId="", + Location="", + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_Profile=attribute.Profile, + Requirements_Attributes_AssessmentStatus=attribute.AssessmentStatus, + Requirements_Attributes_Description=attribute.Description, + Requirements_Attributes_RationaleStatement=attribute.RationaleStatement, + Requirements_Attributes_ImpactStatement=attribute.ImpactStatement, + Requirements_Attributes_RemediationProcedure=attribute.RemediationProcedure, + Requirements_Attributes_AuditProcedure=attribute.AuditProcedure, + Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, + Requirements_Attributes_DefaultValue=attribute.DefaultValue, + Requirements_Attributes_References=attribute.References, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/cis/models.py b/prowler/lib/outputs/compliance/cis/models.py index 22278856038..630cab0910b 100644 --- a/prowler/lib/outputs/compliance/cis/models.py +++ b/prowler/lib/outputs/compliance/cis/models.py @@ -66,6 +66,37 @@ class AzureCISModel(BaseModel): Muted: bool +class Microsoft365CISModel(BaseModel): + """ + Microsoft365CISModel generates a finding's output in Microsoft365 CIS Compliance format. + """ + + Provider: str + Description: str + SubscriptionId: str + Location: str + AssessmentDate: str + Requirements_Id: str + Requirements_Description: str + Requirements_Attributes_Section: str + Requirements_Attributes_Profile: str + Requirements_Attributes_AssessmentStatus: str + Requirements_Attributes_Description: str + Requirements_Attributes_RationaleStatement: str + Requirements_Attributes_ImpactStatement: str + Requirements_Attributes_RemediationProcedure: str + Requirements_Attributes_AuditProcedure: str + Requirements_Attributes_AdditionalInformation: str + Requirements_Attributes_DefaultValue: str + Requirements_Attributes_References: str + Status: str + StatusExtended: str + ResourceId: str + ResourceName: str + CheckId: str + Muted: bool + + class GCPCISModel(BaseModel): """ GCPCISModel generates a finding's output in GCP CIS Compliance format. diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 7977fe77ced..88db5bef383 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -234,6 +234,20 @@ def generate_output( ) output_data["region"] = f"namespace: {check_output.namespace}" + elif provider.type == "microsoft365": + output_data["auth_method"] = ( + f"{provider.identity.identity_type}: {provider.identity.identity_id}" + ) + output_data["account_uid"] = get_nested_attribute( + provider, "identity.tenant_id" + ) + output_data["account_name"] = get_nested_attribute( + provider, "identity.tenant_domain" + ) + output_data["resource_name"] = check_output.resource_name + output_data["resource_uid"] = check_output.resource_id + output_data["region"] = check_output.location + # check_output Unique ID # TODO: move this to a function # TODO: in Azure, GCP and K8s there are fidings without resource_name diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 5d5c4715935..f1944b6ee63 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -538,6 +538,52 @@ def get_kubernetes_assessment_summary(provider: Provider) -> str: ) return "" + @staticmethod + def get_microsoft365_assessment_summary(provider: Provider) -> str: + """ + get_microsoft365_assessment_summary gets the HTML assessment summary for the provider + + Args: + provider (Provider): the provider object + + Returns: + str: the HTML assessment summary + """ + try: + return f""" +
+
+
+ Microsoft365 Assessment Summary +
+
    +
  • + Microsoft365 Tenant Domain: {provider.identity.tenant_domain} +
  • +
+
+
+
+
+
+ Microsoft365 Credentials +
+
    +
  • + Microsoft365 Identity Type: {provider.identity.identity_type} +
  • +
  • + Microsoft365 Identity ID: {provider.identity.identity_id} +
  • +
+
+
""" + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + @staticmethod def get_assessment_summary(provider: Provider) -> str: """ diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index d600c851a66..2a61e17c364 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -13,6 +13,8 @@ def stdout_report(finding, color, verbose, status, fix): details = finding.location.lower() if finding.check_metadata.Provider == "kubernetes": details = finding.namespace.lower() + if finding.check_metadata.Provider == "microsoft365": + details = finding.location if (verbose or fix) and (not status or finding.status in status): if finding.muted: diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index a2090e53ef7..63d4480f80e 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -40,6 +40,9 @@ def display_summary_table( elif provider.type == "kubernetes": entity_type = "Context" audited_entities = provider.identity.context + elif provider.type == "microsoft365": + entity_type = "Tenant Domain" + audited_entities = provider.identity.tenant_domain # Check if there are findings and that they are not all MANUAL if findings and not all(finding.status == "MANUAL" for finding in findings): diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 4e86ad42bbf..ec9c5de0dbf 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -211,6 +211,17 @@ def init_global_provider(arguments: Namespace) -> None: mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, ) + elif "microsoft365" in provider_class_name.lower(): + provider_class( + region=arguments.region, + config_path=arguments.config_file, + mutelist_path=arguments.mutelist_file, + sp_env_auth=arguments.sp_env_auth, + az_cli_auth=arguments.az_cli_auth, + browser_auth=arguments.browser_auth, + tenant_id=arguments.tenant_id, + fixer_config=fixer_config, + ) except TypeError as error: logger.critical( diff --git a/prowler/providers/microsoft365/__init__.py b/prowler/providers/microsoft365/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py new file mode 100644 index 00000000000..b1da4f1fbc0 --- /dev/null +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -0,0 +1,279 @@ +from prowler.exceptions.exceptions import ProwlerException + + +# Exceptions codes from 5000 to 5999 are reserved for Microsoft365 exceptions +class Microsoft365BaseException(ProwlerException): + """Base class for Microsoft365 Errors.""" + + MICROSOFT365_ERROR_CODES = { + (6000, "Microsoft365EnvironmentVariableError"): { + "message": "Microsoft365 environment variable error", + "remediation": "Check the Microsoft365 environment variables and ensure they are properly set.", + }, + (6001, "Microsoft365ArgumentTypeValidationError"): { + "message": "Microsoft365 argument type validation error", + "remediation": "Check the provided argument types specific to Microsoft365 and ensure they meet the required format.", + }, + (6002, "Microsoft365SetUpRegionConfigError"): { + "message": "Microsoft365 region configuration setup error", + "remediation": "Check the Microsoft365 region configuration and ensure it is properly set up.", + }, + (6003, "Microsoft365HTTPResponseError"): { + "message": "Error in HTTP response from Microsoft365", + "remediation": "", + }, + (6004, "Microsoft365CredentialsUnavailableError"): { + "message": "Error trying to configure Microsoft365 credentials because they are unavailable", + "remediation": "Check the dictionary and ensure it is properly set up for Microsoft365 credentials. TENANT_ID, CLIENT_ID and CLIENT_SECRET are required.", + }, + (6005, "Microsoft365GetTokenIdentityError"): { + "message": "Error trying to get token from Microsoft365 Identity", + "remediation": "Check the Microsoft365 Identity and ensure it is properly set up.", + }, + (6006, "Microsoft365ClientAuthenticationError"): { + "message": "Error in client authentication", + "remediation": "Check the client authentication and ensure it is properly set up.", + }, + (6007, "Microsoft365NotValidTenantIdError"): { + "message": "The provided tenant ID is not valid", + "remediation": "Check the tenant ID and ensure it is a valid ID.", + }, + (6008, "Microsoft365NotValidClientIdError"): { + "message": "The provided client ID is not valid", + "remediation": "Check the client ID and ensure it is a valid ID.", + }, + (6009, "Microsoft365NotValidClientSecretError"): { + "message": "The provided client secret is not valid", + "remediation": "Check the client secret and ensure it is a valid secret.", + }, + (6010, "Microsoft365ConfigCredentialsError"): { + "message": "Error in configuration of Microsoft365 credentials", + "remediation": "Check the configuration of Microsoft365 credentials and ensure it is properly set up.", + }, + (6011, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { + "message": "The provided client ID and client secret do not belong to the provided tenant ID", + "remediation": "Check the client ID and client secret and ensure they belong to the provided tenant ID.", + }, + (6012, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { + "message": "The provided tenant ID and client secret do not belong to the provided client ID", + "remediation": "Check the tenant ID and client secret and ensure they belong to the provided client ID.", + }, + (6013, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { + "message": "The provided tenant ID and client ID do not belong to the provided client secret", + "remediation": "Check the tenant ID and client ID and ensure they belong to the provided client secret.", + }, + (6014, "Microsoft365InvalidProviderIdError"): { + "message": "The provided provider_id does not match with the available subscriptions", + "remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.", + }, + (6015, "Microsoft365NoAuthenticationMethodError"): { + "message": "No Microsoft365 authentication method found", + "remediation": "Check that any authentication method is properly set up for Microsoft365.", + }, + (6016, "Microsoft365SetUpSessionError"): { + "message": "Error setting up session", + "remediation": "Check the session setup and ensure it is properly set up.", + }, + (6017, "Microsoft365DefaultAzureCredentialError"): { + "message": "Error with DefaultAzureCredential", + "remediation": "Ensure DefaultAzureCredential is correctly configured.", + }, + (6018, "Microsoft365InteractiveBrowserCredentialError"): { + "message": "Error with InteractiveBrowserCredential", + "remediation": "Ensure InteractiveBrowserCredential is correctly configured.", + }, + (6019, "Microsoft365BrowserAuthNoTenantIDError"): { + "message": "Microsoft365 Tenant ID (--tenant-id) is required for browser authentication mode", + "remediation": "Check the Microsoft365 Tenant ID and ensure it is properly set up.", + }, + (6020, "Microsoft365BrowserAuthNoFlagError"): { + "message": "Microsoft365 tenant ID error: browser authentication flag (--browser-auth) not found", + "remediation": "To use browser authentication, ensure the tenant ID is properly set.", + }, + (6021, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): { + "message": "Tenant Id is required for Microsoft365 static credentials. Make sure you are using the correct credentials.", + "remediation": "Check the Microsoft365 Tenant ID and ensure it is properly set up.", + }, + } + + def __init__(self, code, file=None, original_exception=None, message=None): + provider = "Microsoft365" + error_info = self.MICROSOFT365_ERROR_CODES.get((code, self.__class__.__name__)) + if message: + error_info["message"] = message + super().__init__( + code=code, + source=provider, + file=file, + original_exception=original_exception, + error_info=error_info, + ) + + +class Microsoft365CredentialsError(Microsoft365BaseException): + """Base class for Microsoft365 credentials errors.""" + + def __init__(self, code, file=None, original_exception=None, message=None): + super().__init__(code, file, original_exception, message) + + +class Microsoft365EnvironmentVariableError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6000, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ArgumentTypeValidationError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6001, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365SetUpRegionConfigError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6002, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365HTTPResponseError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6003, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365CredentialsUnavailableError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6004, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365GetTokenIdentityError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6005, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ClientAuthenticationError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6006, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotValidTenantIdError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6007, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotValidClientIdError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6008, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotValidClientSecretError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6009, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ConfigCredentialsError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6010, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6011, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6012, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6013, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365InvalidProviderIdError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6014, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NoAuthenticationMethodError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6015, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365SetUpSessionError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6016, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365DefaultAzureCredentialError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6017, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365InteractiveBrowserCredentialError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6018, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365BrowserAuthNoTenantIDError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6019, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365BrowserAuthNoFlagError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6020, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotTenantIdButClientIdAndClienSecretError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6021, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/microsoft365/lib/__init__.py b/prowler/providers/microsoft365/lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/arguments/__init__.py b/prowler/providers/microsoft365/lib/arguments/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py new file mode 100644 index 00000000000..d3e38642e79 --- /dev/null +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -0,0 +1,48 @@ +def init_parser(self): + """Init the Microsoft365 Provider CLI parser""" + microsoft365_parser = self.subparsers.add_parser( + "microsoft365", + parents=[self.common_providers_parser], + help="Microsoft365 Provider", + ) + # Authentication Modes + microsoft365_auth_subparser = microsoft365_parser.add_argument_group( + "Authentication Modes" + ) + microsoft365_auth_modes_group = ( + microsoft365_auth_subparser.add_mutually_exclusive_group() + ) + microsoft365_auth_modes_group.add_argument( + "--az-cli-auth", + action="store_true", + help="Use Azure CLI authentication to log in against Microsoft365", + ) + microsoft365_auth_modes_group.add_argument( + "--sp-env-auth", + action="store_true", + help="Use Azure Service Principal environment variables authentication to log in against Microsoft365", + ) + microsoft365_auth_modes_group.add_argument( + "--browser-auth", + action="store_true", + help="Use Azure interactive browser authentication to log in against Microsoft365", + ) + microsoft365_parser.add_argument( + "--tenant-id", + nargs="?", + default=None, + help="Microsoft365 Tenant ID to be used with --browser-auth option", + ) + # Regions + microsoft365_regions_subparser = microsoft365_parser.add_argument_group("Regions") + microsoft365_regions_subparser.add_argument( + "--region", + nargs="?", + default="Microsoft365Global", + choices=[ + "Microsoft365Global", + "Microsoft365GlobalChina", + "Microsoft365USGovernment", + ], + help="Microsoft365 region to be used, default is Microsoft365Global", + ) diff --git a/prowler/providers/microsoft365/lib/mutelist/__init__.py b/prowler/providers/microsoft365/lib/mutelist/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/mutelist/mutelist.py b/prowler/providers/microsoft365/lib/mutelist/mutelist.py new file mode 100644 index 00000000000..83d32f4c9ac --- /dev/null +++ b/prowler/providers/microsoft365/lib/mutelist/mutelist.py @@ -0,0 +1,17 @@ +from prowler.lib.check.models import Check_Report_Microsoft365 +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.outputs.utils import unroll_dict, unroll_tags + + +class Microsoft365Mutelist(Mutelist): + def is_finding_muted( + self, + finding: Check_Report_Microsoft365, + ) -> bool: + return self.is_muted( + finding.tenant_id, + finding.check_metadata.CheckID, + finding.location, + finding.resource_name, + unroll_dict(unroll_tags(finding.resource_tags)), + ) diff --git a/prowler/providers/microsoft365/lib/regions/__init__.py b/prowler/providers/microsoft365/lib/regions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/regions/microsoft365_regions.py b/prowler/providers/microsoft365/lib/regions/microsoft365_regions.py new file mode 100644 index 00000000000..b9e6d1969aa --- /dev/null +++ b/prowler/providers/microsoft365/lib/regions/microsoft365_regions.py @@ -0,0 +1,27 @@ +from azure.identity import AzureAuthorityHosts + +MICROSOFT365_CHINA_CLOUD = "https://microsoftgraph.chinacloudapi.cn" +MICROSOFT365_US_GOV_CLOUD = "https://graph.microsoft.us" +MICROSOFT365_US_DOD_CLOUD = "https://graph.microsoftmil.us" +MICROSOFT365_GENERIC_CLOUD = "https://graph.microsoft.com" + + +def get_regions_config(region): + allowed_regions = { + "Microsoft365Global": { + "authority": None, + "base_url": MICROSOFT365_GENERIC_CLOUD, + "credential_scopes": [MICROSOFT365_GENERIC_CLOUD + "/.default"], + }, + "Microsoft365China": { + "authority": AzureAuthorityHosts.AZURE_CHINA, + "base_url": MICROSOFT365_CHINA_CLOUD, + "credential_scopes": [MICROSOFT365_CHINA_CLOUD + "/.default"], + }, + "Microsoft365USGovernment": { + "authority": AzureAuthorityHosts.AZURE_GOVERNMENT, + "base_url": MICROSOFT365_US_GOV_CLOUD, + "credential_scopes": [MICROSOFT365_US_GOV_CLOUD + "/.default"], + }, + } + return allowed_regions[region] diff --git a/prowler/providers/microsoft365/lib/service/__init__.py b/prowler/providers/microsoft365/lib/service/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/service/service.py b/prowler/providers/microsoft365/lib/service/service.py new file mode 100644 index 00000000000..85f54b260eb --- /dev/null +++ b/prowler/providers/microsoft365/lib/service/service.py @@ -0,0 +1,14 @@ +from msgraph import GraphServiceClient + +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider + + +class Microsoft365Service: + def __init__( + self, + provider: Microsoft365Provider, + ): + self.client = GraphServiceClient(credentials=provider.session) + + self.audit_config = provider.audit_config + self.fixer_config = provider.fixer_config diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py new file mode 100644 index 00000000000..dff8da4d978 --- /dev/null +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -0,0 +1,925 @@ +import asyncio +import os +import re +from argparse import ArgumentTypeError +from os import getenv +from uuid import UUID + +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError +from azure.identity import ( + ClientSecretCredential, + CredentialUnavailableError, + DefaultAzureCredential, + InteractiveBrowserCredential, +) +from colorama import Fore, Style +from msal import ConfidentialClientApplication +from msgraph import GraphServiceClient + +from prowler.config.config import ( + default_config_file_path, + get_default_mute_file_path, + load_and_validate_config_file, +) +from prowler.lib.logger import logger +from prowler.lib.utils.utils import print_boxes +from prowler.providers.common.models import Audit_Metadata, Connection +from prowler.providers.common.provider import Provider +from prowler.providers.microsoft365.exceptions.exceptions import ( + Microsoft365ArgumentTypeValidationError, + Microsoft365BrowserAuthNoFlagError, + Microsoft365BrowserAuthNoTenantIDError, + Microsoft365ClientAuthenticationError, + Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, + Microsoft365ConfigCredentialsError, + Microsoft365CredentialsUnavailableError, + Microsoft365DefaultAzureCredentialError, + Microsoft365EnvironmentVariableError, + Microsoft365GetTokenIdentityError, + Microsoft365HTTPResponseError, + Microsoft365InteractiveBrowserCredentialError, + Microsoft365InvalidProviderIdError, + Microsoft365NoAuthenticationMethodError, + Microsoft365NotTenantIdButClientIdAndClienSecretError, + Microsoft365NotValidClientIdError, + Microsoft365NotValidClientSecretError, + Microsoft365NotValidTenantIdError, + Microsoft365SetUpRegionConfigError, + Microsoft365SetUpSessionError, + Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError, + Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, +) +from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist +from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( + get_regions_config, +) +from prowler.providers.microsoft365.models import ( + Microsoft365IdentityInfo, + Microsoft365RegionConfig, +) + + +class Microsoft365Provider(Provider): + """ + Represents an Microsoft365 provider. + + This class provides functionality to interact with the Microsoft365 resources. + It handles authentication, region configuration, and provides access to various properties and methods + related to the Microsoft365 provider. + + Attributes: + _type (str): The type of the provider, which is set to "microsoft365". + _session (DefaultMicrosoft365Credential): The session object associated with the Microsoft365 provider. + _identity (Microsoft365IdentityInfo): The identity information for the Microsoft365 provider. + _audit_config (dict): The audit configuration for the Microsoft365 provider. + _region_config (Microsoft365RegionConfig): The region configuration for the Microsoft365 provider. + _mutelist (Microsoft365Mutelist): The mutelist object associated with the Microsoft365 provider. + audit_metadata (Audit_Metadata): The audit metadata for the Microsoft365 provider. + + Methods: + __init__ -> Initializes the Microsoft365 provider. + identity(self): Returns the identity of the Microsoft365 provider. + type(self): Returns the type of the Microsoft365 provider. + session(self): Returns the session object associated with the Microsoft365 provider. + region_config(self): Returns the region configuration for the Microsoft365 provider. + audit_config(self): Returns the audit configuration for the Microsoft365 provider. + fixer_config(self): Returns the fixer configuration. + output_options(self, options: tuple): Sets the output options for the Microsoft365 provider. + mutelist(self) -> Microsoft365Mutelist: Returns the mutelist object associated with the Microsoft365 provider. + setup_region_config(cls, region): Sets up the region configuration for the Microsoft365 provider. + print_credentials(self): Prints the Microsoft365 credentials information. + setup_session(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id, region_config): Set up the Microsoft365 session with the specified authentication method. + """ + + _type: str = "microsoft365" + _session: DefaultAzureCredential # Must be used besides being named for Azure + _identity: Microsoft365IdentityInfo + _audit_config: dict + _region_config: Microsoft365RegionConfig + _mutelist: Microsoft365Mutelist + # TODO: this is not optional, enforce for all providers + audit_metadata: Audit_Metadata + + def __init__( + self, + sp_env_auth: bool, + az_cli_auth: bool, + browser_auth: bool, + tenant_id: str = None, + client_id: str = None, + client_secret: str = None, + region: str = "Microsoft365Global", + config_content: dict = None, + config_path: str = None, + mutelist_path: str = None, + mutelist_content: dict = None, + fixer_config: dict = {}, + ): + """ + Initializes the Microsoft365 provider. + + Args: + tenant_id (str): The Microsoft365 Active Directory tenant ID. + region (str): The Microsoft365 region. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + config_path (str): The path to the configuration file. + config_content (dict): The configuration content. + fixer_config (dict): The fixer configuration. + mutelist_path (str): The path to the mutelist file. + mutelist_content (dict): The mutelist content. + + Returns: + None + + Raises: + Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. + Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. + Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. + Microsoft365GetTokenIdentityError: If there is an error in getting the token from the Microsoft365 identity. + Microsoft365HTTPResponseError: If there is an HTTP response error. + """ + logger.info("Setting Microsoft365 provider ...") + + logger.info("Checking if any credentials mode is set ...") + + # Validate the authentication arguments + self.validate_arguments( + az_cli_auth, + sp_env_auth, + browser_auth, + tenant_id, + client_id, + client_secret, + ) + + logger.info("Checking if region is different than default one") + self._region_config = self.setup_region_config(region) + + # Get the dict from the static credentials + microsoft365_credentials = None + if tenant_id and client_id and client_secret: + microsoft365_credentials = self.validate_static_credentials( + tenant_id=tenant_id, client_id=client_id, client_secret=client_secret + ) + + # Set up the Microsoft365 session + self._session = self.setup_session( + az_cli_auth, + sp_env_auth, + browser_auth, + tenant_id, + microsoft365_credentials, + self._region_config, + ) + + # Set up the identity + self._identity = self.setup_identity( + az_cli_auth, + sp_env_auth, + browser_auth, + client_id, + ) + + # Audit Config + if config_content: + self._audit_config = config_content + else: + if not config_path: + config_path = default_config_file_path + self._audit_config = load_and_validate_config_file(self._type, config_path) + + # Fixer Config + self._fixer_config = fixer_config + + # Mutelist + if mutelist_content: + self._mutelist = Microsoft365Mutelist( + mutelist_content=mutelist_content, + ) + else: + if not mutelist_path: + mutelist_path = get_default_mute_file_path(self.type) + self._mutelist = Microsoft365Mutelist( + mutelist_path=mutelist_path, + ) + + Provider.set_global_provider(self) + + @property + def identity(self): + """Returns the identity of the Microsoft365 provider.""" + return self._identity + + @property + def type(self): + """Returns the type of the Microsoft365 provider.""" + return self._type + + @property + def session(self): + """Returns the session object associated with the Microsoft365 provider.""" + return self._session + + @property + def region_config(self): + """Returns the region configuration for the Microsoft365 provider.""" + return self._region_config + + @property + def audit_config(self): + """Returns the audit configuration for the Microsoft365 provider.""" + return self._audit_config + + @property + def fixer_config(self): + """Returns the fixer configuration.""" + return self._fixer_config + + @property + def mutelist(self) -> Microsoft365Mutelist: + """Mutelist object associated with this Microsoft365 provider.""" + return self._mutelist + + @staticmethod + def validate_arguments( + az_cli_auth: bool, + sp_env_auth: bool, + browser_auth: bool, + tenant_id: str, + client_id: str, + client_secret: str, + ): + """ + Validates the authentication arguments for the Microsoft365 provider. + + Args: + az_cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled. + sp_env_auth (bool): Flag indicating whether application authentication with environment variables is enabled. + browser_auth (bool): Flag indicating whether browser authentication is enabled. + tenant_id (str): The Microsoft365 Tenant ID. + client_id (str): The Microsoft365 Client ID. + client_secret (str): The Microsoft365 Client Secret. + + Raises: + Microsoft365BrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found. + """ + + if not client_id and not client_secret: + if not browser_auth and tenant_id: + raise Microsoft365BrowserAuthNoFlagError( + file=os.path.basename(__file__), + message="Microsoft365 tenant ID error: browser authentication flag (--browser-auth) not found", + ) + elif not az_cli_auth and not sp_env_auth and not browser_auth: + raise Microsoft365NoAuthenticationMethodError( + file=os.path.basename(__file__), + message="Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth]", + ) + elif browser_auth and not tenant_id: + raise Microsoft365BrowserAuthNoTenantIDError( + file=os.path.basename(__file__), + message="Microsoft365 Tenant ID (--tenant-id) is required for browser authentication mode", + ) + else: + if not tenant_id: + raise Microsoft365NotTenantIdButClientIdAndClienSecretError( + file=os.path.basename(__file__), + message="Tenant Id is required for Microsoft365 static credentials. Make sure you are using the correct credentials.", + ) + + @staticmethod + def setup_region_config(region): + """ + Sets up the region configuration for the Microsoft365 provider. + + Args: + region (str): The name of the region. + + Returns: + Microsoft365RegionConfig: The region configuration object. + + """ + try: + config = get_regions_config(region) + + return Microsoft365RegionConfig( + name=region, + authority=config["authority"], + base_url=config["base_url"], + credential_scopes=config["credential_scopes"], + ) + except ArgumentTypeError as validation_error: + logger.error( + f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error}" + ) + raise Microsoft365ArgumentTypeValidationError( + file=os.path.basename(__file__), + original_exception=validation_error, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + raise Microsoft365SetUpRegionConfigError( + file=os.path.basename(__file__), + original_exception=error, + ) + + def print_credentials(self): + """Microsoft365 credentials information. + + This method prints the Microsoft365 Tenant Domain, Microsoft365 Tenant ID, Microsoft365 Region, + Microsoft365 Subscriptions, Microsoft365 Identity Type, and Microsoft365 Identity ID. + + Args: + None + + Returns: + None + """ + report_lines = [ + f"Microsoft365 Region: {Fore.YELLOW}{self.region_config.name}{Style.RESET_ALL}", + f"Microsoft365 Tenant Domain: {Fore.YELLOW}{self._identity.tenant_domain}{Style.RESET_ALL} Microsoft365 Tenant ID: {Fore.YELLOW}{self._identity.tenant_id}{Style.RESET_ALL}", + f"Microsoft365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Microsoft365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}", + ] + report_title = ( + f"{Style.BRIGHT}Using the Microsoft365 credentials below:{Style.RESET_ALL}" + ) + print_boxes(report_lines, report_title) + + # TODO: setup_session or setup_credentials? + # This should be setup_credentials, since it is setting up the credentials for the provider + @staticmethod + def setup_session( + az_cli_auth: bool, + sp_env_auth: bool, + browser_auth: bool, + tenant_id: str, + microsoft365_credentials: dict, + region_config: Microsoft365RegionConfig, + ): + """Returns the Microsoft365 credentials object. + + Set up the Microsoft365 session with the specified authentication method. + + Args: + az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. + sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables. + browser_auth (bool): Flag indicating whether to use interactive browser authentication. + tenant_id (str): The Microsoft365 Active Directory tenant ID. + microsoft365_credentials (dict): The Microsoft365 configuration object. It contains the following keys: + - tenant_id: The Microsoft365 Active Directory tenant ID. + - client_id: The Microsoft365 client ID. + - client_secret: The Microsoft365 client secret + region_config (Microsoft365RegionConfig): The region configuration object. + + Returns: + credentials: The Microsoft365 credentials object. + + Raises: + Exception: If failed to retrieve Microsoft365 credentials. + + """ + if not browser_auth: + if sp_env_auth: + try: + Microsoft365Provider.check_service_principal_creds_env_vars() + except ( + Microsoft365EnvironmentVariableError + ) as environment_credentials_error: + logger.critical( + f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" + ) + raise environment_credentials_error + try: + if microsoft365_credentials: + try: + credentials = ClientSecretCredential( + tenant_id=microsoft365_credentials["tenant_id"], + client_id=microsoft365_credentials["client_id"], + client_secret=microsoft365_credentials["client_secret"], + ) + return credentials + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ClientAuthenticationError( + file=os.path.basename(__file__), original_exception=error + ) + except CredentialUnavailableError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365CredentialsUnavailableError( + file=os.path.basename(__file__), original_exception=error + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ConfigCredentialsError( + file=os.path.basename(__file__), original_exception=error + ) + else: + # Since the authentication method to be used will come as True, we have to negate it since + # DefaultAzureCredential sets just one authentication method, excluding the others + try: + credentials = DefaultAzureCredential( + exclude_environment_credential=not sp_env_auth, + exclude_cli_credential=not az_cli_auth, + # Microsoft365 Auth using Managed Identity is not supported + exclude_managed_identity_credential=True, + # Microsoft365 Auth using Visual Studio is not supported + exclude_visual_studio_code_credential=True, + # Microsoft365 Auth using Shared Token Cache is not supported + exclude_shared_token_cache_credential=True, + # Microsoft365 Auth using PowerShell is not supported + exclude_powershell_credential=True, + # set Authority of a Microsoft Entra endpoint + authority=region_config.authority, + ) + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ClientAuthenticationError( + file=os.path.basename(__file__), original_exception=error + ) + except CredentialUnavailableError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365CredentialsUnavailableError( + file=os.path.basename(__file__), original_exception=error + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365DefaultAzureCredentialError( + file=os.path.basename(__file__), original_exception=error + ) + except Exception as error: + logger.critical("Failed to retrieve Microsoft365 credentials") + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365SetUpSessionError( + file=os.path.basename(__file__), original_exception=error + ) + else: + try: + credentials = InteractiveBrowserCredential(tenant_id=tenant_id) + except Exception as error: + logger.critical( + "Failed to retrieve Microsoft365 credentials using browser authentication" + ) + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365InteractiveBrowserCredentialError( + file=os.path.basename(__file__), original_exception=error + ) + + return credentials + + @staticmethod + def test_connection( + az_cli_auth: bool = False, + sp_env_auth: bool = False, + browser_auth: bool = False, + tenant_id: str = None, + region: str = "Microsoft365Global", + raise_on_exception=True, + client_id=None, + client_secret=None, + ) -> Connection: + """Test connection to Microsoft365 subscription. + + Test the connection to an Microsoft365 subscription using the provided credentials. + + Args: + + az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. + sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables. + browser_auth (bool): Flag indicating whether to use interactive browser authentication. + tenant_id (str): The Microsoft365 Active Directory tenant ID. + region (str): The Microsoft365 region. + raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + + Returns: + bool: True if the connection is successful, False otherwise. + + Raises: + Exception: If failed to test the connection to Microsoft365 subscription. + Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. + Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. + Microsoft365InteractiveBrowserCredentialError: If there is an error in retrieving the Microsoft365 credentials using browser authentication. + Microsoft365HTTPResponseError: If there is an HTTP response error. + Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. + + + Examples: + >>> Microsoft365Provider.test_connection(az_cli_auth=True) + True + >>> Microsoft365Provider.test_connection(sp_env_auth=False, browser_auth=True, tenant_id=None) + False, ArgumentTypeError: Microsoft365 Tenant ID is required only for browser authentication mode + >>> Microsoft365Provider.test_connection(tenant_id="XXXXXXXXXX", client_id="XXXXXXXXXX", client_secret="XXXXXXXXXX") + True + """ + try: + Microsoft365Provider.validate_arguments( + az_cli_auth, + sp_env_auth, + browser_auth, + tenant_id, + client_id, + client_secret, + ) + region_config = Microsoft365Provider.setup_region_config(region) + + # Get the dict from the static credentials + microsoft365_credentials = None + if tenant_id and client_id and client_secret: + microsoft365_credentials = ( + Microsoft365Provider.validate_static_credentials( + tenant_id=tenant_id, + client_id=client_id, + client_secret=client_secret, + ) + ) + + # Set up the Microsoft365 session + credentials = Microsoft365Provider.setup_session( + az_cli_auth, + sp_env_auth, + browser_auth, + tenant_id, + microsoft365_credentials, + region_config, + ) + + GraphServiceClient(credentials=credentials) + + logger.info("Microsoft365 provider: Connection to Microsoft365 successful") + + return Connection(is_connected=True) + + # Exceptions from setup_region_config + except Microsoft365ArgumentTypeValidationError as type_validation_error: + logger.error( + f"{type_validation_error.__class__.__name__}[{type_validation_error.__traceback__.tb_lineno}]: {type_validation_error}" + ) + if raise_on_exception: + raise type_validation_error + return Connection(error=type_validation_error) + except Microsoft365SetUpRegionConfigError as region_config_error: + logger.error( + f"{region_config_error.__class__.__name__}[{region_config_error.__traceback__.tb_lineno}]: {region_config_error}" + ) + if raise_on_exception: + raise region_config_error + return Connection(error=region_config_error) + # Exceptions from setup_session + except Microsoft365EnvironmentVariableError as environment_credentials_error: + logger.error( + f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}]: {environment_credentials_error}" + ) + if raise_on_exception: + raise environment_credentials_error + return Connection(error=environment_credentials_error) + except Microsoft365ConfigCredentialsError as config_credentials_error: + logger.error( + f"{config_credentials_error.__class__.__name__}[{config_credentials_error.__traceback__.tb_lineno}]: {config_credentials_error}" + ) + if raise_on_exception: + raise config_credentials_error + return Connection(error=config_credentials_error) + except Microsoft365ClientAuthenticationError as client_auth_error: + logger.error( + f"{client_auth_error.__class__.__name__}[{client_auth_error.__traceback__.tb_lineno}]: {client_auth_error}" + ) + if raise_on_exception: + raise client_auth_error + return Connection(error=client_auth_error) + except Microsoft365CredentialsUnavailableError as credential_unavailable_error: + logger.error( + f"{credential_unavailable_error.__class__.__name__}[{credential_unavailable_error.__traceback__.tb_lineno}]: {credential_unavailable_error}" + ) + if raise_on_exception: + raise credential_unavailable_error + return Connection(error=credential_unavailable_error) + except ( + Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError + ) as tenant_id_error: + logger.error( + f"{tenant_id_error.__class__.__name__}[{tenant_id_error.__traceback__.tb_lineno}]: {tenant_id_error}" + ) + if raise_on_exception: + raise tenant_id_error + return Connection(error=tenant_id_error) + except ( + Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError + ) as client_id_error: + logger.error( + f"{client_id_error.__class__.__name__}[{client_id_error.__traceback__.tb_lineno}]: {client_id_error}" + ) + if raise_on_exception: + raise client_id_error + return Connection(error=client_id_error) + except ( + Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError + ) as client_secret_error: + logger.error( + f"{client_secret_error.__class__.__name__}[{client_secret_error.__traceback__.tb_lineno}]: {client_secret_error}" + ) + if raise_on_exception: + raise client_secret_error + return Connection(error=client_secret_error) + # Exceptions from provider_id validation + except Microsoft365InvalidProviderIdError as invalid_credentials_error: + logger.error( + f"{invalid_credentials_error.__class__.__name__}[{invalid_credentials_error.__traceback__.tb_lineno}]: {invalid_credentials_error}" + ) + if raise_on_exception: + raise invalid_credentials_error + return Connection(error=invalid_credentials_error) + # Exceptions from SubscriptionClient + except HttpResponseError as http_response_error: + logger.error( + f"{http_response_error.__class__.__name__}[{http_response_error.__traceback__.tb_lineno}]: {http_response_error}" + ) + if raise_on_exception: + raise Microsoft365HTTPResponseError( + file=os.path.basename(__file__), + original_exception=http_response_error, + ) + return Connection(error=http_response_error) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + if raise_on_exception: + # Raise directly the exception + raise error + return Connection(error=error) + + @staticmethod + def check_service_principal_creds_env_vars(): + """ + Checks the presence of required environment variables for service principal authentication against Azure. + + This method checks for the presence of the following environment variables: + - AZURE_CLIENT_ID: Azure client ID + - AZURE_TENANT_ID: Azure tenant ID + - AZURE_CLIENT_SECRET: Azure client secret + + If any of the environment variables is missing, it logs a critical error and exits the program. + """ + logger.info( + "Microsoft365 provider: checking service principal environment variables ..." + ) + for env_var in ["AZURE_CLIENT_ID", "AZURE_TENANT_ID", "AZURE_CLIENT_SECRET"]: + if not getenv(env_var): + logger.critical( + f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365" + ) + raise Microsoft365EnvironmentVariableError( + file=os.path.basename(__file__), + message=f"Missing environment variable {env_var} required to authenticate.", + ) + + def setup_identity( + self, + az_cli_auth, + sp_env_auth, + browser_auth, + client_id, + ): + """ + Sets up the identity for the Microsoft365 provider. + + Args: + az_cli_auth (bool): Flag indicating if Azure CLI authentication is used. + sp_env_auth (bool): Flag indicating if application authentication with environment variables is used. + browser_auth (bool): Flag indicating if interactive browser authentication is used. + client_id (str): The Microsoft365 client ID. + + Returns: + Microsoft365IdentityInfo: An instance of Microsoft365IdentityInfo containing the identity information. + """ + credentials = self.session + # TODO: fill this object with real values not default and set to none + identity = Microsoft365IdentityInfo() + + # If credentials comes from service principal or browser, if the required permissions are assigned + # the identity can access AAD and retrieve the tenant domain name. + # With cli also should be possible but right now it does not work, microsoft365 python package issue is coming + # At the time of writting this with az cli creds is not working, despite that is included + if az_cli_auth or sp_env_auth or browser_auth or client_id: + + async def get_microsoft365_identity(): + # Trying to recover tenant domain info + try: + logger.info( + "Trying to retrieve tenant domain from AAD to populate identity structure ..." + ) + client = GraphServiceClient(credentials=credentials) + + domain_result = await client.domains.get() + if getattr(domain_result, "value"): + if getattr(domain_result.value[0], "id"): + identity.tenant_domain = domain_result.value[0].id + + except HttpResponseError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365HTTPResponseError( + file=os.path.basename(__file__), + original_exception=error, + ) + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365GetTokenIdentityError( + file=os.path.basename(__file__), + original_exception=error, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + # since that exception is not considered as critical, we keep filling another identity fields + if sp_env_auth or client_id: + # The id of the sp can be retrieved from environment variables + identity.identity_id = getenv("AZURE_CLIENT_ID") + identity.identity_type = "Service Principal" + # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli + # should work but it doesn't, pending issue + else: + identity.identity_id = "Unknown user id (Missing AAD permissions)" + identity.identity_type = "User" + try: + logger.info( + "Trying to retrieve user information from AAD to populate identity structure ..." + ) + client = GraphServiceClient(credentials=credentials) + + me = await client.me.get() + if me: + if getattr(me, "user_principal_name"): + identity.identity_id = me.user_principal_name + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + + # Retrieve tenant id from the client + client = GraphServiceClient(credentials=credentials) + organization_info = await client.organization.get() + identity.tenant_id = organization_info.value[0].id + + asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) + return identity + + @staticmethod + def validate_static_credentials( + tenant_id: str = None, client_id: str = None, client_secret: str = None + ) -> dict: + """ + Validates the static credentials for the Microsoft365 provider. + + Args: + tenant_id (str): The Microsoft365 Active Directory tenant ID. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + + Raises: + Microsoft365NotValidTenantIdError: If the provided Microsoft365 Tenant ID is not valid. + Microsoft365NotValidClientIdError: If the provided Microsoft365 Client ID is not valid. + Microsoft365NotValidClientSecretError: If the provided Microsoft365 Client Secret is not valid. + Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError: If the provided Microsoft365 Client ID and Client Secret do not belong to the specified Tenant ID. + Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError: If the provided Microsoft365 Tenant ID and Client Secret do not belong to the specified Client ID. + Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError: If the provided Microsoft365 Tenant ID and Client ID do not belong to the specified Client Secret. + + Returns: + dict: A dictionary containing the validated static credentials. + """ + # Validate the Tenant ID + try: + UUID(tenant_id) + except ValueError: + raise Microsoft365NotValidTenantIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Tenant ID is not valid.", + ) + + # Validate the Client ID + try: + UUID(client_id) + except ValueError: + raise Microsoft365NotValidClientIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Client ID is not valid.", + ) + # Validate the Client Secret + if not re.match("^[a-zA-Z0-9._~-]+$", client_secret): + raise Microsoft365NotValidClientSecretError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Client Secret is not valid.", + ) + + try: + Microsoft365Provider.verify_client(tenant_id, client_id, client_secret) + return { + "tenant_id": tenant_id, + "client_id": client_id, + "client_secret": client_secret, + } + except Microsoft365NotValidTenantIdError as tenant_id_error: + logger.error( + f"{tenant_id_error.__class__.__name__}[{tenant_id_error.__traceback__.tb_lineno}]: {tenant_id_error}" + ) + raise Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Client ID and Client Secret do not belong to the specified Tenant ID.", + ) + except Microsoft365NotValidClientIdError as client_id_error: + logger.error( + f"{client_id_error.__class__.__name__}[{client_id_error.__traceback__.tb_lineno}]: {client_id_error}" + ) + raise Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Tenant ID and Client Secret do not belong to the specified Client ID.", + ) + except Microsoft365NotValidClientSecretError as client_secret_error: + logger.error( + f"{client_secret_error.__class__.__name__}[{client_secret_error.__traceback__.tb_lineno}]: {client_secret_error}" + ) + raise Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Tenant ID and Client ID do not belong to the specified Client Secret.", + ) + + @staticmethod + def verify_client(tenant_id, client_id, client_secret) -> None: + """ + Verifies the Microsoft365 client credentials using the specified tenant ID, client ID, and client secret. + + Args: + tenant_id (str): The Microsoft365 Active Directory tenant ID. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + + Raises: + Microsoft365NotValidTenantIdError: If the provided Microsoft365 Tenant ID is not valid. + Microsoft365NotValidClientIdError: If the provided Microsoft365 Client ID is not valid. + Microsoft365NotValidClientSecretError: If the provided Microsoft365 Client Secret is not valid. + + Returns: + None + """ + authority = f"https://login.microsoftonline.com/{tenant_id}" + try: + # Create a ConfidentialClientApplication instance + app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=authority, + ) + + # Attempt to acquire a token + result = app.acquire_token_for_client( + scopes=["https://graph.microsoft.com/.default"] + ) + + # Check if token acquisition was successful + if "access_token" not in result: + # Handle specific errors based on the MSAL response + error_description = result.get("error_description", "") + if f"Tenant '{tenant_id}'" in error_description: + raise Microsoft365NotValidTenantIdError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Tenant ID is not valid for the specified Client ID and Client Secret.", + ) + if f"Application with identifier '{client_id}'" in error_description: + raise Microsoft365NotValidClientIdError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Client ID is not valid for the specified Tenant ID and Client Secret.", + ) + if "Invalid client secret provided" in error_description: + raise Microsoft365NotValidClientSecretError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Client Secret is not valid for the specified Tenant ID and Client ID.", + ) + + except Exception as e: + # Generic exception handling (if needed) + raise RuntimeError(f"An unexpected error occurred: {str(e)}") diff --git a/prowler/providers/microsoft365/models.py b/prowler/providers/microsoft365/models.py new file mode 100644 index 00000000000..c1e028e8d6c --- /dev/null +++ b/prowler/providers/microsoft365/models.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel + +from prowler.config.config import output_file_timestamp +from prowler.providers.common.models import ProviderOutputOptions + + +class Microsoft365IdentityInfo(BaseModel): + identity_id: str = "" + identity_type: str = "" + tenant_id: str = "" + tenant_domain: str = "Unknown tenant domain (missing AAD permissions)" + location: str = "" + + +class Microsoft365RegionConfig(BaseModel): + name: str = "" + authority: str = None + base_url: str = "" + credential_scopes: list = [] + + +class Microsoft365OutputOptions(ProviderOutputOptions): + def __init__(self, arguments, bulk_checks_metadata, identity): + # First call Provider_Output_Options init + super().__init__(arguments, bulk_checks_metadata) + + # Check if custom output filename was input, if not, set the default + if ( + not hasattr(arguments, "output_filename") + or arguments.output_filename is None + ): + if ( + identity.tenant_domain + != "Unknown tenant domain (missing AAD permissions)" + ): + self.output_filename = ( + f"prowler-output-{identity.tenant_domain}-{output_file_timestamp}" + ) + else: + self.output_filename = ( + f"prowler-output-{identity.tenant_id}-{output_file_timestamp}" + ) + else: + self.output_filename = arguments.output_filename diff --git a/prowler/providers/microsoft365/services/admincenter/__init__.py b/prowler/providers/microsoft365/services/admincenter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_client.py b/prowler/providers/microsoft365/services/admincenter/admincenter_client.py new file mode 100644 index 00000000000..72facdeefd0 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + AdminCenter, +) + +admincenter_client = AdminCenter(Provider.get_global_provider()) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json new file mode 100644 index 00000000000..1bfc5cf677a --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "admincenter_groups_not_public_visibility", + "CheckTitle": "Ensure that only organizationally managed/approved public groups exist", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Microsoft365Group", + "Description": "Ensure that only organizationally managed and approved public groups exist to prevent unauthorized access to sensitive group resources like SharePoint, Teams, or other shared assets.", + "Risk": "Unmanaged public groups can allow unauthorized access to organizational resources, posing a risk of data leakage or misuse through easily guessable SharePoint URLs or self-adding to groups via the Azure portal.", + "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/admin/create-groups/manage-groups?view=o365-worldwide", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Review and adjust the privacy settings of Microsoft 365 Groups to ensure only organizationally managed and approved public groups exist.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/microsoft-365-groups-governance" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py new file mode 100644 index 00000000000..aea037c8a1d --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_groups_not_public_visibility(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + for group in admincenter_client.groups.values(): + report = Check_Report_Microsoft365(metadata=self.metadata(), resource=group) + report.resource_id = group.id + report.resource_name = group.name + report.status = "FAIL" + report.status_extended = f"Group {group.name} has {group.visibility} visibility and should be Private." + + if group.visibility != "Public": + report.status = "PASS" + report.status_extended = ( + f"Group {group.name} has {group.visibility} visibility." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py new file mode 100644 index 00000000000..2fc14d1ae6e --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -0,0 +1,151 @@ +from asyncio import gather, get_event_loop +from typing import List, Optional + +from msgraph.generated.models.o_data_errors.o_data_error import ODataError +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.microsoft365.lib.service.service import Microsoft365Service +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider + + +class AdminCenter(Microsoft365Service): + def __init__(self, provider: Microsoft365Provider): + super().__init__(provider) + + loop = get_event_loop() + + # Get users first alone because it is a dependency for other attributes + self.users = loop.run_until_complete(self._get_users()) + + attributes = loop.run_until_complete( + gather( + self._get_directory_roles(), + self._get_groups(), + ) + ) + + self.directory_roles = attributes[0] + self.groups = attributes[1] + + async def _get_users(self): + logger.info("Microsoft365 - Getting users...") + users = {} + try: + users_list = await self.client.users.get() + users.update({}) + for user in users_list.value: + license_details = await self.client.users.by_user_id( + user.id + ).license_details.get() + try: + mailbox_settings = await self.client.users.by_user_id( + user.id + ).mailbox_settings.get() + mailbox_settings.user_purpose + except ODataError as error: + if error.error.code == "MailboxNotEnabledForRESTAPI": + logger.warning( + f"MailboxNotEnabledForRESTAPI for user {user.id}" + ) + else: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + users.update( + { + user.id: User( + id=user.id, + name=user.display_name, + license=( + license_details.value[0].sku_part_number + if license_details.value + else None + ), + ) + } + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return users + + async def _get_directory_roles(self): + logger.info("Microsoft365 - Getting directory roles...") + directory_roles_with_members = {} + try: + directory_roles_with_members.update({}) + directory_roles = await self.client.directory_roles.get() + for directory_role in directory_roles.value: + directory_role_members = ( + await self.client.directory_roles.by_directory_role_id( + directory_role.id + ).members.get() + ) + members_with_roles = [] + for member in directory_role_members.value: + user = self.users.get(member.id, None) + if user: + user.directory_roles.append(directory_role.display_name) + members_with_roles.append(user) + + directory_roles_with_members.update( + { + directory_role.display_name: DirectoryRole( + id=directory_role.id, + name=directory_role.display_name, + members=members_with_roles, + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return directory_roles_with_members + + async def _get_groups(self): + logger.info("Microsoft365 - Getting groups...") + groups = {} + try: + groups_list = await self.client.groups.get() + groups.update({}) + for group in groups_list.value: + groups.update( + { + group.id: Group( + id=group.id, + name=group.display_name, + visibility=group.visibility, + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return groups + + +class User(BaseModel): + id: str + name: str + directory_roles: List[str] = [] + license: Optional[str] = None + user_type: Optional[str] = None + + +class DirectoryRole(BaseModel): + id: str + name: str + members: List[User] + + +class Group(BaseModel): + id: str + name: str + visibility: str diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json new file mode 100644 index 00000000000..c141732dd47 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "admincenter_users_admins_reduced_license_footprint", + "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AdministrativeAccount", + "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", + "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", + "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/enterprise/protect-your-global-administrator-accounts?view=o365-worldwide", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/add-users?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py new file mode 100644 index 00000000000..0adc464c36b --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -0,0 +1,35 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_users_admins_reduced_license_footprint(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] + for user in admincenter_client.users.values(): + admin_roles = ", ".join( + [ + role + for role in user.directory_roles + if "Administrator" in role or "Globar Reader" in role + ] + ) + + if admin_roles: + report = Check_Report_Microsoft365( + metadata=self.metadata(), resource=user + ) + report.resource_id = user.id + report.resource_name = user.name + report.status = "FAIL" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and an invalid license {user.license if user.license else ''}." + + if user.license in allowed_licenses: + report.status = "PASS" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and a valid license: {user.license}." + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json new file mode 100644 index 00000000000..fafaf16cd14 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "admincenter_users_between_two_and_four_global_admins", + "CheckTitle": "Ensure that between two and four global admins are designated", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AdministrativeRole", + "Description": "Ensure that there are between two and four global administrators designated in your tenant. This ensures monitoring, redundancy, and reduces the risk associated with having too many privileged accounts.", + "Risk": "Having only one global administrator increases the risk of unmonitored actions and operational disruptions if that administrator is unavailable. Having more than four increases the likelihood of a breach through one of these highly privileged accounts.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Review the number of global administrators in your tenant. Add or remove global admins as necessary to ensure compliance with the recommended range of two to four.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py new file mode 100644 index 00000000000..aa0fc978c03 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -0,0 +1,39 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_users_between_two_and_four_global_admins(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + + directory_roles = admincenter_client.directory_roles + report = Check_Report_Microsoft365( + metadata=self.metadata(), resource=admincenter_client.directory_roles + ) + report.status = "FAIL" + report.resource_name = "Global Administrator" + + if "Global Administrator" in directory_roles: + report.resource_id = getattr( + directory_roles["Global Administrator"], + "id", + "Global Administrator", + ) + + num_global_admins = len( + getattr(directory_roles["Global Administrator"], "members", []) + ) + + if num_global_admins >= 2 and num_global_admins < 5: + report.status = "PASS" + report.status_extended = ( + f"There are {num_global_admins} global administrators." + ) + else: + report.status_extended = f"There are {num_global_admins} global administrators. It should be more than one and less than five." + + findings.append(report) + + return findings diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py index 3f92ca2d57c..16b322ac9c6 100644 --- a/tests/lib/cli/parser_test.py +++ b/tests/lib/cli/parser_test.py @@ -16,13 +16,11 @@ # capsys # https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html -prowler_default_usage_error = ( - "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,dashboard} ..." -) +prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,dashboard} ..." def mock_get_available_providers(): - return ["aws", "azure", "gcp", "kubernetes"] + return ["aws", "azure", "gcp", "kubernetes", "microsoft365"] @pytest.mark.arg_parser diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index 853903fcc2e..e754964e393 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -438,7 +438,7 @@ def test_test_connection_with_exception(self): raise_on_exception=True, ) - assert exception.type == Exception + assert exception.type is Exception assert exception.value.args[0] == "Simulated Exception" @pytest.mark.parametrize( diff --git a/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml b/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml new file mode 100644 index 00000000000..07c2b38b4ca --- /dev/null +++ b/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml @@ -0,0 +1,16 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTELIST EXAMPLE ########################### +Mutelist: + Accounts: + "subscription_1": + Checks: + "admincenter_users_between_two_and_four_global_admins": + Regions: + - "*" + Resources: + - "resource_1" + - "resource_2" diff --git a/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py new file mode 100644 index 00000000000..5bbde2e89d9 --- /dev/null +++ b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py @@ -0,0 +1,103 @@ +import yaml +from mock import MagicMock + +from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist +from tests.lib.outputs.fixtures.fixtures import generate_finding_output + +MUTELIST_FIXTURE_PATH = ( + "tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml" +) + + +class TestMicrosoft365Mutelist: + def test_get_mutelist_file_from_local_file(self): + mutelist = Microsoft365Mutelist(mutelist_path=MUTELIST_FIXTURE_PATH) + + with open(MUTELIST_FIXTURE_PATH) as f: + mutelist_fixture = yaml.safe_load(f)["Mutelist"] + + assert mutelist.mutelist == mutelist_fixture + assert mutelist.mutelist_file_path == MUTELIST_FIXTURE_PATH + + def test_get_mutelist_file_from_local_file_non_existent(self): + mutelist_path = "tests/lib/mutelist/fixtures/not_present" + mutelist = Microsoft365Mutelist(mutelist_path=mutelist_path) + + assert mutelist.mutelist == {} + assert mutelist.mutelist_file_path == mutelist_path + + def test_validate_mutelist_not_valid_key(self): + mutelist_path = MUTELIST_FIXTURE_PATH + with open(mutelist_path) as f: + mutelist_fixture = yaml.safe_load(f)["Mutelist"] + + mutelist_fixture["Accounts1"] = mutelist_fixture["Accounts"] + del mutelist_fixture["Accounts"] + + mutelist = Microsoft365Mutelist(mutelist_content=mutelist_fixture) + + assert not mutelist.validate_mutelist() + assert mutelist.mutelist == {} + assert mutelist.mutelist_file_path is None + + def test_is_finding_muted(self): + # Mutelist + mutelist_content = { + "Accounts": { + "subscription_1": { + "Checks": { + "check_test": { + "Regions": ["*"], + "Resources": ["test_resource"], + } + } + } + } + } + + mutelist = Microsoft365Mutelist(mutelist_content=mutelist_content) + + finding = MagicMock + finding.tenant_id = "subscription_1" + finding.check_metadata = MagicMock + finding.check_metadata.CheckID = "check_test" + finding.status = "FAIL" + finding.location = "global" + finding.resource_name = "test_resource" + finding.tenant_domain = "test_domain" + finding.resource_tags = [] + + assert mutelist.is_finding_muted(finding) + + def test_mute_finding(self): + # Mutelist + mutelist_content = { + "Accounts": { + "subscription_1": { + "Checks": { + "check_test": { + "Regions": ["*"], + "Resources": ["test_resource"], + } + } + } + } + } + + mutelist = Microsoft365Mutelist(mutelist_content=mutelist_content) + + finding_1 = generate_finding_output( + check_id="check_test", + status="FAIL", + account_uid="subscription_1", + region="subscription_1", + resource_uid="test_resource", + resource_tags=[], + muted=False, + ) + + muted_finding = mutelist.mute_finding(finding=finding_1) + + assert muted_finding.status == "MUTED" + assert muted_finding.muted is True + assert muted_finding.raw["status"] == "FAIL" diff --git a/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py new file mode 100644 index 00000000000..3e358511b8b --- /dev/null +++ b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py @@ -0,0 +1,38 @@ +from azure.identity import AzureAuthorityHosts + +from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( + MICROSOFT365_CHINA_CLOUD, + MICROSOFT365_GENERIC_CLOUD, + MICROSOFT365_US_GOV_CLOUD, + get_regions_config, +) + + +class Test_microsoft365_regions: + def test_get_regions_config(self): + allowed_regions = [ + "Microsoft365Global", + "Microsoft365China", + "Microsoft365USGovernment", + ] + expected_output = { + "Microsoft365Global": { + "authority": None, + "base_url": MICROSOFT365_GENERIC_CLOUD, + "credential_scopes": [MICROSOFT365_GENERIC_CLOUD + "/.default"], + }, + "Microsoft365China": { + "authority": AzureAuthorityHosts.AZURE_CHINA, + "base_url": MICROSOFT365_CHINA_CLOUD, + "credential_scopes": [MICROSOFT365_CHINA_CLOUD + "/.default"], + }, + "Microsoft365USGovernment": { + "authority": AzureAuthorityHosts.AZURE_GOVERNMENT, + "base_url": MICROSOFT365_US_GOV_CLOUD, + "credential_scopes": [MICROSOFT365_US_GOV_CLOUD + "/.default"], + }, + } + + for region in allowed_regions: + region_config = get_regions_config(region) + assert region_config == expected_output[region] diff --git a/tests/providers/microsoft365/microsoft365_fixtures.py b/tests/providers/microsoft365/microsoft365_fixtures.py new file mode 100644 index 00000000000..79e8ec118f1 --- /dev/null +++ b/tests/providers/microsoft365/microsoft365_fixtures.py @@ -0,0 +1,40 @@ +from azure.identity import DefaultAzureCredential +from mock import MagicMock + +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider +from prowler.providers.microsoft365.models import ( + Microsoft365IdentityInfo, + Microsoft365RegionConfig, +) + +# Azure Identity +IDENTITY_ID = "00000000-0000-0000-0000-000000000000" +IDENTITY_TYPE = "Application" +TENANT_ID = "00000000-0000-0000-0000-000000000000" +CLIENT_ID = "00000000-0000-0000-0000-000000000000" +CLIENT_SECRET = "00000000-0000-0000-0000-000000000000" +DOMAIN = "user.onmicrosoft.com" +LOCATION = "global" + + +# Mocked Azure Audit Info +def set_mocked_microsoft365_provider( + credentials: DefaultAzureCredential = DefaultAzureCredential(), + identity: Microsoft365IdentityInfo = Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type=IDENTITY_TYPE, + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + ), + audit_config: dict = None, + azure_region_config: Microsoft365RegionConfig = Microsoft365RegionConfig(), +) -> Microsoft365Provider: + + provider = MagicMock() + provider.type = "microsoft365" + provider.session.credentials = credentials + provider.identity = identity + provider.audit_config = audit_config + provider.region_config = azure_region_config + + return provider diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py new file mode 100644 index 00000000000..b475a02ca66 --- /dev/null +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -0,0 +1,314 @@ +from unittest.mock import patch +from uuid import uuid4 + +import pytest +from azure.core.credentials import AccessToken +from azure.identity import ( + ClientSecretCredential, + DefaultAzureCredential, + InteractiveBrowserCredential, +) +from mock import MagicMock + +from prowler.config.config import ( + default_config_file_path, + default_fixer_config_file_path, + load_and_validate_config_file, +) +from prowler.providers.common.models import Connection +from prowler.providers.microsoft365.exceptions.exceptions import ( + Microsoft365HTTPResponseError, + Microsoft365NoAuthenticationMethodError, +) +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider +from prowler.providers.microsoft365.models import ( + Microsoft365IdentityInfo, + Microsoft365RegionConfig, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + CLIENT_ID, + CLIENT_SECRET, + DOMAIN, + IDENTITY_ID, + IDENTITY_TYPE, + LOCATION, + TENANT_ID, +) + + +class TestMicrosoft365Provider: + def test_microsoft365_provider(self): + tenant_id = None + client_id = None + client_secret = None + + fixer_config = load_and_validate_config_file( + "microsoft365", default_fixer_config_file_path + ) + azure_region = "Microsoft365Global" + + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session", + return_value=ClientSecretCredential( + client_id=CLIENT_ID, + tenant_id=TENANT_ID, + client_secret=CLIENT_SECRET, + ), + ), + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", + return_value=Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type=IDENTITY_TYPE, + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ), + ), + ): + microsoft365_provider = Microsoft365Provider( + sp_env_auth=True, + az_cli_auth=False, + browser_auth=False, + tenant_id=tenant_id, + client_id=client_id, + client_secret=client_secret, + region=azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, + ) + + assert microsoft365_provider.region_config == Microsoft365RegionConfig( + name="Microsoft365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + assert microsoft365_provider.identity == Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type=IDENTITY_TYPE, + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ) + + def test_microsoft365_provider_cli_auth(self): + """Test Microsoft365 Provider initialization with CLI authentication""" + azure_region = "Microsoft365Global" + fixer_config = load_and_validate_config_file( + "microsoft365", default_fixer_config_file_path + ) + + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session", + return_value=DefaultAzureCredential( + exclude_environment_credential=True, + exclude_cli_credential=False, + exclude_managed_identity_credential=True, + exclude_visual_studio_code_credential=True, + exclude_shared_token_cache_credential=True, + exclude_powershell_credential=True, + exclude_browser_credential=True, + ), + ), + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", + return_value=Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ), + ), + ): + microsoft365_provider = Microsoft365Provider( + sp_env_auth=False, + az_cli_auth=True, + browser_auth=False, + region=azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, + ) + + assert microsoft365_provider.region_config == Microsoft365RegionConfig( + name="Microsoft365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + assert microsoft365_provider.identity == Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ) + assert isinstance(microsoft365_provider.session, DefaultAzureCredential) + + def test_microsoft365_provider_browser_auth(self): + """Test Microsoft365 Provider initialization with Browser authentication""" + azure_region = "Microsoft365Global" + fixer_config = load_and_validate_config_file( + "microsoft365", default_fixer_config_file_path + ) + + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session", + return_value=InteractiveBrowserCredential( + tenant_id=TENANT_ID, + ), + ), + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", + return_value=Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ), + ), + ): + microsoft365_provider = Microsoft365Provider( + sp_env_auth=False, + az_cli_auth=False, + browser_auth=True, + tenant_id=TENANT_ID, + region=azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, + ) + + assert microsoft365_provider.region_config == Microsoft365RegionConfig( + name="Microsoft365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + assert microsoft365_provider.identity == Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ) + assert isinstance( + microsoft365_provider.session, InteractiveBrowserCredential + ) + + def test_test_connection_browser_auth(self): + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" + ) as mock_default_credential, + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.microsoft365.microsoft365_provider.GraphServiceClient" + ) as mock_graph_client, + ): + + # Mock the return value of DefaultAzureCredential + mock_credentials = MagicMock() + mock_credentials.get_token.return_value = AccessToken( + token="fake_token", expires_on=9999999999 + ) + mock_default_credential.return_value = mock_credentials + + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock GraphServiceClient to avoid real API calls + mock_client = MagicMock() + mock_graph_client.return_value = mock_client + + test_connection = Microsoft365Provider.test_connection( + browser_auth=True, + tenant_id=str(uuid4()), + region="Microsoft365Global", + raise_on_exception=False, + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_tenant_id_client_id_client_secret(self): + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" + ) as mock_validate_static_credentials, + ): + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock ValidateStaticCredentials to avoid real API calls + mock_validate_static_credentials.return_value = None + + test_connection = Microsoft365Provider.test_connection( + tenant_id=str(uuid4()), + region="Microsoft365Global", + raise_on_exception=False, + client_id=str(uuid4()), + client_secret=str(uuid4()), + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_with_httpresponseerror(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session: + + mock_setup_session.side_effect = Microsoft365HTTPResponseError( + file="test_file", original_exception="Simulated HttpResponseError" + ) + + with pytest.raises(Microsoft365HTTPResponseError) as exception: + Microsoft365Provider.test_connection( + az_cli_auth=True, + raise_on_exception=True, + ) + + assert exception.type == Microsoft365HTTPResponseError + assert ( + exception.value.args[0] + == "[6003] Error in HTTP response from Microsoft365 - Simulated HttpResponseError" + ) + + def test_test_connection_with_exception(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session: + mock_setup_session.side_effect = Exception("Simulated Exception") + + with pytest.raises(Exception) as exception: + Microsoft365Provider.test_connection( + sp_env_auth=True, + raise_on_exception=True, + ) + + assert exception.type is Exception + assert exception.value.args[0] == "Simulated Exception" + + def test_test_connection_without_any_method(self): + with pytest.raises(Microsoft365NoAuthenticationMethodError) as exception: + Microsoft365Provider.test_connection() + + assert exception.type == Microsoft365NoAuthenticationMethodError + assert ( + "Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth]" + in exception.value.args[0] + ) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py new file mode 100644 index 00000000000..118aab01be8 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py @@ -0,0 +1,97 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_groups_not_public_visibility: + def test_admincenter_no_groups(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + + admincenter_client.groups = {} + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_user_no_admin(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Group, + ) + + id_group1 = str(uuid4()) + + admincenter_client.groups = { + id_group1: Group(id=id_group1, name="Group1", visibility="Private"), + } + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "Group Group1 has Private visibility." + assert result[0].resource_name == "Group1" + assert result[0].resource_id == id_group1 + + def test_admincenter_user_admin_compliant_license(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Group, + ) + + id_group1 = str(uuid4()) + + admincenter_client.groups = { + id_group1: Group(id=id_group1, name="Group1", visibility="Private"), + } + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "Group Group1 has Private visibility." + assert result[0].resource_name == "Group1" + assert result[0].resource_id == id_group1 diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py new file mode 100644 index 00000000000..1aaf11f8f4f --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py @@ -0,0 +1,84 @@ +from unittest.mock import patch + +from prowler.providers.microsoft365.models import Microsoft365IdentityInfo +from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + AdminCenter, + DirectoryRole, + Group, + User, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +async def mock_admincenter_get_users(_): + return { + "user-1@tenant1.es": User( + id="id-1", + name="User 1", + directory_roles=[], + ), + } + + +async def mock_admincenter_get_directory_roles(_): + return { + "GlobalAdministrator": DirectoryRole( + id="id-directory-role", + name="GlobalAdministrator", + members=[], + ) + } + + +async def mock_admincenter_get_groups(_): + return { + "id-1": Group(id="id-1", name="Test", visibility="Public"), + } + + +@patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_service.AdminCenter._get_users", + new=mock_admincenter_get_users, +) +@patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_service.AdminCenter._get_directory_roles", + new=mock_admincenter_get_directory_roles, +) +@patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_service.AdminCenter._get_groups", + new=mock_admincenter_get_groups, +) +class Test_AdminCenter_Service: + def test_get_client(self): + admincenter_client = AdminCenter( + set_mocked_microsoft365_provider( + identity=Microsoft365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert admincenter_client.client.__class__.__name__ == "GraphServiceClient" + + def test_get_users(self): + admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) + assert len(admincenter_client.users) == 1 + assert admincenter_client.users["user-1@tenant1.es"].id == "id-1" + assert admincenter_client.users["user-1@tenant1.es"].name == "User 1" + + def test_get_group_settings(self): + admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) + assert len(admincenter_client.groups) == 1 + assert admincenter_client.groups["id-1"].id == "id-1" + assert admincenter_client.groups["id-1"].name == "Test" + assert admincenter_client.groups["id-1"].visibility == "Public" + + def test_get_directory_roles(self): + admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) + assert ( + admincenter_client.directory_roles["GlobalAdministrator"].id + == "id-directory-role" + ) + assert ( + len(admincenter_client.directory_roles["GlobalAdministrator"].members) == 0 + ) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py new file mode 100644 index 00000000000..554cd511694 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py @@ -0,0 +1,159 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_users_admins_reduced_license_footprint: + def test_admincenter_no_users(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + admincenter_client.users = {} + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_user_no_admin(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + id_user1 = str(uuid4()) + + admincenter_client.users = { + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Exchange User"], + license="O365 BUSINESS", + ), + } + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_user_admin_compliant_license(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + id_user1 = str(uuid4()) + + admincenter_client.users = { + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Global Administrator"], + license="AAD_PREMIUM", + ), + } + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "User User1 has administrative roles Global Administrator and a valid license: AAD_PREMIUM." + ) + assert result[0].resource_name == "User1" + assert result[0].resource_id == id_user1 + + def test_admincenter_user_admin_non_compliant_license(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + id_user1 = str(uuid4()) + + admincenter_client.users = { + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Global Administrator"], + license="O365 BUSINESS", + ), + } + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "User User1 has administrative roles Global Administrator and an invalid license O365 BUSINESS." + ) + assert result[0].resource_name == "User1" + assert result[0].resource_id == id_user1 diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py new file mode 100644 index 00000000000..6416682e93b --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py @@ -0,0 +1,172 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_users_between_two_and_four_global_admins: + def test_admincenter_no_directory_roles(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + admincenter_client.directory_roles = {} + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_less_than_five_global_admins(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + DirectoryRole, + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + id = str(uuid4()) + id_user1 = str(uuid4()) + id_user2 = str(uuid4()) + + admincenter_client.directory_roles = { + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + User(id=id_user2, name="User2"), + ], + ) + } + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "There are 2 global administrators." + assert result[0].resource_name == "Global Administrator" + assert result[0].resource_id == id + + def test_admincenter_more_than_five_global_admins(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + DirectoryRole, + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + id = str(uuid4()) + id_user1 = str(uuid4()) + id_user2 = str(uuid4()) + id_user3 = str(uuid4()) + id_user4 = str(uuid4()) + id_user5 = str(uuid4()) + id_user6 = str(uuid4()) + + admincenter_client.directory_roles = { + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + User(id=id_user2, name="User2"), + User(id=id_user3, name="User3"), + User(id=id_user4, name="User4"), + User(id=id_user5, name="User5"), + User(id=id_user6, name="User6"), + ], + ) + } + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "There are 6 global administrators. It should be more than one and less than five." + ) + assert result[0].resource_name == "Global Administrator" + assert result[0].resource_id == id + + def test_admincenter_one_global_admin(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + DirectoryRole, + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + id = str(uuid4()) + id_user1 = str(uuid4()) + + admincenter_client.directory_roles = { + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + ], + ) + } + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "There are 1 global administrators. It should be more than one and less than five." + ) + assert result[0].resource_name == "Global Administrator" + assert result[0].resource_id == id From ddd83b340ef3b0fc3288fcd93dec5df0d8ff0fe6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 13:39:42 +0100 Subject: [PATCH 05/27] chore(deps): bump uuid from 10.0.0 to 11.0.5 in /ui (#6516) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 10 +++++----- ui/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index cfcadc291c4..fe7529820a7 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -47,7 +47,7 @@ "sharp": "^0.33.5", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", - "uuid": "^10.0.0", + "uuid": "^11.0.5", "zod": "^3.23.8", "zustand": "^4.5.5" }, @@ -14673,15 +14673,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/victory-vendor": { diff --git a/ui/package.json b/ui/package.json index f5c3d269694..d1a72e54588 100644 --- a/ui/package.json +++ b/ui/package.json @@ -39,7 +39,7 @@ "sharp": "^0.33.5", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", - "uuid": "^10.0.0", + "uuid": "^11.0.5", "zod": "^3.23.8", "zustand": "^4.5.5" }, From 45d44a16698cf4fdd7caa63e62a7ffe95bbc3a8a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 28 Jan 2025 06:58:18 +0100 Subject: [PATCH 06/27] fix: fixed bug when opening finding details while a scan is in progress (#6708) --- ui/components/ui/entities/date-with-time.tsx | 39 +++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/ui/components/ui/entities/date-with-time.tsx b/ui/components/ui/entities/date-with-time.tsx index 97c5fa6f6b3..17600762c13 100644 --- a/ui/components/ui/entities/date-with-time.tsx +++ b/ui/components/ui/entities/date-with-time.tsx @@ -13,20 +13,31 @@ export const DateWithTime: React.FC = ({ inline = false, }) => { if (!dateTime) return --; - const date = parseISO(dateTime); - const formattedDate = format(date, "MMM dd, yyyy"); - const formattedTime = format(date, "p 'UTC'"); - return ( -
-
- {formattedDate} - {showTime && ( - {formattedTime} - )} + try { + const date = parseISO(dateTime); + + // Validate if the date is valid + if (isNaN(date.getTime())) { + return -; + } + + const formattedDate = format(date, "MMM dd, yyyy"); + const formattedTime = format(date, "p 'UTC'"); + + return ( +
+
+ {formattedDate} + {showTime && ( + {formattedTime} + )} +
-
- ); + ); + } catch (error) { + return -; + } }; From 4d2859d145d021a67e36864a0a0ec9cd91bd76d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Tue, 28 Jan 2025 11:56:58 +0100 Subject: [PATCH 07/27] fix(scans, findings): Improve API performance ordering by inserted_at instead of id (#6711) --- api/pyproject.toml | 2 +- api/src/backend/api/specs/v1.yaml | 2 +- api/src/backend/api/v1/views.py | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index be3234bfba1..5442a1a6574 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -8,7 +8,7 @@ description = "Prowler's API (Django/DRF)" license = "Apache-2.0" name = "prowler-api" package-mode = false -version = "1.3.0" +version = "1.3.1" [tool.poetry.dependencies] celery = {extras = ["pytest"], version = "^5.4.0"} diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index e43b6ba5163..38bc2a9cd15 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.2.0 + version: 1.3.1 description: |- Prowler API specification. diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index c12da92026a..a67e38e1caa 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -194,7 +194,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.2.0" + spectacular_settings.VERSION = "1.3.1" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -1306,9 +1306,8 @@ class FindingViewSet(BaseRLSViewSet): } http_method_names = ["get"] filterset_class = FindingFilter - ordering = ["-id"] + ordering = ["-inserted_at"] ordering_fields = [ - "id", "status", "severity", "check_id", @@ -2006,7 +2005,7 @@ def providers(self, request): uid=OuterRef("uid"), scan__provider=OuterRef("scan__provider"), ) - .order_by("-id") # Most recent by id + .order_by("-inserted_at") # Most recent .values("id")[:1] ) @@ -2075,7 +2074,7 @@ def findings(self, request): state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id"), ) - .order_by("-id") + .order_by("-inserted_at") .values("id")[:1] ) @@ -2120,7 +2119,7 @@ def findings_severity(self, request): state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id"), ) - .order_by("-id") + .order_by("-inserted_at") .values("id")[:1] ) @@ -2156,7 +2155,7 @@ def services(self, request): state=StateChoices.COMPLETED, provider_id=OuterRef("scan__provider_id"), ) - .order_by("-id") + .order_by("-inserted_at") .values("id")[:1] ) From 44281afc5472130ce0beb4f32d57f46930596c70 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 28 Jan 2025 13:26:31 +0100 Subject: [PATCH 08/27] fix(scans): filters and sorting for scan table (#6713) --- ui/app/(prowler)/scans/page.tsx | 4 +++- ui/components/filters/data-filters.ts | 4 ++-- ui/components/scans/table/scans/column-get-scans.tsx | 12 ++---------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index 7bd1317c128..97816271188 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react"; import { getProvider, getProviders } from "@/actions/providers"; import { getScans } from "@/actions/scans"; -import { filterScans } from "@/components/filters"; +import { FilterControls, filterScans } from "@/components/filters"; import { ButtonRefreshData, NoProvidersAdded, @@ -83,6 +83,8 @@ export default async function Scans({
+ + { "use server"; diff --git a/ui/components/filters/data-filters.ts b/ui/components/filters/data-filters.ts index 4b40aff8e67..9cf93fb3927 100644 --- a/ui/components/filters/data-filters.ts +++ b/ui/components/filters/data-filters.ts @@ -14,8 +14,8 @@ export const filterScans = [ values: ["aws", "azure", "gcp", "kubernetes"], }, { - key: "state", - labelCheckboxGroup: "State", + key: "state__in", + labelCheckboxGroup: "Status", values: [ "available", "scheduled", diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx index 1fec3742f0f..87113c21fad 100644 --- a/ui/components/scans/table/scans/column-get-scans.tsx +++ b/ui/components/scans/table/scans/column-get-scans.tsx @@ -81,9 +81,7 @@ export const ColumnGetScans: ColumnDef[] = [ }, { accessorKey: "status", - header: ({ column }) => ( - - ), + header: "Status", cell: ({ row }) => { const { attributes: { state }, @@ -126,13 +124,7 @@ export const ColumnGetScans: ColumnDef[] = [ }, { accessorKey: "next_scan_at", - header: ({ column }) => ( - - ), + header: "Scheduled at", cell: ({ row }) => { const { attributes: { next_scan_at }, From 47bc2ed2dc472fc24ecab35970205448ab3b090d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Tue, 28 Jan 2025 15:52:56 +0100 Subject: [PATCH 09/27] fix(defender): add field to SecurityContacts (#6693) --- prowler/providers/azure/services/defender/defender_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/providers/azure/services/defender/defender_service.py b/prowler/providers/azure/services/defender/defender_service.py index 25a0e4f34db..ef90ed7e70f 100644 --- a/prowler/providers/azure/services/defender/defender_service.py +++ b/prowler/providers/azure/services/defender/defender_service.py @@ -176,6 +176,7 @@ def _get_security_contacts(self): { "default": SecurityContacts( resource_id=f"/subscriptions/{self.subscriptions[subscription_name]}/providers/Microsoft.Security/securityContacts/default", + name="", emails="", phone="", alert_notifications_minimal_severity="", From 06dd03b1708fa35a4d56b1174f82847e2ab162ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Tue, 28 Jan 2025 17:11:29 +0100 Subject: [PATCH 10/27] fix(scan-summaries): Improve efficiency on providers overview (#6716) --- .../0007_scan_and_scan_summaries_indexes.py | 25 +++++++ api/src/backend/api/models.py | 8 +++ api/src/backend/api/specs/v1.yaml | 8 +-- api/src/backend/api/tests/test_views.py | 13 ++-- api/src/backend/api/v1/serializers.py | 4 +- api/src/backend/api/v1/views.py | 68 +++++++------------ 6 files changed, 67 insertions(+), 59 deletions(-) create mode 100644 api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py diff --git a/api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py b/api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py new file mode 100644 index 00000000000..819016d1450 --- /dev/null +++ b/api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.5 on 2025-01-28 15:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0006_findings_first_seen"), + ] + + operations = [ + migrations.AddIndex( + model_name="scan", + index=models.Index( + fields=["tenant_id", "provider_id", "state", "inserted_at"], + name="scans_prov_state_insert_idx", + ), + ), + migrations.AddConstraint( + model_name="scansummary", + constraint=models.Index( + fields=["tenant_id", "scan_id"], name="scan_summaries_tenant_scan_idx" + ), + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index d1b9434246f..bbb4663369b 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -428,6 +428,10 @@ class Meta(RowLevelSecurityProtectedModel.Meta): fields=["provider", "state", "trigger", "scheduled_at"], name="scans_prov_state_trig_sche_idx", ), + models.Index( + fields=["tenant_id", "provider_id", "state", "inserted_at"], + name="scans_prov_state_insert_idx", + ), ] class JSONAPIMeta: @@ -1094,6 +1098,10 @@ class Meta(RowLevelSecurityProtectedModel.Meta): fields=("tenant", "scan", "check_id", "service", "severity", "region"), name="unique_scan_summary", ), + models.Index( + fields=["tenant_id", "scan_id"], + name="scan_summaries_tenant_scan_idx", + ), RowLevelSecurityConstraint( field="tenant_id", name="rls_on_%(class)s", diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 38bc2a9cd15..cb355b1aa7b 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -714,8 +714,6 @@ paths: items: type: string enum: - - id - - -id - status - -status - severity @@ -1242,8 +1240,6 @@ paths: items: type: string enum: - - id - - -id - status - -status - severity @@ -1714,8 +1710,6 @@ paths: items: type: string enum: - - id - - -id - status - -status - severity @@ -6753,7 +6747,7 @@ components: type: integer fail: type: integer - manual: + muted: type: integer total: type: integer diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index da50c8ca915..184843af185 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -4280,18 +4280,15 @@ def test_overview_list_invalid_method(self, authenticated_client): assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_overview_providers_list( - self, authenticated_client, findings_fixture, resources_fixture + self, authenticated_client, scan_summaries_fixture, resources_fixture ): response = authenticated_client.get(reverse("overview-providers")) assert response.status_code == status.HTTP_200_OK - # Only findings from one provider assert len(response.json()["data"]) == 1 - assert response.json()["data"][0]["attributes"]["findings"]["total"] == len( - findings_fixture - ) - assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 0 - assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 2 - assert response.json()["data"][0]["attributes"]["findings"]["manual"] == 0 + assert response.json()["data"][0]["attributes"]["findings"]["total"] == 4 + assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 2 + assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 1 + assert response.json()["data"][0]["attributes"]["findings"]["muted"] == 1 assert response.json()["data"][0]["attributes"]["resources"]["total"] == len( resources_fixture ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index e73825daa17..6046799d0cf 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1735,7 +1735,7 @@ def get_root_meta(self, _resource, _many): "properties": { "pass": {"type": "integer"}, "fail": {"type": "integer"}, - "manual": {"type": "integer"}, + "muted": {"type": "integer"}, "total": {"type": "integer"}, }, } @@ -1744,7 +1744,7 @@ def get_findings(self, obj): return { "pass": obj["findings_passed"], "fail": obj["findings_failed"], - "manual": obj["findings_manual"], + "muted": obj["findings_muted"], "total": obj["total_findings"], } diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index a67e38e1caa..debfcdd57c0 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -4,7 +4,7 @@ from django.contrib.postgres.search import SearchQuery from django.db import transaction from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum -from django.db.models.functions import JSONObject +from django.db.models.functions import Coalesce, JSONObject from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control @@ -74,7 +74,6 @@ ScanSummary, SeverityChoices, StateChoices, - StatusChoices, Task, User, UserRoleRelationship, @@ -1998,68 +1997,53 @@ def retrieve(self, request, *args, **kwargs): @action(detail=False, methods=["get"], url_name="providers") def providers(self, request): tenant_id = self.request.tenant_id - # Subquery to get the most recent finding for each uid - latest_finding_ids = ( - Finding.objects.filter( + + latest_scan_ids = ( + Scan.objects.filter( tenant_id=tenant_id, - uid=OuterRef("uid"), - scan__provider=OuterRef("scan__provider"), + state=StateChoices.COMPLETED, ) - .order_by("-inserted_at") # Most recent - .values("id")[:1] - ) - - # Filter findings to only include the most recent for each uid - recent_findings = Finding.objects.filter( - tenant_id=tenant_id, id__in=Subquery(latest_finding_ids) + .order_by("provider_id", "-inserted_at") + .distinct("provider_id") + .values_list("id", flat=True) ) - # Aggregate findings by provider findings_aggregated = ( - recent_findings.values("scan__provider__provider") + ScanSummary.objects.filter(tenant_id=tenant_id, scan_id__in=latest_scan_ids) + .values("scan__provider__provider") .annotate( - findings_passed=Count("id", filter=Q(status=StatusChoices.PASS.value)), - findings_failed=Count("id", filter=Q(status=StatusChoices.FAIL.value)), - findings_manual=Count( - "id", filter=Q(status=StatusChoices.MANUAL.value) - ), - total_findings=Count("id"), + findings_passed=Coalesce(Sum("_pass"), 0), + findings_failed=Coalesce(Sum("fail"), 0), + findings_muted=Coalesce(Sum("muted"), 0), + total_findings=Coalesce(Sum("total"), 0), ) - .order_by("-findings_failed") ) - # Aggregate total resources by provider resources_aggregated = ( Resource.objects.filter(tenant_id=tenant_id) .values("provider__provider") .annotate(total_resources=Count("id")) ) + resources_dict = { + row["provider__provider"]: row["total_resources"] + for row in resources_aggregated + } - # Combine findings and resources data overview = [] - for findings in findings_aggregated: - provider = findings["scan__provider__provider"] - total_resources = next( - ( - res["total_resources"] - for res in resources_aggregated - if res["provider__provider"] == provider - ), - 0, - ) + for row in findings_aggregated: + provider_type = row["scan__provider__provider"] overview.append( { - "provider": provider, - "total_resources": total_resources, - "total_findings": findings["total_findings"], - "findings_passed": findings["findings_passed"], - "findings_failed": findings["findings_failed"], - "findings_manual": findings["findings_manual"], + "provider": provider_type, + "total_resources": resources_dict.get(provider_type, 0), + "total_findings": row["total_findings"], + "findings_passed": row["findings_passed"], + "findings_failed": row["findings_failed"], + "findings_muted": row["findings_muted"], } ) serializer = OverviewProviderSerializer(overview, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) @action(detail=False, methods=["get"], url_name="findings") From 84955c066c2ded44cee35b457b89c6445082c7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Tue, 28 Jan 2025 17:30:01 +0100 Subject: [PATCH 11/27] revert: Update Django DB manager to use psycopg3 and connection pooling (#6717) --- .env | 4 - api/.env.example | 4 - api/poetry.lock | 906 ++++++++++---------- api/pyproject.toml | 2 +- api/src/backend/api/db_utils.py | 61 +- api/src/backend/config/django/base.py | 6 - api/src/backend/config/django/devel.py | 16 - api/src/backend/config/django/production.py | 16 - api/src/backend/config/django/testing.py | 8 - poetry.lock | 7 +- 10 files changed, 471 insertions(+), 559 deletions(-) diff --git a/.env b/.env index 40eab3da901..b0f3dfd7f19 100644 --- a/.env +++ b/.env @@ -92,7 +92,3 @@ jQIDAQAB # openssl rand -base64 32 DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc=" DJANGO_BROKER_VISIBILITY_TIMEOUT=86400 -DJANGO_DB_CONNECTION_POOL_MIN_SIZE=4 -DJANGO_DB_CONNECTION_POOL_MAX_SIZE=10 -DJANGO_DB_CONNECTION_POOL_MAX_IDLE=36000 -DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME=86400 diff --git a/api/.env.example b/api/.env.example index cba05aa602f..4b0894b3ba9 100644 --- a/api/.env.example +++ b/api/.env.example @@ -23,10 +23,6 @@ DJANGO_SECRETS_ENCRYPTION_KEY="" DJANGO_MANAGE_DB_PARTITIONS=[True|False] DJANGO_CELERY_DEADLOCK_ATTEMPTS=5 DJANGO_BROKER_VISIBILITY_TIMEOUT=86400 -DJANGO_DB_CONNECTION_POOL_MIN_SIZE=4 -DJANGO_DB_CONNECTION_POOL_MAX_SIZE=10 -DJANGO_DB_CONNECTION_POOL_MAX_IDLE=36000 -DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME=86400 # PostgreSQL settings # If running django and celery on host, use 'localhost', else use 'postgres-db' diff --git a/api/poetry.lock b/api/poetry.lock index 25d94edd6a2..ec24e9da8d4 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -164,13 +164,13 @@ vine = ">=5.0.0,<6.0.0" [[package]] name = "anyio" -version = "4.7.0" +version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, - {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, ] [package.dependencies] @@ -180,7 +180,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -221,13 +221,13 @@ files = [ [[package]] name = "attrs" -version = "24.3.0" +version = "25.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [package.extras] @@ -240,13 +240,13 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "authlib" -version = "1.3.2" +version = "1.4.1" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"}, - {file = "authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2"}, + {file = "Authlib-1.4.1-py2.py3-none-any.whl", hash = "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"}, + {file = "authlib-1.4.1.tar.gz", hash = "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d"}, ] [package.dependencies] @@ -362,13 +362,13 @@ isodate = ">=0.6.1,<1.0.0" [[package]] name = "azure-mgmt-compute" -version = "33.1.0" +version = "34.0.0" description = "Microsoft Azure Compute Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure_mgmt_compute-33.1.0-py3-none-any.whl", hash = "sha256:9b119a1b7f621aee951074ef110b16d545d7b45c9cfb303becf04210bd772c91"}, - {file = "azure_mgmt_compute-33.1.0.tar.gz", hash = "sha256:f5a5e18a5a7a0354562bbfa589b5db4aaf1e8ac3a194a2a910db55900d2535e9"}, + {file = "azure_mgmt_compute-34.0.0-py3-none-any.whl", hash = "sha256:f8f7b1c5c187a26fae4d1f099adf93561244242f28899484d9a42747bf0d5af4"}, + {file = "azure_mgmt_compute-34.0.0.tar.gz", hash = "sha256:58cd01d025efa02870b84dbfb69834a3b23501a135658c03854d2434e8dfee1e"}, ] [package.dependencies] @@ -395,13 +395,13 @@ isodate = ">=0.6.1,<1.0.0" [[package]] name = "azure-mgmt-containerservice" -version = "33.0.0" +version = "34.0.0" description = "Microsoft Azure Container Service Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure_mgmt_containerservice-33.0.0-py3-none-any.whl", hash = "sha256:5b36cb25075c9fc3070135b5a45debae9eca3183b00f8c21faddb37b2daf4a60"}, - {file = "azure_mgmt_containerservice-33.0.0.tar.gz", hash = "sha256:868583dcdb8a4905de03a84a9b7903d76a1cb59acd9c3736f02bc743b5047c9e"}, + {file = "azure_mgmt_containerservice-34.0.0-py3-none-any.whl", hash = "sha256:34be8172241e3c2c444682407970a938f60e3b2bd06304eaae0a1ba641f2262d"}, + {file = "azure_mgmt_containerservice-34.0.0.tar.gz", hash = "sha256:822d07828b746a5ea5408a8b3770f41dc424d6c4c28de53c29611b62bef8aea3"}, ] [package.dependencies] @@ -606,13 +606,13 @@ msrest = ">=0.7.1" [[package]] name = "azure-mgmt-web" -version = "7.3.1" +version = "8.0.0" description = "Microsoft Azure Web Apps Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-mgmt-web-7.3.1.tar.gz", hash = "sha256:87b771436bc99a7a8df59d0ad185b96879a06dce14764a06b3fc3dafa8fcb56b"}, - {file = "azure_mgmt_web-7.3.1-py3-none-any.whl", hash = "sha256:ccf881e3ab31c3fdbf9cbff32773d9c0006b5dcd621ea074d7ec89e51049fb72"}, + {file = "azure_mgmt_web-8.0.0-py3-none-any.whl", hash = "sha256:0536aac05bfc673b56ed930f2966b77856e84df675d376e782a7af6bb92449af"}, + {file = "azure_mgmt_web-8.0.0.tar.gz", hash = "sha256:c8d9c042c09db7aacb20270a9effed4d4e651e365af32d80897b84dc7bf35098"}, ] [package.dependencies] @@ -623,13 +623,13 @@ typing-extensions = ">=4.6.0" [[package]] name = "azure-storage-blob" -version = "12.24.0" +version = "12.24.1" description = "Microsoft Azure Blob Storage Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure_storage_blob-12.24.0-py3-none-any.whl", hash = "sha256:4f0bb4592ea79a2d986063696514c781c9e62be240f09f6397986e01755bc071"}, - {file = "azure_storage_blob-12.24.0.tar.gz", hash = "sha256:eaaaa1507c8c363d6e1d1342bd549938fdf1adec9b1ada8658c8f5bf3aea844e"}, + {file = "azure_storage_blob-12.24.1-py3-none-any.whl", hash = "sha256:77fb823fdbac7f3c11f7d86a5892e2f85e161e8440a7489babe2195bf248f09e"}, + {file = "azure_storage_blob-12.24.1.tar.gz", hash = "sha256:052b2a1ea41725ba12e2f4f17be85a54df1129e13ea0321f5a2fcc851cbf47d4"}, ] [package.dependencies] @@ -689,17 +689,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.94" +version = "1.35.99" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.94-py3-none-any.whl", hash = "sha256:516c514fb447d6f216833d06a0781c003fcf43099a4ca2f5a363a8afe0942070"}, - {file = "boto3-1.35.94.tar.gz", hash = "sha256:5aa606239f0fe0dca0506e0ad6bbe4c589048e7e6c2486cee5ec22b6aa7ec2f8"}, + {file = "boto3-1.35.99-py3-none-any.whl", hash = "sha256:83e560faaec38a956dfb3d62e05e1703ee50432b45b788c09e25107c5058bd71"}, + {file = "boto3-1.35.99.tar.gz", hash = "sha256:e0abd794a7a591d90558e92e29a9f8837d25ece8e3c120e530526fe27eba5fca"}, ] [package.dependencies] -botocore = ">=1.35.94,<1.36.0" +botocore = ">=1.35.99,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -727,13 +727,13 @@ crt = ["awscrt (==0.22.0)"] [[package]] name = "cachetools" -version = "5.5.0" +version = "5.5.1" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, - {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, + {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, + {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, ] [[package]] @@ -885,127 +885,114 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -1283,55 +1270,55 @@ files = [ [[package]] name = "debugpy" -version = "1.8.11" +version = "1.8.12" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, - {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, - {file = "debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737"}, - {file = "debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1"}, - {file = "debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296"}, - {file = "debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1"}, - {file = "debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9"}, - {file = "debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e"}, - {file = "debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308"}, - {file = "debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768"}, - {file = "debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b"}, - {file = "debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1"}, - {file = "debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3"}, - {file = "debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e"}, - {file = "debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28"}, - {file = "debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"}, - {file = "debugpy-1.8.11-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db"}, - {file = "debugpy-1.8.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0"}, - {file = "debugpy-1.8.11-cp38-cp38-win32.whl", hash = "sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280"}, - {file = "debugpy-1.8.11-cp38-cp38-win_amd64.whl", hash = "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5"}, - {file = "debugpy-1.8.11-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458"}, - {file = "debugpy-1.8.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851"}, - {file = "debugpy-1.8.11-cp39-cp39-win32.whl", hash = "sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7"}, - {file = "debugpy-1.8.11-cp39-cp39-win_amd64.whl", hash = "sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0"}, - {file = "debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920"}, - {file = "debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57"}, + {file = "debugpy-1.8.12-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:a2ba7ffe58efeae5b8fad1165357edfe01464f9aef25e814e891ec690e7dd82a"}, + {file = "debugpy-1.8.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbbd4149c4fc5e7d508ece083e78c17442ee13b0e69bfa6bd63003e486770f45"}, + {file = "debugpy-1.8.12-cp310-cp310-win32.whl", hash = "sha256:b202f591204023b3ce62ff9a47baa555dc00bb092219abf5caf0e3718ac20e7c"}, + {file = "debugpy-1.8.12-cp310-cp310-win_amd64.whl", hash = "sha256:9649eced17a98ce816756ce50433b2dd85dfa7bc92ceb60579d68c053f98dff9"}, + {file = "debugpy-1.8.12-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:36f4829839ef0afdfdd208bb54f4c3d0eea86106d719811681a8627ae2e53dd5"}, + {file = "debugpy-1.8.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a28ed481d530e3138553be60991d2d61103ce6da254e51547b79549675f539b7"}, + {file = "debugpy-1.8.12-cp311-cp311-win32.whl", hash = "sha256:4ad9a94d8f5c9b954e0e3b137cc64ef3f579d0df3c3698fe9c3734ee397e4abb"}, + {file = "debugpy-1.8.12-cp311-cp311-win_amd64.whl", hash = "sha256:4703575b78dd697b294f8c65588dc86874ed787b7348c65da70cfc885efdf1e1"}, + {file = "debugpy-1.8.12-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:7e94b643b19e8feb5215fa508aee531387494bf668b2eca27fa769ea11d9f498"}, + {file = "debugpy-1.8.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:086b32e233e89a2740c1615c2f775c34ae951508b28b308681dbbb87bba97d06"}, + {file = "debugpy-1.8.12-cp312-cp312-win32.whl", hash = "sha256:2ae5df899732a6051b49ea2632a9ea67f929604fd2b036613a9f12bc3163b92d"}, + {file = "debugpy-1.8.12-cp312-cp312-win_amd64.whl", hash = "sha256:39dfbb6fa09f12fae32639e3286112fc35ae976114f1f3d37375f3130a820969"}, + {file = "debugpy-1.8.12-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:696d8ae4dff4cbd06bf6b10d671e088b66669f110c7c4e18a44c43cf75ce966f"}, + {file = "debugpy-1.8.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:898fba72b81a654e74412a67c7e0a81e89723cfe2a3ea6fcd3feaa3395138ca9"}, + {file = "debugpy-1.8.12-cp313-cp313-win32.whl", hash = "sha256:22a11c493c70413a01ed03f01c3c3a2fc4478fc6ee186e340487b2edcd6f4180"}, + {file = "debugpy-1.8.12-cp313-cp313-win_amd64.whl", hash = "sha256:fdb3c6d342825ea10b90e43d7f20f01535a72b3a1997850c0c3cefa5c27a4a2c"}, + {file = "debugpy-1.8.12-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:b0232cd42506d0c94f9328aaf0d1d0785f90f87ae72d9759df7e5051be039738"}, + {file = "debugpy-1.8.12-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9af40506a59450f1315168d47a970db1a65aaab5df3833ac389d2899a5d63b3f"}, + {file = "debugpy-1.8.12-cp38-cp38-win32.whl", hash = "sha256:5cc45235fefac57f52680902b7d197fb2f3650112379a6fa9aa1b1c1d3ed3f02"}, + {file = "debugpy-1.8.12-cp38-cp38-win_amd64.whl", hash = "sha256:557cc55b51ab2f3371e238804ffc8510b6ef087673303890f57a24195d096e61"}, + {file = "debugpy-1.8.12-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:b5c6c967d02fee30e157ab5227706f965d5c37679c687b1e7bbc5d9e7128bd41"}, + {file = "debugpy-1.8.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a77f422f31f170c4b7e9ca58eae2a6c8e04da54121900651dfa8e66c29901a"}, + {file = "debugpy-1.8.12-cp39-cp39-win32.whl", hash = "sha256:a4042edef80364239f5b7b5764e55fd3ffd40c32cf6753da9bda4ff0ac466018"}, + {file = "debugpy-1.8.12-cp39-cp39-win_amd64.whl", hash = "sha256:f30b03b0f27608a0b26c75f0bb8a880c752c0e0b01090551b9d87c7d783e2069"}, + {file = "debugpy-1.8.12-py2.py3-none-any.whl", hash = "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6"}, + {file = "debugpy-1.8.12.tar.gz", hash = "sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce"}, ] [[package]] name = "deprecated" -version = "1.2.15" +version = "1.2.18" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" files = [ - {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, - {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, ] [package.dependencies] wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] [[package]] name = "detect-secrets" @@ -1503,13 +1490,13 @@ test = ["coveralls (==3.3.0)", "dj-database-url (==0.5.0)", "freezegun (==1.1.0) [[package]] name = "django-timezone-field" -version = "7.0" +version = "7.1" description = "A Django app providing DB, form, and REST framework fields for zoneinfo and pytz timezone objects." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "django_timezone_field-7.0-py3-none-any.whl", hash = "sha256:3232e7ecde66ba4464abb6f9e6b8cc739b914efb9b29dc2cf2eee451f7cc2acb"}, - {file = "django_timezone_field-7.0.tar.gz", hash = "sha256:aa6f4965838484317b7f08d22c0d91a53d64e7bbbd34264468ae83d4023898a7"}, + {file = "django_timezone_field-7.1-py3-none-any.whl", hash = "sha256:93914713ed882f5bccda080eda388f7006349f25930b6122e9b07bf8db49c4b4"}, + {file = "django_timezone_field-7.1.tar.gz", hash = "sha256:b3ef409d88a2718b566fabe10ea996f2838bc72b22d3a2900c0aa905c761380c"}, ] [package.dependencies] @@ -1552,18 +1539,18 @@ openapi = ["pyyaml (>=5.4)", "uritemplate (>=3.0.1)"] [[package]] name = "djangorestframework-simplejwt" -version = "5.3.1" +version = "5.4.0" description = "A minimal JSON Web Token authentication plugin for Django REST Framework" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "djangorestframework_simplejwt-5.3.1-py3-none-any.whl", hash = "sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220"}, - {file = "djangorestframework_simplejwt-5.3.1.tar.gz", hash = "sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae"}, + {file = "djangorestframework_simplejwt-5.4.0-py3-none-any.whl", hash = "sha256:7aec953db9ed4163430c16d086eecb0f028f814ce6bba62b06c25919261e9077"}, + {file = "djangorestframework_simplejwt-5.4.0.tar.gz", hash = "sha256:cccecce1a0e1a4a240fae80da73e5fc23055bababb8b67de88fa47cd36822320"}, ] [package.dependencies] -django = ">=3.2" -djangorestframework = ">=3.12" +django = ">=4.2" +djangorestframework = ">=3.14" pyjwt = ">=1.7.1,<3" [package.extras] @@ -1901,13 +1888,13 @@ files = [ [[package]] name = "google-api-core" -version = "2.24.0" +version = "2.24.1" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_core-2.24.0-py3-none-any.whl", hash = "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9"}, - {file = "google_api_core-2.24.0.tar.gz", hash = "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"}, + {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, + {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, ] [package.dependencies] @@ -1943,13 +1930,13 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.37.0" +version = "2.38.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"}, - {file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"}, + {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, + {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, ] [package.dependencies] @@ -2059,13 +2046,13 @@ hyperframe = ">=6.0,<7" [[package]] name = "hpack" -version = "4.0.0" -description = "Pure-Python HPACK header compression" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.9" files = [ - {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, - {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, ] [[package]] @@ -2105,13 +2092,13 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -2120,7 +2107,6 @@ certifi = "*" h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] @@ -2131,13 +2117,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "hyperframe" -version = "6.0.1" -description = "HTTP/2 framing layer for Python" +version = "6.1.0" +description = "Pure-Python HTTP/2 framing" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.9" files = [ - {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, - {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, ] [[package]] @@ -2454,13 +2440,13 @@ files = [ [[package]] name = "marshmallow" -version = "3.23.2" +version = "3.26.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" files = [ - {file = "marshmallow-3.23.2-py3-none-any.whl", hash = "sha256:bcaf2d6fd74fb1459f8450e85d994997ad3e70036452cbfa4ab685acb19479b3"}, - {file = "marshmallow-3.23.2.tar.gz", hash = "sha256:c448ac6455ca4d794773f00bae22c2f351d62d739929f761dce5eacb5c468d7f"}, + {file = "marshmallow-3.26.0-py3-none-any.whl", hash = "sha256:1287bca04e6a5f4094822ac153c03da5e214a0a60bcd557b140f3e66991b8ca1"}, + {file = "marshmallow-3.26.0.tar.gz", hash = "sha256:eb36762a1cc76d7abf831e18a3a1b26d3d481bbc74581b8e532a3d3a8115e1cb"}, ] [package.dependencies] @@ -2468,7 +2454,7 @@ packaging = ">=17.0" [package.extras] dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] -docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.14)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-version-warning (==1.1.2)"] +docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"] tests = ["pytest", "simplejson"] [[package]] @@ -2511,98 +2497,98 @@ std-uritemplate = ">=2.0.0" [[package]] name = "microsoft-kiota-authentication-azure" -version = "1.6.2" +version = "1.6.8" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "microsoft_kiota_authentication_azure-1.6.2-py3-none-any.whl", hash = "sha256:e9dfecdb4c45ddfcc00def1abd1b9cbcf8970cb3d7ae799efa8414b6e9aaf226"}, - {file = "microsoft_kiota_authentication_azure-1.6.2.tar.gz", hash = "sha256:f24c4c8334a32d345df5216edfdf78dd4a3f8bdca2d2d7de3c936e007bba81d4"}, + {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"}, ] [package.dependencies] aiohttp = ">=3.8.0" azure-core = ">=1.21.1" -microsoft-kiota-abstractions = ">=1.6.2,<1.7.0" +microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" opentelemetry-api = ">=1.27.0" opentelemetry-sdk = ">=1.27.0" [[package]] name = "microsoft-kiota-http" -version = "1.6.2" +version = "1.6.8" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "microsoft_kiota_http-1.6.2-py3-none-any.whl", hash = "sha256:5914d1b95f1969b1cb7403888ee0c65a3211953a520f41fc94a0e8f2135689f5"}, - {file = "microsoft_kiota_http-1.6.2.tar.gz", hash = "sha256:f70ae4cd712ba81253cbff3cb98e80d33bb70e1ff57de39c72eb2b13dd48b47b"}, + {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"}, ] [package.dependencies] -httpx = {version = ">=0.23,<0.28", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.6.2,<1.7.0" +httpx = {version = ">=0.28", extras = ["http2"]} +microsoft-kiota-abstractions = ">=1.6.8,<1.7.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.2" +version = "1.6.8" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "microsoft_kiota_serialization_form-1.6.2-py3-none-any.whl", hash = "sha256:c8eeb287a3a1102807685e3ce3e50f72074ef0fc8c42d37476da5c288960d49e"}, - {file = "microsoft_kiota_serialization_form-1.6.2.tar.gz", hash = "sha256:e03263a851d17b66e20f083f1d95886eec2652a5fdcce7873c0033a7362b8b3e"}, + {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"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.2,<1.7.0" +microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" pendulum = ">=3.0.0b1" [[package]] name = "microsoft-kiota-serialization-json" -version = "1.6.2" +version = "1.6.8" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "microsoft_kiota_serialization_json-1.6.2-py3-none-any.whl", hash = "sha256:f20791574b62899db2d42dd6202bc982008ff17a69960dcece8f5ac87a7f5276"}, - {file = "microsoft_kiota_serialization_json-1.6.2.tar.gz", hash = "sha256:46370cb3563b8167829142ab7b745b28cbf743d367ffc6a25995b5d832537b1e"}, + {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"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.2,<1.7.0" +microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" pendulum = ">=3.0.0b1" [[package]] name = "microsoft-kiota-serialization-multipart" -version = "1.6.2" +version = "1.6.8" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "microsoft_kiota_serialization_multipart-1.6.2-py3-none-any.whl", hash = "sha256:7b798b8cce2731217e39a144a7d13164bf95a1b250845cbd53ccebec2e7e735b"}, - {file = "microsoft_kiota_serialization_multipart-1.6.2.tar.gz", hash = "sha256:b3839a2695880e6cf83b49cd15b7050549538c14d2d50ca8d0d62f0ad4b66924"}, + {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"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.2,<1.7.0" +microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" pendulum = ">=3.0.0b1" [[package]] name = "microsoft-kiota-serialization-text" -version = "1.6.2" +version = "1.6.8" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "microsoft_kiota_serialization_text-1.6.2-py3-none-any.whl", hash = "sha256:5c80aff9e4f7765ec530d5cac30318a194f783f7fe6be36b3f1e7d5cb4389689"}, - {file = "microsoft_kiota_serialization_text-1.6.2.tar.gz", hash = "sha256:2a01d7257973faa8862fb759f0e957cb0c59e0760c30082a3a6efaafd60add2a"}, + {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"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.2,<1.7.0" +microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" python-dateutil = "2.9.0.post0" [[package]] @@ -2641,13 +2627,13 @@ portalocker = ">=1.4,<3" [[package]] name = "msgraph-core" -version = "1.1.8" +version = "1.2.0" description = "Core component of the Microsoft Graph Python SDK" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "msgraph_core-1.1.8-py3-none-any.whl", hash = "sha256:5b8d28ec16f6b2b9ef328b01368aa4500166afb0fa3748c1255b4340b344197f"}, - {file = "msgraph_core-1.1.8.tar.gz", hash = "sha256:58c50f1cfdf0098dc9120b8565988ecf7d7e0be6ae9e8a3c1b4805415469159a"}, + {file = "msgraph_core-1.2.0-py3-none-any.whl", hash = "sha256:4ce14bbe743c0f2dd8b53c7fcd338fc14081e0df6b14021f0d0e4bb63cd5a840"}, + {file = "msgraph_core-1.2.0.tar.gz", hash = "sha256:a4e42f692e664c60d63359e610bbf990f57b42d8080417261ff7042bbd59c98b"}, ] [package.dependencies] @@ -2661,13 +2647,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] [[package]] name = "msgraph-sdk" -version = "1.17.0" +version = "1.18.0" description = "The Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" files = [ - {file = "msgraph_sdk-1.17.0-py3-none-any.whl", hash = "sha256:5582a258ded19a486ab407a67b5f65d666758a63864da77bd20c2581d1c00fba"}, - {file = "msgraph_sdk-1.17.0.tar.gz", hash = "sha256:577e41942b0f794b8cf2f54db030bc039a750a81b515dcd0ba1d66fd961fa7bf"}, + {file = "msgraph_sdk-1.18.0-py3-none-any.whl", hash = "sha256:f09b015bb9d7690bc6f30c9a28f9a414107aaf06be4952c27b3653dcdf33f2a3"}, + {file = "msgraph_sdk-1.18.0.tar.gz", hash = "sha256:ef49166ada7b459b5a843ceb3d253c1ab99d8987ebf3112147eb6cbcaa101793"}, ] [package.dependencies] @@ -3263,13 +3249,13 @@ tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "p [[package]] name = "prompt-toolkit" -version = "3.0.48" +version = "3.0.50" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, - {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, + {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, + {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, ] [package.dependencies] @@ -3368,13 +3354,13 @@ files = [ [[package]] name = "proto-plus" -version = "1.25.0" -description = "Beautiful, Pythonic protocol buffers." +version = "1.26.0" +description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" files = [ - {file = "proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961"}, - {file = "proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"}, + {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, + {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, ] [package.dependencies] @@ -3385,27 +3371,27 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.29.2" +version = "5.29.3" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-5.29.2-cp310-abi3-win32.whl", hash = "sha256:c12ba8249f5624300cf51c3d0bfe5be71a60c63e4dcf51ffe9a68771d958c851"}, - {file = "protobuf-5.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:842de6d9241134a973aab719ab42b008a18a90f9f07f06ba480df268f86432f9"}, - {file = "protobuf-5.29.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a0c53d78383c851bfa97eb42e3703aefdc96d2036a41482ffd55dc5f529466eb"}, - {file = "protobuf-5.29.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:494229ecd8c9009dd71eda5fd57528395d1eacdf307dbece6c12ad0dd09e912e"}, - {file = "protobuf-5.29.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b6b0d416bbbb9d4fbf9d0561dbfc4e324fd522f61f7af0fe0f282ab67b22477e"}, - {file = "protobuf-5.29.2-cp38-cp38-win32.whl", hash = "sha256:e621a98c0201a7c8afe89d9646859859be97cb22b8bf1d8eacfd90d5bda2eb19"}, - {file = "protobuf-5.29.2-cp38-cp38-win_amd64.whl", hash = "sha256:13d6d617a2a9e0e82a88113d7191a1baa1e42c2cc6f5f1398d3b054c8e7e714a"}, - {file = "protobuf-5.29.2-cp39-cp39-win32.whl", hash = "sha256:36000f97ea1e76e8398a3f02936aac2a5d2b111aae9920ec1b769fc4a222c4d9"}, - {file = "protobuf-5.29.2-cp39-cp39-win_amd64.whl", hash = "sha256:2d2e674c58a06311c8e99e74be43e7f3a8d1e2b2fdf845eaa347fbd866f23355"}, - {file = "protobuf-5.29.2-py3-none-any.whl", hash = "sha256:fde4554c0e578a5a0bcc9a276339594848d1e89f9ea47b4427c80e5d72f90181"}, - {file = "protobuf-5.29.2.tar.gz", hash = "sha256:b2cc8e8bb7c9326996f0e160137b0861f1a82162502658df2951209d0cb0309e"}, + {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, + {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, + {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, + {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, + {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, + {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, + {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, + {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, + {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, ] [[package]] name = "prowler" -version = "5.2.0" +version = "5.3.0" description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks." optional = false python-versions = ">=3.9,<3.13" @@ -3419,9 +3405,9 @@ azure-identity = "1.19.0" azure-keyvault-keys = "4.10.0" azure-mgmt-applicationinsights = "4.0.0" azure-mgmt-authorization = "4.0.0" -azure-mgmt-compute = "33.1.0" +azure-mgmt-compute = "34.0.0" azure-mgmt-containerregistry = "10.3.0" -azure-mgmt-containerservice = "33.0.0" +azure-mgmt-containerservice = "34.0.0" azure-mgmt-cosmosdb = "9.7.0" azure-mgmt-keyvault = "10.3.1" azure-mgmt-monitor = "6.0.2" @@ -3433,9 +3419,9 @@ azure-mgmt-security = "7.0.0" azure-mgmt-sql = "3.0.1" azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" -azure-mgmt-web = "7.3.1" -azure-storage-blob = "12.24.0" -boto3 = "1.35.94" +azure-mgmt-web = "8.0.0" +azure-storage-blob = "12.24.1" +boto3 = "1.35.99" botocore = "1.35.99" colorama = "0.4.6" cryptography = "43.0.1" @@ -3447,7 +3433,7 @@ google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "31.0.0" microsoft-kiota-abstractions = "1.6.8" -msgraph-sdk = "1.17.0" +msgraph-sdk = "1.18.0" numpy = "2.0.2" pandas = "2.2.3" py-ocsf-models = "0.2.0" @@ -3464,7 +3450,7 @@ tzlocal = "5.2" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "f6585078472e29e52913ec6460c1e7d960dd104f" +resolved_reference = "47bc2ed2dc472fc24ecab35970205448ab3b090d" [[package]] name = "psutil" @@ -3496,117 +3482,86 @@ files = [ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] -name = "psycopg" -version = "3.2.3" -description = "PostgreSQL database adapter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, - {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, -] - -[package.dependencies] -psycopg-binary = {version = "3.2.3", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} -psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} -typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} -tzdata = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -binary = ["psycopg-binary (==3.2.3)"] -c = ["psycopg-c (==3.2.3)"] -dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] -docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] -pool = ["psycopg-pool"] -test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] - -[[package]] -name = "psycopg-binary" -version = "3.2.3" -description = "PostgreSQL database adapter for Python -- C optimisation distribution" -optional = false -python-versions = ">=3.8" -files = [ - {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea"}, - {file = "psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585"}, - {file = "psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9"}, - {file = "psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd"}, - {file = "psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b"}, - {file = "psycopg_binary-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f"}, - {file = "psycopg_binary-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749"}, -] - -[[package]] -name = "psycopg-pool" -version = "3.2.4" -description = "Connection Pool for Psycopg" +name = "psycopg2-binary" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"}, - {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"}, + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, ] -[package.dependencies] -typing-extensions = ">=4.6" - [[package]] name = "py-ocsf-models" version = "0.2.0" @@ -3760,13 +3715,13 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -3821,13 +3776,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pyparsing" -version = "3.2.0" +version = "3.2.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" files = [ - {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, - {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, ] [package.extras] @@ -4150,18 +4105,19 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)" [[package]] name = "referencing" -version = "0.35.1" +version = "0.36.2" description = "JSON Referencing + Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, - {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, ] [package.dependencies] attrs = ">=22.2.0" rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" @@ -4376,13 +4332,13 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruamel-yaml" -version = "0.18.6" +version = "0.18.10" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3.7" files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, + {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, + {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, ] [package.dependencies] @@ -4552,23 +4508,23 @@ files = [ [[package]] name = "setuptools" -version = "75.6.0" +version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, - {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "shellingham" @@ -4766,13 +4722,13 @@ files = [ [[package]] name = "tzdata" -version = "2024.2" +version = "2025.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, ] [[package]] @@ -4805,13 +4761,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] @@ -4899,87 +4855,101 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "wrapt" -version = "1.17.0" +version = "1.17.2" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" files = [ - {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, - {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, - {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, - {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, - {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, - {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, - {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, - {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, - {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, - {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, - {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, - {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, - {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, - {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, - {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, - {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, - {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, - {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, - {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, - {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, - {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, - {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, - {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, ] [[package]] name = "xlsxwriter" -version = "3.2.0" +version = "3.2.1" description = "A Python module for creating Excel XLSX files." optional = false python-versions = ">=3.6" files = [ - {file = "XlsxWriter-3.2.0-py3-none-any.whl", hash = "sha256:ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e"}, - {file = "XlsxWriter-3.2.0.tar.gz", hash = "sha256:9977d0c661a72866a61f9f7a809e25ebbb0fb7036baa3b9fe74afcfca6b3cb8c"}, + {file = "XlsxWriter-3.2.1-py3-none-any.whl", hash = "sha256:7e8f7c60b7a1660ef791d46ab5de78469cb978b991ca841af61f5832d2f9f4fe"}, + {file = "XlsxWriter-3.2.1.tar.gz", hash = "sha256:97618759cb264fb6a93397f660cca156ffa9561743b1823dafb60dc4474e1902"}, ] [[package]] @@ -5100,4 +5070,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "7755046b3fc91d759b5827f370fabef742b2fd90a7fc419498275d1c0195beb7" +content-hash = "af973deaabe32ea4179c44ae702f6a8cc359aabe6f1641dcf2b05534b0bbb3d5" diff --git a/api/pyproject.toml b/api/pyproject.toml index 5442a1a6574..74d213b0fd4 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -28,7 +28,7 @@ drf-spectacular = "0.27.2" drf-spectacular-jsonapi = "0.5.1" gunicorn = "23.0.0" prowler = {git = "https://github.com/prowler-cloud/prowler.git", branch = "master"} -psycopg = {extras = ["pool", "binary"], version = "3.2.3"} +psycopg2-binary = "2.9.9" pytest-celery = {extras = ["redis"], version = "^1.0.1"} # Needed for prowler compatibility python = ">=3.11,<3.13" diff --git a/api/src/backend/api/db_utils.py b/api/src/backend/api/db_utils.py index b6fe6bd6b16..1bcf14209c3 100644 --- a/api/src/backend/api/db_utils.py +++ b/api/src/backend/api/db_utils.py @@ -6,10 +6,8 @@ from django.conf import settings from django.contrib.auth.models import BaseUserManager from django.db import connection, models, transaction -from psycopg import connect as psycopg_connect -from psycopg.adapt import Dumper -from psycopg.types import TypeInfo -from psycopg.types.string import TextLoader +from psycopg2 import connect as psycopg2_connect +from psycopg2.extensions import AsIs, new_type, register_adapter, register_type from rest_framework_json_api.serializers import ValidationError DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test" @@ -22,7 +20,6 @@ DB_PROWLER_PASSWORD = ( settings.DATABASES["prowler_user"]["PASSWORD"] if not settings.TESTING else "test" ) - TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult" POSTGRES_TENANT_VAR = "api.tenant_id" POSTGRES_USER_VAR = "api.user_id" @@ -32,25 +29,21 @@ @contextmanager def psycopg_connection(database_alias: str): - """ - Context manager returning a psycopg 3 connection - for the specified 'database_alias' in Django settings. - """ - pg_conn = None + psycopg2_connection = None try: admin_db = settings.DATABASES[database_alias] - pg_conn = psycopg_connect( + psycopg2_connection = psycopg2_connect( dbname=admin_db["NAME"], user=admin_db["USER"], password=admin_db["PASSWORD"], host=admin_db["HOST"], port=admin_db["PORT"], ) - yield pg_conn + yield psycopg2_connection finally: - if pg_conn is not None: - pg_conn.close() + if psycopg2_connection is not None: + psycopg2_connection.close() @contextmanager @@ -66,7 +59,7 @@ def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR): with transaction.atomic(): with connection.cursor() as cursor: try: - # Just in case the value is a UUID object + # just in case the value is an UUID object uuid.UUID(str(value)) except ValueError: raise ValidationError("Must be a valid UUID") @@ -194,24 +187,32 @@ def __str__(self): return self.value -def register_enum(apps, schema_editor, enum_class): - """ - psycopg 3 approach: register a loader + dumper for the given enum_class, - so we can read/write the custom Postgres ENUM seamlessly. - """ - with psycopg_connection(schema_editor.connection.alias) as conn: - ti = TypeInfo.fetch(conn, enum_class.enum_type_name) +def enum_adapter(enum_obj): + return AsIs(f"'{enum_obj.value}'::{enum_obj.__class__.enum_type_name}") - class EnumLoader(TextLoader): - def load(self, data): - return data - class EnumDumper(Dumper): - def dump(self, obj): - return f"'{obj.value}'::{obj.__class__.enum_type_name}" +def get_enum_oid(connection, enum_type_name: str): + with connection.cursor() as cursor: + cursor.execute("SELECT oid FROM pg_type WHERE typname = %s;", (enum_type_name,)) + result = cursor.fetchone() + if result is None: + raise ValueError(f"Enum type '{enum_type_name}' not found") + return result[0] + + +def register_enum(apps, schema_editor, enum_class): # noqa: F841 + with psycopg_connection(schema_editor.connection.alias) as connection: + enum_oid = get_enum_oid(connection, enum_class.enum_type_name) + enum_instance = new_type( + (enum_oid,), + enum_class.enum_type_name, + lambda value, cur: value, # noqa: F841 + ) + register_type(enum_instance, connection) + register_adapter(enum_class, enum_adapter) + - conn.adapters.register_loader(ti.oid, EnumLoader) - conn.adapters.register_dumper(enum_class, EnumDumper) +# Postgres enum definition for member role class MemberRoleEnum(EnumType): diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 12f0d782f25..f78cc94ab85 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -115,12 +115,6 @@ DATABASE_ROUTERS = ["api.db_router.MainRouter"] -# Database connection pool -DB_CP_MIN_SIZE = env.int("DJANGO_DB_CONNECTION_POOL_MIN_SIZE", 4) -DB_CP_MAX_SIZE = env.int("DJANGO_DB_CONNECTION_POOL_MAX_SIZE", 10) -DB_CP_MAX_IDLE = env.int("DJANGO_DB_CONNECTION_POOL_MAX_IDLE", 36000) -DB_CP_MAX_LIFETIME = env.int("DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME", 86400) - # Password validation # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators diff --git a/api/src/backend/config/django/devel.py b/api/src/backend/config/django/devel.py index a382b48fdb5..825e1ce36a9 100644 --- a/api/src/backend/config/django/devel.py +++ b/api/src/backend/config/django/devel.py @@ -13,14 +13,6 @@ "PASSWORD": env("POSTGRES_PASSWORD", default="prowler"), "HOST": env("POSTGRES_HOST", default="postgres-db"), "PORT": env("POSTGRES_PORT", default="5432"), - "OPTIONS": { - "pool": { - "min_size": DB_CP_MIN_SIZE, # noqa: F405 - "max_size": DB_CP_MAX_SIZE, # noqa: F405 - "max_idle": DB_CP_MAX_IDLE, # noqa: F405 - "max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405 - } - }, }, "admin": { "ENGINE": "psqlextra.backend", @@ -29,14 +21,6 @@ "PASSWORD": env("POSTGRES_ADMIN_PASSWORD", default="S3cret"), "HOST": env("POSTGRES_HOST", default="postgres-db"), "PORT": env("POSTGRES_PORT", default="5432"), - "OPTIONS": { - "pool": { - "min_size": DB_CP_MIN_SIZE, # noqa: F405 - "max_size": DB_CP_MAX_SIZE, # noqa: F405 - "max_idle": DB_CP_MAX_IDLE, # noqa: F405 - "max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405 - } - }, }, } DATABASES["default"] = DATABASES["prowler_user"] diff --git a/api/src/backend/config/django/production.py b/api/src/backend/config/django/production.py index 5197a296904..0f9479625c2 100644 --- a/api/src/backend/config/django/production.py +++ b/api/src/backend/config/django/production.py @@ -14,14 +14,6 @@ "PASSWORD": env("POSTGRES_PASSWORD"), "HOST": env("POSTGRES_HOST"), "PORT": env("POSTGRES_PORT"), - "OPTIONS": { - "pool": { - "min_size": DB_CP_MIN_SIZE, # noqa: F405 - "max_size": DB_CP_MAX_SIZE, # noqa: F405 - "max_idle": DB_CP_MAX_IDLE, # noqa: F405 - "max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405 - } - }, }, "admin": { "ENGINE": "psqlextra.backend", @@ -30,14 +22,6 @@ "PASSWORD": env("POSTGRES_ADMIN_PASSWORD"), "HOST": env("POSTGRES_HOST"), "PORT": env("POSTGRES_PORT"), - "OPTIONS": { - "pool": { - "min_size": DB_CP_MIN_SIZE, # noqa: F405 - "max_size": DB_CP_MAX_SIZE, # noqa: F405 - "max_idle": DB_CP_MAX_IDLE, # noqa: F405 - "max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405 - } - }, }, } DATABASES["default"] = DATABASES["prowler_user"] diff --git a/api/src/backend/config/django/testing.py b/api/src/backend/config/django/testing.py index 3ebdf9575aa..d7f1f941ff1 100644 --- a/api/src/backend/config/django/testing.py +++ b/api/src/backend/config/django/testing.py @@ -13,14 +13,6 @@ "PASSWORD": env("POSTGRES_PASSWORD", default="postgres"), "HOST": env("POSTGRES_HOST", default="localhost"), "PORT": env("POSTGRES_PORT", default="5432"), - "OPTIONS": { - "pool": { - "min_size": DB_CP_MIN_SIZE, # noqa: F405 - "max_size": DB_CP_MAX_SIZE, # noqa: F405 - "max_idle": DB_CP_MAX_IDLE, # noqa: F405 - "max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405 - } - }, }, } diff --git a/poetry.lock b/poetry.lock index 4e293e6759e..b0e6e0e4ad0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand. [[package]] name = "about-time" @@ -4431,7 +4431,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -4440,7 +4439,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -4449,7 +4447,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -4458,7 +4455,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -4467,7 +4463,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, From 545c2dc685f7db5cf7d88b4ca9ed97147b8f0039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Wed, 29 Jan 2025 09:39:04 +0100 Subject: [PATCH 12/27] fix(migrations): Use indexes instead of constraints to define an index (#6722) --- .../migrations/0007_scan_and_scan_summaries_indexes.py | 4 ++-- api/src/backend/api/models.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py b/api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py index 819016d1450..1ab3dbffc6a 100644 --- a/api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py +++ b/api/src/backend/api/migrations/0007_scan_and_scan_summaries_indexes.py @@ -16,9 +16,9 @@ class Migration(migrations.Migration): name="scans_prov_state_insert_idx", ), ), - migrations.AddConstraint( + migrations.AddIndex( model_name="scansummary", - constraint=models.Index( + index=models.Index( fields=["tenant_id", "scan_id"], name="scan_summaries_tenant_scan_idx" ), ), diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index bbb4663369b..09014321cb4 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1098,16 +1098,18 @@ class Meta(RowLevelSecurityProtectedModel.Meta): fields=("tenant", "scan", "check_id", "service", "severity", "region"), name="unique_scan_summary", ), - models.Index( - fields=["tenant_id", "scan_id"], - name="scan_summaries_tenant_scan_idx", - ), RowLevelSecurityConstraint( field="tenant_id", name="rls_on_%(class)s", statements=["SELECT", "INSERT", "UPDATE", "DELETE"], ), ] + indexes = [ + models.Index( + fields=["tenant_id", "scan_id"], + name="scan_summaries_tenant_scan_idx", + ) + ] class JSONAPIMeta: resource_name = "scan-summaries" From edd793c9f55e87f35d0060f1951ec0f4b99095a8 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 29 Jan 2025 10:46:49 +0100 Subject: [PATCH 13/27] fix(scans): change label for next scan (#6725) --- ui/components/scans/table/scans/column-get-scans.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx index 87113c21fad..55087a0e666 100644 --- a/ui/components/scans/table/scans/column-get-scans.tsx +++ b/ui/components/scans/table/scans/column-get-scans.tsx @@ -124,7 +124,7 @@ export const ColumnGetScans: ColumnDef[] = [ }, { accessorKey: "next_scan_at", - header: "Scheduled at", + header: "Next scan", cell: ({ row }) => { const { attributes: { next_scan_at }, From 350d7595177a4b0c935ee00eb7f812b7f6393024 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Thu, 30 Jan 2025 07:02:01 +0000 Subject: [PATCH 14/27] chore: Update Google Analytics ID across all docs.prowler.com sites. (#6730) --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index dd04e16881a..b3ee70140e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -124,7 +124,7 @@ extra: make our documentation better. analytics: provider: google - property: G-H5TFH6WJRQ + property: G-KBKV70W5Y2 social: - icon: fontawesome/brands/github link: https://github.com/prowler-cloud From 5b57079ecd5483f2f48f9b719b94571fe534cb37 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 30 Jan 2025 14:38:21 +0545 Subject: [PATCH 15/27] fix(sns): Add region to subscriptions (#6731) --- prowler/providers/aws/services/sns/sns_service.py | 2 ++ .../sns_subscription_not_using_http_endpoints_test.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/prowler/providers/aws/services/sns/sns_service.py b/prowler/providers/aws/services/sns/sns_service.py index 81474b4de6d..a2f2d3be29a 100644 --- a/prowler/providers/aws/services/sns/sns_service.py +++ b/prowler/providers/aws/services/sns/sns_service.py @@ -97,6 +97,7 @@ def _list_subscriptions_by_topic(self): owner=sub["Owner"], protocol=sub["Protocol"], endpoint=sub["Endpoint"], + region=sub["SubscriptionArn"].split(":")[3], ) for sub in response["Subscriptions"] ] @@ -117,6 +118,7 @@ class Subscription(BaseModel): owner: str protocol: str endpoint: str + region: str class Topic(BaseModel): diff --git a/tests/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints_test.py b/tests/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints_test.py index 456c09db8cd..ac4be798e54 100644 --- a/tests/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints_test.py +++ b/tests/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints_test.py @@ -65,6 +65,7 @@ def test_subscriptions_with_pending_confirmation(self): owner=AWS_ACCOUNT_NUMBER, protocol="https", endpoint="https://www.endpoint.com", + region=AWS_REGION_EU_WEST_1, ) ) sns_client.topics = [] @@ -100,6 +101,7 @@ def test_subscriptions_with_https(self): owner=AWS_ACCOUNT_NUMBER, protocol="https", endpoint="https://www.endpoint.com", + region=AWS_REGION_EU_WEST_1, ) ) sns_client.topics = [] @@ -131,6 +133,7 @@ def test_subscriptions_with_https(self): ) assert result[0].resource_id == subscription_id_1 assert result[0].resource_arn == subscription_arn_1 + assert result[0].region == AWS_REGION_EU_WEST_1 def test_subscriptions_with_http(self): sns_client = mock.MagicMock @@ -142,6 +145,7 @@ def test_subscriptions_with_http(self): owner=AWS_ACCOUNT_NUMBER, protocol="http", endpoint="http://www.endpoint.com", + region=AWS_REGION_EU_WEST_1, ) ) sns_client.topics = [] @@ -173,6 +177,7 @@ def test_subscriptions_with_http(self): ) assert result[0].resource_id == subscription_id_2 assert result[0].resource_arn == subscription_arn_2 + assert result[0].region == AWS_REGION_EU_WEST_1 def test_subscriptions_with_http_and_https(self): sns_client = mock.MagicMock @@ -184,6 +189,7 @@ def test_subscriptions_with_http_and_https(self): owner=AWS_ACCOUNT_NUMBER, protocol="https", endpoint="https://www.endpoint.com", + region=AWS_REGION_EU_WEST_1, ) ) subscriptions.append( @@ -193,6 +199,7 @@ def test_subscriptions_with_http_and_https(self): owner=AWS_ACCOUNT_NUMBER, protocol="http", endpoint="http://www.endpoint.com", + region=AWS_REGION_EU_WEST_1, ) ) sns_client.topics = [] @@ -224,6 +231,7 @@ def test_subscriptions_with_http_and_https(self): ) assert result[0].resource_id == subscription_id_1 assert result[0].resource_arn == subscription_arn_1 + assert result[0].region == AWS_REGION_EU_WEST_1 assert result[1].status == "FAIL" assert ( @@ -232,3 +240,4 @@ def test_subscriptions_with_http_and_https(self): ) assert result[1].resource_id == subscription_id_2 assert result[1].resource_arn == subscription_arn_2 + assert result[1].region == AWS_REGION_EU_WEST_1 From 8f0772cb946039175b87fc67cd86d0cc1ca39641 Mon Sep 17 00:00:00 2001 From: Kay Agahd Date: Thu, 30 Jan 2025 10:43:21 +0100 Subject: [PATCH 16/27] fix(aws): iam_user_with_temporary_credentials resource in OCSF (#6697) Co-authored-by: Pepe Fagoaga --- .../iam_user_with_temporary_credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.py b/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.py index 0734ddb0626..fc6ceb9c61e 100644 --- a/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.py +++ b/prowler/providers/aws/services/iam/iam_user_with_temporary_credentials/iam_user_with_temporary_credentials.py @@ -15,7 +15,7 @@ def execute(self) -> Check_Report_AWS: report = Check_Report_AWS( metadata=self.metadata(), - resource=iam_client.user_temporary_credentials_usage, + resource={"name": user_name, "arn": user_arn}, ) report.resource_id = user_name report.resource_arn = user_arn From bf2210d0f44fe3a9aed06284174b5cb35c8ab251 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 30 Jan 2025 15:54:31 +0545 Subject: [PATCH 17/27] fix(acm): Key Error DomainName (#6739) --- prowler/providers/aws/services/acm/acm_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prowler/providers/aws/services/acm/acm_service.py b/prowler/providers/aws/services/acm/acm_service.py index ad69a328390..cb9996831ce 100644 --- a/prowler/providers/aws/services/acm/acm_service.py +++ b/prowler/providers/aws/services/acm/acm_service.py @@ -57,7 +57,7 @@ def _list_certificates(self, regional_client): certificate_expiration_time = 0 self.certificates[certificate["CertificateArn"]] = Certificate( arn=certificate["CertificateArn"], - name=certificate["DomainName"], + name=certificate.get("DomainName", ""), id=certificate["CertificateArn"].split("/")[-1], type=certificate["Type"], key_algorithm=certificate["KeyAlgorithm"], From 82a1b1c9213f077b2617e444f7cd3d245612488e Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 30 Jan 2025 15:59:38 +0545 Subject: [PATCH 18/27] fix(finding): raise when generating invalid findings (#6738) --- prowler/__main__.py | 15 ++++++----- prowler/lib/outputs/finding.py | 8 +++++- prowler/lib/scan/scan.py | 16 ++++++----- tests/lib/outputs/finding_test.py | 44 +++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/prowler/__main__.py b/prowler/__main__.py index d1aee094eb5..c01f2a0a56d 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -332,18 +332,21 @@ def prowler(): # Outputs # TODO: this part is needed since the checks generates a Check_Report_XXX and the output uses Finding # This will be refactored for the outputs generate directly the Finding - finding_outputs = [ - Finding.generate_output(global_provider, finding, output_options) - for finding in findings - ] + finding_outputs = [] + for finding in findings: + try: + finding_outputs.append( + Finding.generate_output(global_provider, finding, output_options) + ) + except Exception: + continue generated_outputs = {"regular": [], "compliance": []} if args.output_formats: for mode in args.output_formats: filename = ( - f"{output_options.output_directory}/" - f"{output_options.output_filename}" + f"{output_options.output_directory}/{output_options.output_filename}" ) if mode == "csv": csv_output = CSV( diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 88db5bef383..62c5cfe47db 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationError from prowler.config.config import prowler_version from prowler.lib.check.models import Check_Report, CheckMetadata @@ -257,7 +257,13 @@ def generate_output( ) return cls(**output_data) + except ValidationError as validation_error: + logger.error( + f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error} - {output_data}" + ) + raise validation_error except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + raise error diff --git a/prowler/lib/scan/scan.py b/prowler/lib/scan/scan.py index 8c40db55a86..b5b621f28e4 100644 --- a/prowler/lib/scan/scan.py +++ b/prowler/lib/scan/scan.py @@ -296,12 +296,16 @@ def scan( self.get_completed_checks(), ) - findings = [ - Finding.generate_output( - self._provider, finding, output_options=None - ) - for finding in check_findings - ] + findings = [] + for finding in check_findings: + try: + findings.append( + Finding.generate_output( + self._provider, finding, output_options=None + ) + ) + except Exception: + continue yield self.progress, findings # If check does not exists in the provider or is from another provider diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index 98af2989068..6ae17416d04 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -1,6 +1,9 @@ from datetime import datetime from unittest.mock import MagicMock, patch +import pytest +from pydantic import ValidationError + from prowler.lib.check.models import ( CheckMetadata, Code, @@ -417,3 +420,44 @@ def test_get_metadata(self): assert metadata is not None assert isinstance(metadata, dict) self.assert_keys_lowercase(metadata) + + @patch( + "prowler.lib.outputs.finding.get_check_compliance", + new=mock_get_check_compliance, + ) + def test_generate_output_validation_error(self): + # Mock provider + provider = MagicMock() + provider.type = "aws" + provider.identity.profile = "mock_auth" + provider.identity.account = "mock_account_uid" + provider.identity.partition = "aws" + provider.organizations_metadata.account_name = "mock_account_name" + provider.organizations_metadata.account_email = "mock_account_email" + provider.organizations_metadata.organization_arn = "mock_account_org_uid" + provider.organizations_metadata.organization_id = "mock_account_org_name" + provider.organizations_metadata.account_tags = {"tag1": "value1"} + + # Mock check result + check_output = MagicMock() + check_output.resource_id = "test_resource_id" + check_output.resource_arn = "test_resource_arn" + check_output.resource_details = "test_resource_details" + check_output.resource_tags = {"tag1": "value1"} + check_output.region = "us-west-1" + check_output.partition = "aws" + check_output.status_extended = "mock_status_extended" + check_output.muted = False + check_output.check_metadata = mock_check_metadata(provider="aws") + check_output.resource = {} + + # Mock output options + output_options = MagicMock() + output_options.unix_timestamp = False + + # Bad Status Value + check_output.status = "Invalid" + + # Generate the finding + with pytest.raises(ValidationError): + Finding.generate_output(provider, check_output, output_options) From c159a28016d992f1f703a68c1f83f69215bed450 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 30 Jan 2025 17:16:18 +0545 Subject: [PATCH 19/27] fix(neptune): correct service name (#6743) --- .../neptune_cluster_iam_authentication_enabled.metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json index 5be9fa6c4fd..c6e29617f02 100644 --- a/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json +++ b/prowler/providers/aws/services/neptune/neptune_cluster_iam_authentication_enabled/neptune_cluster_iam_authentication_enabled.metadata.json @@ -3,7 +3,7 @@ "CheckID": "neptune_cluster_iam_authentication_enabled", "CheckTitle": "Check if Neptune Clusters have IAM authentication enabled.", "CheckType": [], - "ServiceName": "rds", + "ServiceName": "neptune", "SubServiceName": "", "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", "Severity": "medium", From 5061da6897943408cbf98a4183cfc26294411419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Thu, 30 Jan 2025 13:31:43 +0100 Subject: [PATCH 20/27] feat(findings): Improve /findings/metadata performance (#6748) --- api/poetry.lock | 55 ++++++++- api/pyproject.toml | 3 +- api/src/backend/api/db_router.py | 8 +- api/src/backend/api/filters.py | 47 ++++---- api/src/backend/api/specs/v1.yaml | 141 +----------------------- api/src/backend/api/tests/test_views.py | 32 +++--- api/src/backend/api/v1/serializers.py | 3 +- api/src/backend/api/v1/urls.py | 20 ++-- api/src/backend/api/v1/views.py | 81 ++++++++------ api/src/backend/config/django/devel.py | 6 + api/src/backend/tasks/jobs/scan.py | 8 +- 11 files changed, 175 insertions(+), 229 deletions(-) diff --git a/api/poetry.lock b/api/poetry.lock index ec24e9da8d4..491b06933ab 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -252,6 +252,20 @@ files = [ [package.dependencies] cryptography = "*" +[[package]] +name = "autopep8" +version = "2.3.2" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +optional = false +python-versions = ">=3.9" +files = [ + {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, + {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, +] + +[package.dependencies] +pycodestyle = ">=2.12.0" + [[package]] name = "awsipranges" version = "0.3.3" @@ -1488,6 +1502,23 @@ docs = ["Sphinx (==2.2.0)", "docutils (<0.18)", "sphinx-rtd-theme (==0.4.3)"] publish = ["build (==0.7.0)", "twine (==3.7.1)"] test = ["coveralls (==3.3.0)", "dj-database-url (==0.5.0)", "freezegun (==1.1.0)", "psycopg2 (>=2.8.4,<3.0.0)", "pytest (==6.2.5)", "pytest-benchmark (==3.4.1)", "pytest-cov (==3.0.0)", "pytest-django (==4.4.0)", "pytest-freezegun (==0.4.2)", "pytest-lazy-fixture (==0.6.3)", "snapshottest (==0.6.0)", "tox (==3.24.4)"] +[[package]] +name = "django-silk" +version = "5.3.2" +description = "Silky smooth profiling for the Django Framework" +optional = false +python-versions = ">=3.9" +files = [ + {file = "django_silk-5.3.2-py3-none-any.whl", hash = "sha256:49f1caebfda28b1707f0cfef524e0476beb82b8c5e40f5ccff7f73a6b4f6d3ac"}, + {file = "django_silk-5.3.2.tar.gz", hash = "sha256:b0db54eebedb8d16f572321bd6daccac0bd3f547ae2618bb45d96fe8fc02229d"}, +] + +[package.dependencies] +autopep8 = "*" +Django = ">=4.2" +gprof2dot = ">=2017.09.19" +sqlparse = "*" + [[package]] name = "django-timezone-field" version = "7.1" @@ -1984,6 +2015,17 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4 [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +[[package]] +name = "gprof2dot" +version = "2024.6.6" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=3.8" +files = [ + {file = "gprof2dot-2024.6.6-py2.py3-none-any.whl", hash = "sha256:45b14ad7ce64e299c8f526881007b9eb2c6b75505d5613e96e66ee4d5ab33696"}, + {file = "gprof2dot-2024.6.6.tar.gz", hash = "sha256:fa1420c60025a9eb7734f65225b4da02a10fc6dd741b37fa129bc6b41951e5ab"}, +] + [[package]] name = "grapheme" version = "0.6.0" @@ -3603,6 +3645,17 @@ files = [ [package.dependencies] pyasn1 = ">=0.4.6,<0.7.0" +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + [[package]] name = "pycparser" version = "2.22" @@ -5070,4 +5123,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "af973deaabe32ea4179c44ae702f6a8cc359aabe6f1641dcf2b05534b0bbb3d5" +content-hash = "6465edb36efd1fa6db06d4103fea8046951acc3f4f8b357facaaa34ae2bc74bd" diff --git a/api/pyproject.toml b/api/pyproject.toml index 74d213b0fd4..3c7338eb53a 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -8,7 +8,7 @@ description = "Prowler's API (Django/DRF)" license = "Apache-2.0" name = "prowler-api" package-mode = false -version = "1.3.1" +version = "1.3.2" [tool.poetry.dependencies] celery = {extras = ["pytest"], version = "^5.4.0"} @@ -37,6 +37,7 @@ uuid6 = "2024.7.10" [tool.poetry.group.dev.dependencies] bandit = "1.7.9" coverage = "7.5.4" +django-silk = "5.3.2" docker = "7.1.0" freezegun = "1.5.1" mypy = "1.10.1" diff --git a/api/src/backend/api/db_router.py b/api/src/backend/api/db_router.py index f3a986ac206..939672f88e3 100644 --- a/api/src/backend/api/db_router.py +++ b/api/src/backend/api/db_router.py @@ -4,13 +4,17 @@ class MainRouter: def db_for_read(self, model, **hints): # noqa: F841 model_table_name = model._meta.db_table - if model_table_name.startswith("django_"): + if model_table_name.startswith("django_") or model_table_name.startswith( + "silk_" + ): 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_"): + if model_table_name.startswith("django_") or model_table_name.startswith( + "silk_" + ): return self.admin_db return None diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 1042c3aa861..0ace11fe9cf 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -319,26 +319,27 @@ class FindingFilter(FilterSet): field_name="resources__type", lookup_expr="icontains" ) - resource_tag_key = CharFilter(field_name="resources__tags__key") - resource_tag_key__in = CharInFilter( - field_name="resources__tags__key", lookup_expr="in" - ) - resource_tag_key__icontains = CharFilter( - field_name="resources__tags__key", lookup_expr="icontains" - ) - resource_tag_value = CharFilter(field_name="resources__tags__value") - resource_tag_value__in = CharInFilter( - field_name="resources__tags__value", lookup_expr="in" - ) - resource_tag_value__icontains = CharFilter( - field_name="resources__tags__value", lookup_expr="icontains" - ) - resource_tags = CharInFilter( - method="filter_resource_tag", - lookup_expr="in", - help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be " - "separated by commas.", - ) + # Temporarily disabled until we implement tag filtering in the UI + # resource_tag_key = CharFilter(field_name="resources__tags__key") + # resource_tag_key__in = CharInFilter( + # field_name="resources__tags__key", lookup_expr="in" + # ) + # resource_tag_key__icontains = CharFilter( + # field_name="resources__tags__key", lookup_expr="icontains" + # ) + # resource_tag_value = CharFilter(field_name="resources__tags__value") + # resource_tag_value__in = CharInFilter( + # field_name="resources__tags__value", lookup_expr="in" + # ) + # resource_tag_value__icontains = CharFilter( + # field_name="resources__tags__value", lookup_expr="icontains" + # ) + # resource_tags = CharInFilter( + # method="filter_resource_tag", + # lookup_expr="in", + # help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be " + # "separated by commas.", + # ) scan = UUIDFilter(method="filter_scan_id") scan__in = UUIDInFilter(method="filter_scan_id_in") @@ -374,12 +375,6 @@ class Meta: }, } - @property - def qs(self): - # Force distinct results to prevent duplicates with many-to-many relationships - parent_qs = super().qs - return parent_qs.distinct() - # Convert filter values to UUIDv7 values for use with partitioning def filter_scan_id(self, queryset, name, value): try: diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index cb355b1aa7b..7448843b54d 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.3.1 + version: 1.3.2 description: |- Prowler API specification. @@ -478,51 +478,6 @@ paths: description: Multiple values may be separated by commas. explode: false style: form - - in: query - name: filter[resource_tag_key] - schema: - type: string - - in: query - name: filter[resource_tag_key__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_key__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tag_value] - schema: - type: string - - in: query - name: filter[resource_tag_value__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_value__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tags] - schema: - type: array - items: - type: string - description: |- - Filter by resource tags `key:value` pairs. - Multiple values may be separated by commas. - explode: false - style: form - in: query name: filter[resource_type] schema: @@ -1028,51 +983,6 @@ paths: description: Multiple values may be separated by commas. explode: false style: form - - in: query - name: filter[resource_tag_key] - schema: - type: string - - in: query - name: filter[resource_tag_key__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_key__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tag_value] - schema: - type: string - - in: query - name: filter[resource_tag_value__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_value__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tags] - schema: - type: array - items: - type: string - description: |- - Filter by resource tags `key:value` pairs. - Multiple values may be separated by commas. - explode: false - style: form - in: query name: filter[resource_type] schema: @@ -1280,7 +1190,6 @@ paths: - services - regions - resource_types - - tags description: endpoint return only specific fields in the response on a per-type basis by including a fields[TYPE] query parameter. explode: false @@ -1498,51 +1407,6 @@ paths: description: Multiple values may be separated by commas. explode: false style: form - - in: query - name: filter[resource_tag_key] - schema: - type: string - - in: query - name: filter[resource_tag_key__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_key__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tag_value] - schema: - type: string - - in: query - name: filter[resource_tag_value__icontains] - schema: - type: string - - in: query - name: filter[resource_tag_value__in] - schema: - type: array - items: - type: string - description: Multiple values may be separated by commas. - explode: false - style: form - - in: query - name: filter[resource_tags] - schema: - type: array - items: - type: string - description: |- - Filter by resource tags `key:value` pairs. - Multiple values may be separated by commas. - explode: false - style: form - in: query name: filter[resource_type] schema: @@ -6081,13 +5945,10 @@ components: type: array items: type: string - tags: - description: Tags are described as key-value pairs. required: - services - regions - resource_types - - tags FindingMetadataResponse: type: object properties: diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 184843af185..01737970625 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -2454,15 +2454,16 @@ def test_findings_list_include( ("search", "ec2", 2), # full text search on finding tags ("search", "value2", 2), - ("resource_tag_key", "key", 2), - ("resource_tag_key__in", "key,key2", 2), - ("resource_tag_key__icontains", "key", 2), - ("resource_tag_value", "value", 2), - ("resource_tag_value__in", "value,value2", 2), - ("resource_tag_value__icontains", "value", 2), - ("resource_tags", "key:value", 2), - ("resource_tags", "not:exists", 0), - ("resource_tags", "not:exists,key:value", 2), + # Temporary disabled until we implement tag filtering in the UI + # ("resource_tag_key", "key", 2), + # ("resource_tag_key__in", "key,key2", 2), + # ("resource_tag_key__icontains", "key", 2), + # ("resource_tag_value", "value", 2), + # ("resource_tag_value__in", "value,value2", 2), + # ("resource_tag_value__icontains", "value", 2), + # ("resource_tags", "key:value", 2), + # ("resource_tags", "not:exists", 0), + # ("resource_tags", "not:exists,key:value", 2), ] ), ) @@ -2611,7 +2612,8 @@ def test_findings_metadata_retrieve(self, authenticated_client, findings_fixture expected_services = {"ec2", "s3"} expected_regions = {"eu-west-1", "us-east-1"} - expected_tags = {"key": ["value"], "key2": ["value2"]} + # Temporarily disabled until we implement tag filtering in the UI + # expected_tags = {"key": ["value"], "key2": ["value2"]} expected_resource_types = {"prowler-test"} assert data["data"]["type"] == "findings-metadata" @@ -2621,7 +2623,7 @@ def test_findings_metadata_retrieve(self, authenticated_client, findings_fixture assert ( set(data["data"]["attributes"]["resource_types"]) == expected_resource_types ) - assert data["data"]["attributes"]["tags"] == expected_tags + # assert data["data"]["attributes"]["tags"] == expected_tags def test_findings_metadata_severity_retrieve( self, authenticated_client, findings_fixture @@ -2638,7 +2640,8 @@ def test_findings_metadata_severity_retrieve( expected_services = {"s3"} expected_regions = {"eu-west-1"} - expected_tags = {"key": ["value"], "key2": ["value2"]} + # Temporary disabled until we implement tag filtering in the UI + # expected_tags = {"key": ["value"], "key2": ["value2"]} expected_resource_types = {"prowler-test"} assert data["data"]["type"] == "findings-metadata" @@ -2648,7 +2651,7 @@ def test_findings_metadata_severity_retrieve( assert ( set(data["data"]["attributes"]["resource_types"]) == expected_resource_types ) - assert data["data"]["attributes"]["tags"] == expected_tags + # assert data["data"]["attributes"]["tags"] == expected_tags def test_findings_metadata_future_date(self, authenticated_client): response = authenticated_client.get( @@ -2660,7 +2663,8 @@ def test_findings_metadata_future_date(self, authenticated_client): assert data["data"]["id"] is None assert data["data"]["attributes"]["services"] == [] assert data["data"]["attributes"]["regions"] == [] - assert data["data"]["attributes"]["tags"] == {} + # Temporary disabled until we implement tag filtering in the UI + # assert data["data"]["attributes"]["tags"] == {} assert data["data"]["attributes"]["resource_types"] == [] def test_findings_metadata_invalid_date(self, authenticated_client): diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 6046799d0cf..4f17d2d61cd 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -933,7 +933,8 @@ class FindingMetadataSerializer(serializers.Serializer): resource_types = serializers.ListField( child=serializers.CharField(), allow_empty=True ) - tags = serializers.JSONField(help_text="Tags are described as key-value pairs.") + # Temporarily disabled until we implement tag filtering in the UI + # tags = serializers.JSONField(help_text="Tags are described as key-value pairs.") class Meta: resource_name = "findings-metadata" diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index fd3f9ad18a9..6b230960e1d 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -1,30 +1,31 @@ +from django.conf import settings from django.urls import include, path from drf_spectacular.views import SpectacularRedocView from rest_framework_nested import routers from api.v1.views import ( + ComplianceOverviewViewSet, CustomTokenObtainView, CustomTokenRefreshView, FindingViewSet, + InvitationAcceptViewSet, + InvitationViewSet, MembershipViewSet, - ProviderGroupViewSet, + OverviewViewSet, ProviderGroupProvidersRelationshipView, + ProviderGroupViewSet, ProviderSecretViewSet, - InvitationViewSet, - InvitationAcceptViewSet, - RoleViewSet, - RoleProviderGroupRelationshipView, - UserRoleRelationshipView, - OverviewViewSet, - ComplianceOverviewViewSet, ProviderViewSet, ResourceViewSet, + RoleProviderGroupRelationshipView, + RoleViewSet, ScanViewSet, ScheduleViewSet, SchemaView, TaskViewSet, TenantMembersViewSet, TenantViewSet, + UserRoleRelationshipView, UserViewSet, ) @@ -112,3 +113,6 @@ path("schema", SchemaView.as_view(), name="schema"), path("docs", SpectacularRedocView.as_view(url_name="schema"), name="docs"), ] + +if settings.DEBUG: + urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))] diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index debfcdd57c0..3ecb6474fb3 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -4,7 +4,7 @@ from django.contrib.postgres.search import SearchQuery from django.db import transaction from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum -from django.db.models.functions import Coalesce, JSONObject +from django.db.models.functions import Coalesce from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control @@ -193,7 +193,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.3.1" + spectacular_settings.VERSION = "1.3.2" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -1392,48 +1392,59 @@ def findings_services_regions(self, request): @action(detail=False, methods=["get"], url_name="metadata") def metadata(self, request): + tenant_id = self.request.tenant_id queryset = self.get_queryset() filtered_queryset = self.filter_queryset(queryset) - result = filtered_queryset.aggregate( - services=ArrayAgg("resources__service", flat=True, distinct=True), - regions=ArrayAgg("resources__region", flat=True, distinct=True), - tags=ArrayAgg( - JSONObject( - key=F("resources__tags__key"), value=F("resources__tags__value") - ), - distinct=True, - filter=Q(resources__tags__key__isnull=False), - ), - resource_types=ArrayAgg("resources__type", flat=True, distinct=True), - ) - if result["services"] is None: - result["services"] = [] - if result["regions"] is None: - result["regions"] = [] - if result["regions"] is None: - result["regions"] = [] - if result["resource_types"] is None: - result["resource_types"] = [] - if result["tags"] is None: - result["tags"] = [] + relevant_resources = Resource.objects.filter( + tenant_id=tenant_id, findings__in=filtered_queryset + ).distinct() - tags_dict = {} - for t in result["tags"]: - key, value = t["key"], t["value"] - if key not in tags_dict: - tags_dict[key] = [] - tags_dict[key].append(value) + services = ( + relevant_resources.values_list("service", flat=True) + .distinct() + .order_by("service") + ) - result["tags"] = tags_dict + regions = ( + relevant_resources.exclude(region="") + .values_list("region", flat=True) + .distinct() + .order_by("region") + ) - serializer = self.get_serializer( - data=result, + resource_types = ( + relevant_resources.values_list("type", flat=True) + .distinct() + .order_by("type") ) - serializer.is_valid(raise_exception=True) + # Temporarily disabled until we implement tag filtering in the UI + # tag_data = ( + # relevant_resources + # .filter(tags__key__isnull=False, tags__value__isnull=False) + # .exclude(tags__key="") + # .exclude(tags__value="") + # .values("tags__key", "tags__value") + # .distinct() + # .order_by("tags__key", "tags__value") + # ) + # + # tags_dict = {} + # for row in tag_data: + # k, v = row["tags__key"], row["tags__value"] + # tags_dict.setdefault(k, []).append(v) + + result = { + "services": list(services), + "regions": list(regions), + "resource_types": list(resource_types), + # "tags": tags_dict + } - return Response(data=serializer.data, status=status.HTTP_200_OK) + serializer = self.get_serializer(data=result) + serializer.is_valid(raise_exception=True) + return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema_view( diff --git a/api/src/backend/config/django/devel.py b/api/src/backend/config/django/devel.py index 825e1ce36a9..6ee92a6ecb8 100644 --- a/api/src/backend/config/django/devel.py +++ b/api/src/backend/config/django/devel.py @@ -37,3 +37,9 @@ ) + ("api.filters.CustomDjangoFilterBackend",) SECRETS_ENCRYPTION_KEY = "ZMiYVo7m4Fbe2eXXPyrwxdJss2WSalXSv3xHBcJkPl0=" + +MIDDLEWARE += [ # noqa: F405 + "silk.middleware.SilkyMiddleware", +] + +INSTALLED_APPS += ["silk"] # noqa: F405 diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 25993f5bbcf..89612ae6fd9 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -152,6 +152,9 @@ def perform_prowler_scan( for progress, findings in prowler_scan.scan(): for finding in findings: + if finding is None: + logger.error(f"None finding detected on scan {scan_id}.") + continue for attempt in range(CELERY_DEADLOCK_ATTEMPTS): try: with rls_transaction(tenant_id): @@ -176,7 +179,10 @@ def perform_prowler_scan( # Update resource fields if necessary updated_fields = [] - if resource_instance.region != finding.region: + if ( + finding.region + and resource_instance.region != finding.region + ): resource_instance.region = finding.region updated_fields.append("region") if resource_instance.service != finding.service_name: From 5bfaedf903bf06e0b3b07a01c8b3d818a84bdb15 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 30 Jan 2025 14:05:39 +0100 Subject: [PATCH 21/27] fix: Enable hot reloading when using Docker Compose for UI (#6750) --- docker-compose-dev.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index d49af9dfbb3..c8ecea96d53 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -35,6 +35,9 @@ services: required: false ports: - 3000:3000 + volumes: + - "./ui:/app" + - "/app/node_modules" postgres: image: postgres:16.3-alpine3.20 From 5186e029b3fc89db58517c6f059739ee256dddb0 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 30 Jan 2025 20:48:51 +0545 Subject: [PATCH 22/27] fix(set_report_color): Add more details to error (#6751) --- prowler/lib/outputs/outputs.py | 4 +++- tests/lib/outputs/outputs_test.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index 2a61e17c364..b4699157d74 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -83,7 +83,9 @@ def set_report_color(status: str, muted: bool = False) -> str: elif status == "MANUAL": color = Fore.YELLOW else: - raise Exception("Invalid Report Status. Must be PASS, FAIL or MANUAL.") + raise Exception( + f"Invalid Report Status: {status}. Must be PASS, FAIL or MANUAL." + ) return color diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py index 624366af5e6..dbf7d5c8bd9 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -21,7 +21,6 @@ class TestOutputs: - def test_set_report_color(self): test_status = ["PASS", "FAIL", "MANUAL"] test_colors = [Fore.GREEN, Fore.RED, Fore.YELLOW] @@ -35,8 +34,9 @@ def test_set_report_color_invalid(self): with pytest.raises(Exception) as exc: set_report_color(test_status) - assert "Invalid Report Status. Must be PASS, FAIL or MANUAL" in str(exc.value) - assert exc.type == Exception + assert "Invalid Report Status: INVALID. Must be PASS, FAIL or MANUAL" in str( + exc.value + ) def test_unroll_list_no_separator(self): list = ["test", "test1", "test2"] From 712ba84f068e7f158658e1129363cd46ab749f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Thu, 30 Jan 2025 16:06:12 +0100 Subject: [PATCH 23/27] feat(scans): Optimize read queries during scans (#6753) --- api/src/backend/tasks/jobs/scan.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 89612ae6fd9..b11366a859f 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -228,8 +228,10 @@ def perform_prowler_scan( last_first_seen_at = None if finding_uid not in last_status_cache: most_recent_finding = ( - Finding.objects.filter(uid=finding_uid) - .order_by("-id") + Finding.all_objects.filter( + tenant_id=tenant_id, uid=finding_uid + ) + .order_by("-inserted_at") .values("status", "first_seen_at") .first() ) @@ -378,7 +380,7 @@ def aggregate_findings(tenant_id: str, scan_id: str): - muted_changed: Muted findings with a delta of 'changed'. """ with rls_transaction(tenant_id): - findings = Finding.objects.filter(scan_id=scan_id) + findings = Finding.objects.filter(tenant_id=tenant_id, scan_id=scan_id) aggregation = findings.values( "check_id", From 627c11503f1484db1728184bf19bf0dd458d4cdd Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Thu, 30 Jan 2025 21:46:43 +0545 Subject: [PATCH 24/27] fix(db_event): Handle other events (#6754) --- ...ds_instance_critical_event_subscription.py | 3 ++ ...stance_critical_event_subscription_test.py | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py index e2f379232b5..43751d73697 100644 --- a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py +++ b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py @@ -54,6 +54,9 @@ def execute(self): }: report.status = "FAIL" report.status_extended = "RDS instance event category of maintenance is not subscribed." + else: + report.status = "FAIL" + report.status_extended = "RDS instance event categories of maintenance, configuration change, and failure are not subscribed." findings.append(report) return findings diff --git a/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py b/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py index d72df2303ac..02729be2316 100644 --- a/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py +++ b/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py @@ -483,3 +483,52 @@ def test_rds_instance_event_failure_and_maintenance(self): == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) assert result[0].resource_tags == [] + + @mock_aws + def test_rds_instance_event_invalid(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_parameter_group( + DBParameterGroupName="test", + DBParameterGroupFamily="default.aurora-postgresql14", + Description="test parameter group", + ) + conn.create_event_subscription( + SubscriptionName="TestSub", + SnsTopicArn=f"arn:aws:sns:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:test", + SourceType="db-instance", + EventCategories=["invalid"], + Enabled=True, + ) + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_critical_event_subscription.rds_instance_critical_event_subscription.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_critical_event_subscription.rds_instance_critical_event_subscription import ( + rds_instance_critical_event_subscription, + ) + + check = rds_instance_critical_event_subscription() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS instance event categories of maintenance, configuration change, and failure are not subscribed." + ) + assert result[0].resource_id == "TestSub" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" + ) + assert result[0].resource_tags == [] From 18b7b48a991ca0941bd8c41f4f8963ae2e49db3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:07:17 +0100 Subject: [PATCH 25/27] chore(deps): bump microsoft-kiota-abstractions from 1.6.8 to 1.9.1 (#6734) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 188 ++++++++++--------------------------------------- pyproject.toml | 2 +- 2 files changed, 37 insertions(+), 153 deletions(-) diff --git a/poetry.lock b/poetry.lock index b0e6e0e4ad0..cbb2504f70b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "about-time" @@ -2359,13 +2359,13 @@ files = [ [[package]] name = "microsoft-kiota-abstractions" -version = "1.6.8" +version = "1.9.1" 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.1-py3-none-any.whl", hash = "sha256:aca6147946fc56167038faddda0a07026b0ec4e76666520586de7ac5c1233295"}, + {file = "microsoft_kiota_abstractions-1.9.1.tar.gz", hash = "sha256:291f4f88bec5452b2c6bafed4927640d2c7d2f41b08c941ee786f6ecad3dcaa5"}, ] [package.dependencies] @@ -2375,19 +2375,19 @@ std-uritemplate = ">=2.0.0" [[package]] name = "microsoft-kiota-authentication-azure" -version = "1.6.8" +version = "1.9.1" 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.1-py3-none-any.whl", hash = "sha256:3a3030b01e0cbf007736ec6548ac483742e04ad0d20979b49e293a683f839fc6"}, + {file = "microsoft_kiota_authentication_azure-1.9.1.tar.gz", hash = "sha256:8ba31b1ecf78777128daf3191c7ecd28bfc2d10d0fe80260dd5c07a9ccd5d7f5"}, ] [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.1,<1.10.0" opentelemetry-api = ">=1.27.0" opentelemetry-sdk = ">=1.27.0" @@ -2408,83 +2408,61 @@ microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" opentelemetry-api = ">=1.20.0" opentelemetry-sdk = ">=1.20.0" -[[package]] -name = "microsoft-kiota-http" -version = "1.6.8" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "<4.0,>=3.8" -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"}, -] - -[package.dependencies] -httpx = {version = ">=0.28", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.6.8,<1.7.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.1" 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.1-py3-none-any.whl", hash = "sha256:d86c0dc08b51288f2851a265dde729b200998bca1dd1681bbebcb27cd274ba34"}, + {file = "microsoft_kiota_serialization_form-1.9.1.tar.gz", hash = "sha256:58b81eca5e0ad66bcbd6a4b65ba91e6255d3510f4c4f576fb4f5e83ca9a310c3"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "microsoft-kiota-serialization-json" -version = "1.6.8" +version = "1.9.1" 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.1-py3-none-any.whl", hash = "sha256:ab752bf642a77266713bac3942a4c547dde60b917f674a9ab63261490fecf841"}, + {file = "microsoft_kiota_serialization_json-1.9.1.tar.gz", hash = "sha256:0039458b885875daf246f1e8c0296551695e0ec70f3e4689b00b270ce923c0cc"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "microsoft-kiota-serialization-multipart" -version = "1.6.8" +version = "1.9.1" 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.1-py3-none-any.whl", hash = "sha256:1ee69d8e3b2c0d24431b85fc1628534f97dcafed45dcb6bb3f0143824ce8a665"}, + {file = "microsoft_kiota_serialization_multipart-1.9.1.tar.gz", hash = "sha256:288790a486aad33aac0a7329f05b8851e9be82f50cc9a8f19fe6f267a4f2b183"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -pendulum = ">=3.0.0b1" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "microsoft-kiota-serialization-text" -version = "1.6.8" +version = "1.9.1" 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.1-py3-none-any.whl", hash = "sha256:021fbfad887f7525f9e1c8bbe225d0aa1b25befe54357ae0f739f47576d946ff"}, + {file = "microsoft_kiota_serialization_text-1.9.1.tar.gz", hash = "sha256:b4b1f61c2388edd704ab33a1792f92f3507db9648d389197a625e152b52743ab"}, ] [package.dependencies] -microsoft-kiota-abstractions = ">=1.6.8,<1.7.0" -python-dateutil = "2.9.0.post0" +microsoft-kiota-abstractions = ">=1.9.1,<1.10.0" [[package]] name = "mkdocs" @@ -3241,105 +3219,6 @@ files = [ {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"}, -] - -[package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] - [[package]] name = "platformdirs" version = "4.3.6" @@ -4431,6 +4310,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -4439,6 +4319,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -4447,6 +4328,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -4455,6 +4337,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -4463,6 +4346,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -5206,4 +5090,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "f2d3920001616ff9b66353b61d20dadc358787aa154c31fcbd8f4baf45c36498" +content-hash = "19e7c4654f280256e53757abd04cb63938d34ae4d8305a77f21e75c891b5376d" diff --git a/pyproject.toml b/pyproject.toml index ff3156c743b..9cb926c96ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ google-api-python-client = "2.159.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "31.0.0" -microsoft-kiota-abstractions = "1.6.8" +microsoft-kiota-abstractions = "1.9.1" msgraph-sdk = "1.18.0" numpy = "2.0.2" pandas = "2.2.3" From 1256c040e9d4bcb02633b86f1523608e3f0804a3 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:32:39 +0100 Subject: [PATCH 26/27] fix: microsoft365 mutelist (#6724) --- prowler/lib/check/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 2aca0e83c18..928d68f0755 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -541,6 +541,7 @@ class Check_Report_Microsoft365(Check_Report): resource_name: str resource_id: str + tenant_id: str location: str def __init__(self, metadata: Dict, resource: Any) -> None: @@ -555,6 +556,7 @@ def __init__(self, metadata: Dict, resource: Any) -> None: resource, "name", getattr(resource, "resource_name", "") ) self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", "")) + self.tenant_id = getattr(resource, "tenant_id", "") self.location = getattr(resource, "location", "global") From 763130f253cb119d30b413f0b917abfbe427193b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Fri, 31 Jan 2025 14:45:08 +0100 Subject: [PATCH 27/27] fix(celery): Kill celery worker process after every task to release memory (#6761) --- api/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/docker-entrypoint.sh b/api/docker-entrypoint.sh index 62ceb69a0fc..893d94af210 100755 --- a/api/docker-entrypoint.sh +++ b/api/docker-entrypoint.sh @@ -28,7 +28,7 @@ start_prod_server() { start_worker() { echo "Starting the worker..." - poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E + poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E --max-tasks-per-child 1 } start_worker_beat() {